Skip to content

Audit + fix Jira status transitions (session start → mapped In Progress, all state-change sites) (#529)#530

Merged
dhilgaertner merged 1 commit into
mainfrom
feature/crow-529-jira-status-transitions-audit
Jun 19, 2026
Merged

Audit + fix Jira status transitions (session start → mapped In Progress, all state-change sites) (#529)#530
dhilgaertner merged 1 commit into
mainfrom
feature/crow-529-jira-status-transitions-audit

Conversation

@dhilgaertner

Copy link
Copy Markdown
Contributor

Closes #529.

Audited every ticket-state-change site and brought the Jira path to parity with GitHub. The headline bug: starting a Jira-backed ticket (/crow-workspace, /crow-batch-workspace) never moved it off Backlogsetup.sh only had a GitHub Projects-v2 mutation and explicitly skipped Jira.

Mechanism — app-side Swift REST (operator-approved)

Single owner, headless-safe (Manager + batch + cron). The Crow app drives Jira transitions over the Jira Cloud REST API:

Audit results (per site)

Site Before After
Session start → In Progress Jira skipped → stuck in Backlog new jira_ops() delegates to crow transition-ticket (any code provider; shared setup.sh covers batch)
PR opened → In Review (markInReview) acli, no graceful-degrade REST via jiraConfig(forTicket:), mapped + graceful
Mark done #526 (markIssueDone) acli REST via same path
Mirror-back (syncInReviewSessions) reads Jira status already unchanged (audited)
crow:merge gate GitHub-only, no ticket-status assumption unchanged (audited, documented)

All transitions resolve the target via the per-workspace jiraStatusMap (#523; default ReadyTo Do, e.g. In ProgressIn Development).

Re-sync stuck tickets

crow resync-jira    # walk every Jira-backed session, transition to the status implied by Crow session state
crow transition-ticket --session <uuid> --to inProgress|inReview|done   # single ticket

Both go through the graceful-degrade path, so already-correct tickets are no-ops.

Tests / build

  • New: JiraTransitionClient (URL build, name match, gating, graceful no-op, HTTP error), JiraTaskBackend REST-vs-acli branch, transition-ticket CLI parse.
  • Full suites green: CrowCore 306, CrowProvider 52, CrowCLI 43. make app builds clean.
  • GitHub/GitLab paths untouched.

Coordination

🤖 Generated with Claude Code

@dhilgaertner dhilgaertner requested a review from dgershman as a code owner June 19, 2026 19:40
@dhilgaertner dhilgaertner added the crow:merge Crow auto-merge on green label Jun 19, 2026

@dgershman dgershman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code & Security Review

Critical Issues

None.

Security Review

Strengths:

  • JiraTransitionClient.transitionsURL forces HTTPS (https://\(bareHost)/…), so even if a user pastes http://acme.atlassian.net into Settings the Basic credential is never sent in cleartext (Packages/CrowCore/Sources/CrowCore/JiraTransitionClient.swift:49-59; covered by transitionsURLForcesHTTPSOnCleartextOrigin).
  • Authorization header is reused verbatim from AtlassianMCPResolver (HTTP Basic from the existing atlassianMCP email + API token). No new credential surface — same plumbing #523 already uses. Token isn't logged anywhere in the new path.
  • Issue key is percent-encoded into the URL path (addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)). Site host is whitespace-stripped and rejected when blank.
  • POST body is built via JSONSerialization.data from a fixed shape ({"transition":{"id":transitionID}}), transitionID is the value Jira returned on the GET — no string interpolation, no injection.
  • Capability-gated (.projectBoardStatus): GitLab is a hard no-op, so a Jira-targeted transition can't accidentally hit a code provider.

Concerns:

  • None blocking.

Code Quality

  • Design is right. The single-owner REST path consolidates three formerly divergent transition sites (session start, mark-in-review, mark-done) under one client; the agent-side MCP (which the app can't reach from Manager/cron/UI) is correctly not the path of record for app-driven transitions.
  • Graceful degrade is precise. JiraTransitionClient returns .noMatchingTransition(available:) when the target status isn't a reachable transition — JiraTaskBackend.setTaskStatus logs and returns success, so re-running resync-jira on an already-correct ticket (or one whose workflow has no path to the mapped status) is a clean no-op rather than an error. Verified by testSetTaskStatusRESTGracefulNoOpWhenUnavailable and gracefulNoOpWhenTargetStatusNotReachable.
  • Match precedence is sensible. matchTransitionID prefers to.name first (what jiraStatusMap resolves to) and falls back to the transition's own name only when needed — case-insensitive throughout. Documented and tested.
  • REST-failure semantics are intentional and documented. On HTTP/decode/transport failure the REST path throws rather than silently falling back to acli; the in-code comment + PR description both spell out that this is the path of record (acli is being removed in #528) and that masking a stale-token failure with a acli retry is undesirable. Acceptable trade-off.
  • resync-jira is bounded and idempotent. Sequential awaits over every Jira-backed session; each move re-uses the graceful no-op path, so a workspace with mostly-correct tickets generates a burst of cheap GETs followed by no POSTs. Reasonable for a one-shot remediation command.
  • setup.sh integration is clean. jira_ops correctly runs outside github_ops (a Jira-tasked workspace may be GitHub- or GitLab-coded), gates on TASK_PROVIDER == "jira", respects --skip-project-status, and treats failure as non-fatal (|| log "Warning: …"). The implicit dependency on crow set-ticket having already populated session.ticketURL (so the app can read it back) is satisfied by create_session running before jira_ops.
  • CLI parsing. TransitionTicket.validate accepts the three documented camelCase tokens case-insensitively; AppDelegate's ticketStatus(fromArg:) is slightly more forgiving (also accepts in-progress/in_progress/completed/closed). Inconsistency is harmless — the CLI is the user-facing surface and is the stricter of the two.
  • Tests are comprehensive. URL building (incl. HTTPS coercion), parse, name-match precedence, full flow (GET→POST), graceful no-op, HTTP error surfacing, bad-site short-circuit, REST-vs-acli branch, mapped-name resolution, CLI parse — all covered. Local test runs: CrowCore Jira tests 10/10, CrowProvider Jira tests 25/25, CrowCLI parsing 14/14.
  • Docs. docs/automation.md clearly explains the app-side vs agent-side split and documents resync-jira / transition-ticket. CLAUDE.md adds both verbs to the CLI reference. Good.

Summary Table

Color Meaning Verdict effect
Red Must fix Request changes
Yellow Should fix Request changes
Green Consider Approve allowed

Recommendation: Approve — driven by [0 Red, 0 Yellow, 0 Green] findings. Well-scoped audit + fix, security-sound, well-tested, documentation in sync. Ship it.


🐦‍⬛ Reviewed by Crow via Claude Code

…ss (#529)

Audited every ticket-state-change site and brought the Jira path to parity
with GitHub. The headline bug: starting a Jira-backed ticket never moved it
off Backlog — setup.sh only had a GitHub Projects-v2 mutation and explicitly
skipped Jira.

Mechanism (app-side Swift REST, single owner, headless-safe):
- New JiraTransitionClient (CrowCore): GET the issue's available transitions,
  match the mapped status name, POST it. Unmapped/unreachable target → logged
  no-op, not an error. Reuses the atlassianMCP email+token as HTTP Basic
  (same plumbing JiraStatusFetcher/#523 already use); independent of acli
  (removed by #528) and of the agent-side MCP.
- JiraConfig.authorization; JiraTaskBackend.setTaskStatus prefers REST when
  credentialed, falls back to acli otherwise.

State-change sites:
- Session start → mapped In Progress: new jira_ops() in setup.sh delegates to
  `crow transition-ticket` (runs for any code provider; covers crow-workspace
  and crow-batch-workspace, which share setup.sh). [was missing — the bug]
- PR opened → In Review (markInReview) and mark done → Done (markIssueDone,
  #526) now resolve site + map + auth via IssueTracker.jiraConfig(forTicket:).
- syncInReviewSessions / crow:merge gate: audited, no GitHub-Projects-v2
  assumption for Jira; unchanged.

Re-sync for tickets already stuck in Backlog: `crow resync-jira` walks every
Jira-backed session and transitions its ticket to the status implied by the
Crow session state, via the same graceful-degrade path (no-op when already
correct). Single-ticket form: `crow transition-ticket --session <id> --to ...`.

New RPC verbs transition-ticket / resync-jira; CLI commands; docs.

Tests: JiraTransitionClient (URL/match/gating/graceful no-op), JiraTaskBackend
REST-vs-acli branch, CLI parse. Full suites green (CrowCore 306, CrowProvider
52, CrowCLI 43); app builds clean.

Closes #529

🐦‍⬛ Generated with Claude Code, orchestrated by Crow

Co-Authored-By: Claude <noreply@anthropic.com>
Crow-Session: 2908EA7D-C272-4742-87A3-1B8CFAF9C007
@dhilgaertner dhilgaertner force-pushed the feature/crow-529-jira-status-transitions-audit branch from 9a4881f to c6b9799 Compare June 19, 2026 19:55
@dhilgaertner dhilgaertner merged commit 465c4c7 into main Jun 19, 2026
2 checks passed
@dhilgaertner dhilgaertner deleted the feature/crow-529-jira-status-transitions-audit branch June 19, 2026 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

crow:merge Crow auto-merge on green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Audit Jira status transitions: session start doesn't move ticket to mapped In Progress (and all state-change sites)

2 participants