Skip to content

feat(MAPCO-11431): claim a ticket and release it, with nothing in between - #6

Open
razbroc wants to merge 2 commits into
feat/ticket-polling-workerfrom
feat/claim-and-release
Open

feat(MAPCO-11431): claim a ticket and release it, with nothing in between#6
razbroc wants to merge 2 commits into
feat/ticket-polling-workerfrom
feat/claim-and-release

Conversation

@razbroc

@razbroc razbroc commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Stacked on #5 — review that first, and this PR retargets to master once it merges.

Walks the whole Jira state machine with an empty middle: take the oldest ready ticket, assign the bot, move it to In Progress, then comment, return it to Open and unassign. No branch, no changes, no PR yet.

Claiming is optimistic

Jira is the only state store, so there is no lock to take. The worker writes the assignee and then re-reads the issue; a name that is not its own means a human got there first, so it backs off, writes nothing further, and deliberately does not clear the field — that would take the ticket off them.

The In Progress transition is looked up before any write. A workflow with no route in means the ticket can never be worked, and that is a clean refusal rather than a ticket held by a bot that cannot start it.

Release order is deliberate

Comment → transition to Open → unassign last, which is the reverse of how it reads naturally.

Unassigning is what makes a ticket visible to the poll query again (it filters assignee is EMPTY), so it goes last. A failure part-way through then leaves the ticket held by the bot and In Progress — which the query skips and the boot-time orphan sweep (MAPCO-11432) recovers. Unassigning first would risk leaving a ticket unassigned and In Progress, which polls straight back in and gets commented on every single cycle.

⚠️ The transition vocabulary is unverified

Transitions are matched on their target status, not their own name, because transition names are verbs on a real workflow (Start Progress, not In Progress). Matching by name alone would have refused every ticket and made this a silent no-op in production.

Both jira_get_transitions and expand=transitions are rejected by the write-pilot MCP server, so the real MAPCO vocabulary could not be read the way the poll query was verified in MAPCO-11427. The lookup handles both shapes (target status, name as fallback), and a no-transition refusal logs the offered names — so the first real run reports the vocabulary instead of refusing in silence. Confirm it from that log line before trusting a deployment.

Bot identity is configured, not discovered

Two new required env vars, JIRA_BOT_ACCOUNT and JIRA_BOT_DISPLAY_NAME. They look redundant and are not: the MCP server runs under a shared service account so the worker cannot ask Jira who it is, and Jira takes an identifier on write but returns a surname-first display name on read. Neither is derivable from the other, and setting them inconsistently makes every claim read as lost.

Acceptance criteria

  • A ready ticket is claimed by assigning the bot and transitioning to In Progress
  • An already-assigned ticket is never picked up
  • After writing the assignee, the worker re-reads and backs off if the assignee isn't itself
  • Release comments what was tried, unassigns, and transitions back to Open (order changed — see above)
  • Max tickets per run and max concurrent are honoured, both defaulting to 1
  • Nothing is persisted outside Jira — no database, no files that outlive the run
  • Seam tests: claim, leave-already-assigned-alone, lose-the-re-read, release

61 tests pass; claim.ts, cycle.ts and workerConfig.ts are at 100% line coverage.

Refs: MAPCO-11431

Walks the whole Jira state machine with an empty middle: take the oldest ready
ticket, assign the bot, move it to In Progress, then comment, return it to Open
and unassign.

Claiming is optimistic because Jira is the only state store. After writing the
assignee the worker re-reads the issue, and a name that is not its own means a
human got there first: back off, write nothing further, and do not clear the
field — that would take the ticket off them.

Release runs comment, transition, unassign, in that order. Unassigning is what
makes a ticket visible to the poll query again, so it goes last: a failure
part-way through leaves the ticket held and In Progress, which the query skips
and the boot-time sweep recovers. Unassigning first risks leaving a ticket that
polls straight back in and gets commented on every cycle.

Transitions are matched on their target status rather than their own name,
because transition names are verbs on a real workflow (Start Progress). Both
jira_get_transitions and expand=transitions are rejected by the write-pilot MCP
server, so the actual vocabulary could not be verified the way the poll query
was; a no-transition refusal logs the names it was offered, so the first real
run reports them instead of refusing every ticket in silence.

The bot's identity is configured rather than discovered. The MCP server runs
under a shared service account with no per-user attribution, and Jira takes an
identifier on write but returns a surname-first display name on read, so both
halves are required and neither can be derived from the other.

Refs: MAPCO-11431
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown

🎫 Related Jira Issue: MAPCO-11431

The secrets-context fallback in 908f19a still resolved to an empty input, so
the action kept failing with 'Input required and not supplied: repo-token' —
neither GH_PAT nor secrets.GITHUB_TOKEN came through. github.token is always
populated, and the job already holds the pull-requests: write permission.

This only takes effect for pull requests whose base branch carries it, because
pull_request_target resolves the workflow from the base.

Refs: MAPCO-11431
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