Audit + fix Jira status transitions (session start → mapped In Progress, all state-change sites) (#529)#530
Merged
dhilgaertner merged 1 commit intoJun 19, 2026
Conversation
dgershman
approved these changes
Jun 19, 2026
dgershman
left a comment
Collaborator
There was a problem hiding this comment.
Code & Security Review
Critical Issues
None.
Security Review
Strengths:
JiraTransitionClient.transitionsURLforces HTTPS (https://\(bareHost)/…), so even if a user pasteshttp://acme.atlassian.netinto Settings the Basic credential is never sent in cleartext (Packages/CrowCore/Sources/CrowCore/JiraTransitionClient.swift:49-59; covered bytransitionsURLForcesHTTPSOnCleartextOrigin).Authorizationheader is reused verbatim fromAtlassianMCPResolver(HTTP Basic from the existingatlassianMCPemail + 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.datafrom a fixed shape ({"transition":{"id":transitionID}}),transitionIDis 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.
JiraTransitionClientreturns.noMatchingTransition(available:)when the target status isn't a reachable transition —JiraTaskBackend.setTaskStatuslogs and returns success, so re-runningresync-jiraon 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 bytestSetTaskStatusRESTGracefulNoOpWhenUnavailableandgracefulNoOpWhenTargetStatusNotReachable. - Match precedence is sensible.
matchTransitionIDprefersto.namefirst (whatjiraStatusMapresolves to) and falls back to the transition's ownnameonly 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 aacliretry is undesirable. Acceptable trade-off. resync-jirais 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.shintegration is clean.jira_opscorrectly runs outsidegithub_ops(a Jira-tasked workspace may be GitHub- or GitLab-coded), gates onTASK_PROVIDER == "jira", respects--skip-project-status, and treats failure as non-fatal (|| log "Warning: …"). The implicit dependency oncrow set-tickethaving already populatedsession.ticketURL(so the app can read it back) is satisfied bycreate_sessionrunning beforejira_ops.- CLI parsing.
TransitionTicket.validateaccepts the three documented camelCase tokens case-insensitively; AppDelegate'sticketStatus(fromArg:)is slightly more forgiving (also acceptsin-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.mdclearly explains the app-side vs agent-side split and documentsresync-jira/transition-ticket.CLAUDE.mdadds 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.
…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
9a4881f to
c6b9799
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 Backlog —setup.shonly 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:
JiraTransitionClient(CrowCore):GETthe issue's available transitions → match the mapped status name →POSTit. An unmapped or currently-unavailable target is a logged no-op, not an error.atlassianMCPemail + API token as HTTP Basic (the same plumbingJiraStatusFetcher/Settings UI to map Crow ticket states ↔ Jira workflow statuses #523 already use) — no new credential surface.aclifrom the transition path itself) and of the agent-side MCP (which the app can't reach).JiraTaskBackend.setTaskStatusprefers REST when credentialed, falls back toacliotherwise.Audit results (per site)
jira_ops()delegates tocrow transition-ticket(any code provider; sharedsetup.shcovers batch)markInReview)jiraConfig(forTicket:), mapped + gracefulmarkIssueDone)syncInReviewSessions)crow:mergegateAll transitions resolve the target via the per-workspace
jiraStatusMap(#523; defaultReady→To Do, e.g.In Progress→In Development).Re-sync stuck tickets
Both go through the graceful-degrade path, so already-correct tickets are no-ops.
Tests / build
JiraTransitionClient(URL build, name match, gating, graceful no-op, HTTP error),JiraTaskBackendREST-vs-aclibranch,transition-ticketCLI parse.make appbuilds clean.Coordination
jiraStatusMap) — consumed, merged.jiraMCP) — still open; this lands independently and converges with it (removes the last app-sideaclitransition dependency).🤖 Generated with Claude Code