diff --git a/apps/api/src/storage/task-board.ts b/apps/api/src/storage/task-board.ts index b67ce8a7d0..9badc7dbaa 100644 --- a/apps/api/src/storage/task-board.ts +++ b/apps/api/src/storage/task-board.ts @@ -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) => diff --git a/apps/api/src/tools/task-board/archive-merged.integration.test.ts b/apps/api/src/tools/task-board/archive-merged.integration.test.ts index 372e4c77d4..55a38fec5d 100644 --- a/apps/api/src/tools/task-board/archive-merged.integration.test.ts +++ b/apps/api/src/tools/task-board/archive-merged.integration.test.ts @@ -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( @@ -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"); } }); diff --git a/apps/api/src/tools/task-board/archive-merged.ts b/apps/api/src/tools/task-board/archive-merged.ts index ffc8acc493..1069628b20 100644 --- a/apps/api/src/tools/task-board/archive-merged.ts +++ b/apps/api/src/tools/task-board/archive-merged.ts @@ -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"; @@ -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, @@ -101,7 +102,7 @@ async function archiveIfMerged( prLanding: PrLandingReader, ): Promise { 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( @@ -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;