Skip to content

feat(jira): mirror the board's own columns instead of asking for a mapping - #6720

Merged
viktormarinho merged 1 commit into
mainfrom
feat/jira-mirrors-columns
Aug 28, 2026
Merged

feat(jira): mirror the board's own columns instead of asking for a mapping#6720
viktormarinho merged 1 commit into
mainfrom
feat/jira-mirrors-columns

Conversation

@viktormarinho

@viktormarinho viktormarinho commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

The last piece. An org whose board is its own now gets its columns from Jira, under the names its team gave them, with each card in the column Jira puts it in.

Both halves come from one call:

const columns = await client.getBoardColumns(boardId);   // { name, statuses[] }[]
await ctx.storage.boardColumns.replaceAll(org, columns.map(c => ({ key: c.name, title: c.name })));
// and the reverse index the pull needs: Jira status -> the column that groups it

Jira already knows the mapping. That the mapping screen ever existed is the mistake this removes: it asked a team to restate, by hand and per lane, something their tracker already knew — and it is what made Studio's vocabulary leak into everything downstream, including the agent.

Studio's own board is untouched. It still reads the hand-written mapping, because its lanes are ours and Jira has no idea what they mean.

Honouring the foreign key, not working around it

replaceAll now keeps a column the tracker dropped when it still holds cards, appended after the mirrored set.

That is the obligation ON DELETE RESTRICT created, and I said in #6710 that writing this sync without it would break production the first time someone tidied their Jira board. The alternatives are worse: RESTRICT refuses the delete outright, and relocating someone's cards to a column Studio picked is a worse answer than showing a column their tracker no longer has. It leaves on its own on the first sync after the last card is out.

One implementation detail worth a reviewer's eye: I first made the delete a not in [...kept, sentinel] to keep the list non-empty, using a NUL byte as the sentinel. Postgres rejects NUL in text, which the tests caught. It builds an explicit drop list now and skips the statement when it is empty — a sentinel there was a bad idea regardless of the encoding.

How did you verify your code works?

The case the whole design turns on, against real Postgres: a dropped column that still holds cards survives, and disappears once emptied. Both halves in one test, because either alone passes a broken implementation — keeping forever, or dropping immediately.

apps/api/src: 4051 pass / 49 fail against a baseline of 4050 / 49, measured by checking out origin/main's tree, re-running, and restoring. Same failures, one more test. (My first attempt at that measurement was worthless — I had already committed, so the stash was empty and I compared the change to itself. Worth saying because the number looked fine either way.)

Every *.integration.test.ts under tools/task-board, storage and jira also run individually in find | sort order, the way CI runs them: all green. bun run check 0 errors, lint at baseline, knip clean.

Not verified, flagging honestly:

  • Nothing exercises this against a real Jira. The mirror is one getBoardColumns call and a replaceAll; the storage half is covered, the client call is not, and the suite has no fake Jira. The first real sync on an org board is genuinely the first run.
  • A renamed Jira column reads as delete + create. The column's identity here is its name, and the Agile board API gives no stable id for a column. So renaming "Code Review" leaves the old column (holding its cards, by the rule above) and adds an empty new one. Not destructive, but confusing, and someone will hit it. Matching by status-set to detect a rename is the fix and it is not here.
  • board_column_org is still never written, so the foreign key still sleeps even for these cards. That is the next thing, and until it lands the FK is protection we have paid for and are not using.
  • in_review roles are still inert — settable since feat(task-board): let a column say what it means #6718, read by nothing.

Screenshots/Demonstration

Not captured. With org_board_columns off — every org today — the sync behaves exactly as before.

How to Test

  1. On an org with Jira and the flag off: unchanged, the hand-written mapping still drives everything.
  2. Turn org_board_columns on, run a sync, and confirm the board shows that Jira board's columns, in its order, with its names.
  3. Confirm each card sits in the column Jira has its status in.
  4. Delete a column in Jira that still has cards, sync, and confirm the column stays at the end with its cards — and that the sync does not error.
  5. Move those cards out, sync again, and confirm the column disappears.

Migration Notes

None. No schema change. status_mapping is still read for Studio's own board and is untouched; it becomes dead only once every Jira org is on its own board.

Review Checklist

  • PR title is clear and descriptive
  • Changes are tested and working
  • Documentation is updated (if needed)
  • No breaking changes

Summary by cubic

Org-owned boards now get their columns read from Jira instead of asking teams to hand-maintain a status mapping, placing each card in the column Jira's board config assigns its status. With the org_board_columns flag off, the hand-written mapping still drives everything, and Studio's own board is untouched.

New Features

  • The sync reads the board's columns and their status groups from Jira in one call and replaces the stored board columns, preserving existing roles.
  • A tracker-dropped column that still holds cards is kept at the end of the list and removed on the first sync after the last card leaves, honoring the ON DELETE RESTRICT foreign key.
  • Caveat: a renamed Jira column comes through as delete + create, since the column's name is its identity and the Agile API exposes no stable column id; the old column lingers with its cards and an empty new one appears.

Written for commit f9002bb. Summary will update on new commits.

Review in cubic

…pping

The last piece: an org whose board is its own gets its columns from Jira, under
the names its team gave them, with each card in the column Jira puts it in.

Both come from one call. `getBoardColumns` already returns each column with the
statuses it groups — Jira knows the mapping, so it is read rather than
configured. That the mapping screen ever existed is the mistake this removes:
it asked a team to restate, by hand and per lane, something their tracker
already knew, and it is what made our vocabulary leak into everything
downstream.

`replaceAll` now keeps a column the tracker dropped when it still holds cards,
appended after the mirrored set. That is the obligation the foreign key
created, and honouring it is better than the alternatives: RESTRICT refuses the
delete, and moving someone's cards to a column Studio picked is a worse answer
than showing a column their tracker no longer has. It leaves on its own once
the last card is out.

Studio's own board is untouched: it still reads the hand-written mapping, since
its lanes are ours and Jira has no idea what they mean.
@viktormarinho
viktormarinho merged commit ce453a6 into main Aug 28, 2026
34 checks passed
@viktormarinho
viktormarinho deleted the feat/jira-mirrors-columns branch August 28, 2026 20:05
decocms Bot pushed a commit that referenced this pull request Aug 28, 2026
PR: #6720 feat(jira): mirror the board's own columns instead of asking for a mapping
Bump type: minor

- decocms (apps/api/package.json): 4.300.0 -> 4.301.0
- @decocms/native (apps/native/package.json): 4.300.0 -> 4.301.0

Deploy-Scope: server
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant