Skip to content
Merged
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
35 changes: 35 additions & 0 deletions apps/api/src/jira/sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
rewritesSprint,
rewritesStatus,
runTruncated,
triggersColumnAutomation,
vanishedLinks,
} from "./sync";

Expand Down Expand Up @@ -324,3 +325,37 @@ describe("rewritesStatus", () => {
}
});
});

describe("triggersColumnAutomation", () => {
it("fires when the issue's board column actually changed", () => {
expect(
triggersColumnAutomation({
previousStatus: "BACKLOG",
newStatus: "In Progress",
isRescan: false,
}),
).toBe(true);
});

/** Two Jira statuses ("In Review (Dev)", "In Review (QA)") can map to the
* same board column — the card never left it, so its rule must not refire. */
it("does not fire when two Jira statuses map onto the same board column", () => {
expect(
triggersColumnAutomation({
previousStatus: "In Review",
newStatus: "In Review",
isRescan: false,
}),
).toBe(false);
});

it("never fires on a rescan, however the column reads", () => {
expect(
triggersColumnAutomation({
previousStatus: "BACKLOG",
newStatus: "In Progress",
isRescan: true,
}),
).toBe(false);
});
});
36 changes: 35 additions & 1 deletion apps/api/src/jira/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,34 @@ export function rewritesStatus({
return currentStatus !== null && !boardColumns.has(currentStatus);
}

/**
* Whether an updated issue should run its landing column's automation rule.
*
* Gated on the BOARD column actually changing, not on Jira's status literal
* changing: several Jira statuses can map to the same board column (a
* many-to-one `statusMapping`, or several Kanban-board statuses mirrored onto
* one column), so an issue can flip between two of them without the card ever
* leaving its column. `runColumnAutomation` is a no-op on an already-owned
* card, but a card a prior run un-delegated (a quota rejection, a burned
* retry) would otherwise be re-claimed and re-dispatched by a same-column
* status ping-pong that never moved it anywhere. The board's own drag path
* (`TASK_BOARD_ITEM_UPDATE`) already gates the same way, on `previous.status
* !== item.status` — this is that same rule for the Jira leg.
*/
export function triggersColumnAutomation({
previousStatus,
newStatus,
isRescan,
}: {
/** This card's board column before this sync wrote to it. */
previousStatus: string | null;
/** This card's board column after this sync wrote to it. */
newStatus: string;
isRescan: boolean;
}): boolean {
return !isRescan && previousStatus !== newStatus;
}

export function buildJql(
integration: OrgJiraIntegration,
scopeJql: string,
Expand Down Expand Up @@ -719,7 +747,13 @@ async function runSync(
data: { from: before.status, to: status },
});
}
if (statusChangedOnJira && !isRescan) {
if (
triggersColumnAutomation({
previousStatus: before?.status ?? null,
newStatus: item.status,
isRescan,
})
) {
item = await maybeAutoDelegate(ctx, integration, item);
}
await pullComments(
Expand Down
Loading