Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/api/src/storage/task-board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,8 @@ export class TaskBoardStorage {
const rows = await this.db
.selectFrom("task_board_items as i")
.select(["i.id", "i.organization_id as organizationId"])
.where("i.status", "=", "done")
// Not just Done — a delivery-lane org's shipped cards never reach it.
.where("i.status", "in", TAGGABLE_MERGED_STATUSES)
.where("i.dismissed_at", "is", null)
.where("i.updated_at", "<", settledBefore)
.where((eb) =>
Expand Down
13 changes: 6 additions & 7 deletions apps/api/src/tools/task-board/archive-merged.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ describe("auto-archive sweep", () => {
]);
});

// The delivery lanes sit BEFORE Done, so a card parked in one is still in flight.
it("never sweeps a card resting in a delivery lane", async () => {
// A shipped delivery-lane card never reaches literal `done` — see `shippedLane`.
it("sweeps a settled card resting in a delivery lane, same as Done", async () => {
const settled = new Date(Date.now() - 3 * DAY_MS);
const lanes = ["approved", "merged", "post_deploy_validation"] as const;
const parked = await Promise.all(
Expand All @@ -131,16 +131,15 @@ describe("auto-archive sweep", () => {
200,
);
const ids = candidates.map((c) => c.id);
for (const id of parked) expect(ids).not.toContain(id);
for (const id of parked) expect(ids).toContain(id);

// And the write path refuses too, even when handed the id directly.
const swept = await archiveMergedForOrg(ctx, ORG, parked, async () => ({
state: "closed" as const,
merged: true,
}));
expect(swept.archived).toBe(0);
for (const [i, id] of parked.entries()) {
expect((await taskBoard.getById(id, ORG))?.status).toBe(lanes[i]);
expect(swept.archived).toBe(lanes.length);
for (const id of parked) {
expect((await taskBoard.getById(id, ORG))?.status).toBe("archived");
}
});

Expand Down
15 changes: 10 additions & 5 deletions apps/api/src/tools/task-board/archive-merged.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { LANES } from "@decocms/shared/task-board";
import type { StudioContext } from "@/core/studio-context";
import type { TaskBoardItemPrRef } from "@/storage/types";
import { recordTaskActivity } from "./activity";
import { isTaggableMergedStatus } from "./lanes";
import { fetchPrLanding } from "./prs-get";
import { emitTaskBoardUpdated } from "./run-reactions";

Expand Down Expand Up @@ -90,9 +91,9 @@ function repoLanded(prs: PrLanding[]): boolean {
/**
* Archive one candidate if its PRs are all merged. Returns whether it moved.
*
* Re-reads the card inside the org's context and re-checks `status === "done"`:
* the sweep's work list is a snapshot, and a card someone dragged out of Done
* (or another replica already archived) in the meantime must not be moved.
* Re-reads the card inside the org's context and re-checks it is still a
* shipped status: the sweep's work list is a snapshot, and a card someone
* dragged backward (or another replica already archived) must not be moved.
*/
async function archiveIfMerged(
ctx: StudioContext,
Expand All @@ -101,7 +102,7 @@ async function archiveIfMerged(
prLanding: PrLandingReader,
): Promise<boolean> {
const item = await ctx.storage.taskBoard.getById(itemId, organizationId);
if (!item || item.status !== "done") return false;
if (!item || !isTaggableMergedStatus(item.status)) return false;

const prs = await ctx.storage.taskBoard.listPrs(itemId, organizationId);
const landings = await Promise.all(
Expand All @@ -124,7 +125,11 @@ async function archiveIfMerged(
taskBoardItemId: itemId,
action: "status_changed",
actorId: null,
data: { from: "done", to: archived, reason: "merged_pr_auto_archive" },
data: {
from: item.status,
to: archived,
reason: "merged_pr_auto_archive",
},
});
emitTaskBoardUpdated(organizationId, updated);
return true;
Expand Down
Loading