Skip to content

Feature request: Jira Cloud integration (parity with Linear) #288

@mayakost

Description

@mayakost

Jira Integration for ABCA — Scaffold Plan

Context

ABCA (Autonomous Background Coding Agents on AWS) currently ingests coding tasks from CLI, GitHub webhooks, Slack, and Linear, then opens PRs autonomously. Linear is the only "issue tracker" channel today. Many teams use Jira instead, so we want a parity-level Jira Cloud integration: an issue gets a bgagent label → ABCA picks it up → an agent run produces a PR → status flows back into the Jira issue.

The existing Linear integration (cdk/src/constructs/linear-integration.ts + sibling handlers + agent/src/channel_mcp.py) is the canonical template. Confirmed scope:

  • Jira Cloud only (REST v3, cloud webhooks)
  • Per-workspace OAuth 3LO stored via AgentCore credential provider, mirroring bgagent-linear-oauth-*
  • Trigger: label added (default bgagent), parity with Linear
  • Outbound: Atlassian's Remote MCP Server, registered into .mcp.json when channel_source == "jira" — same shape as the Linear MCP path

Reference files (read these first)

Linear is the closest analog and should be copied/adapted, not reinvented.

  • cdk/src/constructs/linear-integration.ts — webhook + processor + link Lambdas, dedup table, secrets, API routes
  • cdk/src/constructs/linear-project-mapping-table.ts, linear-user-mapping-table.ts, linear-workspace-registry-table.ts — DDB schemas
  • cdk/src/handlers/linear-webhook.ts — HMAC verify, dedup, async-invoke processor
  • cdk/src/handlers/linear-webhook-processor.ts — label-trigger logic, calls createTaskCore, writes channel metadata
  • cdk/src/handlers/linear-link.ts — Cognito-authenticated user→workspace linking
  • cdk/src/stacks/agent.ts — how LinearIntegration is wired (api, userPool, taskTable, taskEventsTable, repoTable, orchestratorFunctionArn, guardrails) and how the orchestrator is granted read on its workspace registry
  • agent/src/channel_mcp.py — gates Linear MCP server entry into .mcp.json based on channel_source
  • agent/src/linear_reactions.py — outbound progress hooks (consult for parity, may not need a Jira twin if MCP covers it)
  • cli/src/**bgagent linear setup, bgagent linear link <code> commands (find the exact files during impl)
  • docs/guides/LINEAR_SETUP_GUIDE.md — template for JIRA_SETUP_GUIDE.md
  • contracts/constants.json / constants.md — channel source enum lives somewhere in contracts/ or cdk/src/types/; locate during impl and add "jira"

Architecture summary

Inbound (Jira → ABCA):

Jira Cloud webhook
  → POST /jira/webhook (API GW, no Cognito, HMAC-verified)
  → JiraWebhookFn (verify signature, dedup, async invoke)
  → JiraWebhookProcessorFn (resolve workspace OAuth, look up project→repo, build task, call createTaskCore)
  → existing orchestrator pipeline (unchanged)

Outbound (Agent → Jira) — MCP only:

runner.py picks task with channel_source="jira"
  → channel_mcp.py writes `jira-server` entry into .mcp.json (Atlassian Remote MCP, OAuth token from secret)
  → Claude Agent SDK exposes Jira tools to the agent
  → agent posts comments / transitions / links PR via MCP tools

No DynamoDB Streams consumer, no outbound-notify Lambda — matches Linear's "inbound-only adapter" stance.

Implementation plan

1. Contracts & constants

  • Add "jira" to whatever channel-source enum/union exists (search cdk/src/types/, contracts/, agent/src/models.py).
  • Add Jira-specific constants alongside Linear ones (env var names, default trigger label bgagent, secret prefix bgagent-jira-oauth-).

2. CDK constructs (mirror Linear, file-for-file)

New files under cdk/src/constructs/:

  • jira-integration.ts — top-level construct; props mirror LinearIntegrationProps (api, userPool, taskTable, taskEventsTable, repoTable, orchestratorFunctionArn, guardrailId, guardrailVersion)
  • jira-project-mapping-table.ts — Jira cloudId + projectKey → repo
  • jira-user-mapping-table.ts — Jira accountId → platform user
  • jira-workspace-registry-table.tscloudId → AgentCore credential-provider name; this is the table the orchestrator reads to resolve OAuth at agent-launch time

The construct must create:

  • JiraWebhookFn Lambda + POST /jira/webhook route (no auth, signature-verified)
  • JiraWebhookProcessorFn Lambda (async-invoked, 512 MB to match Linear's attachment-screening footprint)
  • JiraLinkFn Lambda + POST /jira/link route (Cognito-authenticated)
  • JiraWebhookSecret placeholder in Secrets Manager (populated by bgagent jira setup)
  • JiraWebhookDedupTable — composite key {issueKey}#{webhookEventTimestamp}, 8-hr TTL (Jira retries far less than Linear; 8 hr is safe parity)
  • IAM: webhook Fn reads webhook secret + dedup table + bgagent-jira-oauth-* prefix; processor reads mapping/registry tables, writes task tables, invokes orchestrator alias, applies Bedrock guardrail; link Fn read/writes user mapping

3. Lambda handlers (new, under cdk/src/handlers/)

  • jira-webhook.ts — verify Atlassian webhook signature (X-Hub-Signature HMAC-SHA256 over body using shared secret), parse JSON, accept only jira:issue_updated and jira:issue_created events, dedup, async-invoke processor. Silent 200 for unsupported events.
  • jira-webhook-processor.ts — port Linear's logic:
    • Trigger: issue_created with bgagent label already on, OR issue_updated whose changelog.items shows label bgagent newly added (Jira sends label diffs in changelog, not in the issue body — handle this carefully; this is the one place we can't blindly copy Linear)
    • Resolve cloudId → workspace registry → AgentCore credential-provider name (don't fetch the token here; just hand off)
    • Build task description from issue.fields.summary + ADF→markdown of issue.fields.description
    • Call createTaskCore with channelSource: 'jira' and metadata { jira_cloud_id, jira_project_key, jira_issue_key, jira_issue_id }
    • On failure, post a comment back via REST (one-shot, mirrors reportIssueFailure)
  • jira-link.ts — Cognito JWT in, linking-code lookup, dry-run preview of Atlassian identity, then persist user mapping. Direct port of linear-link.ts.

4. Stack wiring (cdk/src/stacks/agent.ts)

Append after Linear:

const jiraIntegration = new JiraIntegration(this, 'JiraIntegration', {
  api: taskApi.api,
  userPool: taskApi.userPool,
  taskTable: taskTable.table,
  taskEventsTable: taskEventsTable.table,
  repoTable: repoTable.table,
  orchestratorFunctionArn: orchestrator.alias.functionArn,
  guardrailId: inputGuardrail.guardrailId,
  guardrailVersion: inputGuardrail.guardrailVersion,
});

jiraIntegration.workspaceRegistryTable.grantReadData(orchestrator.fn);
orchestrator.fn.addEnvironment(
  'JIRA_WORKSPACE_REGISTRY_TABLE_NAME',
  jiraIntegration.workspaceRegistryTable.tableName,
);
orchestrator.fn.addToRolePolicy(new iam.PolicyStatement({
  actions: ['secretsmanager:GetSecretValue', 'secretsmanager:PutSecretValue'],
  resources: [`arn:aws:secretsmanager:${region}:${account}:secret:bgagent-jira-oauth-*`],
}));

5. Agent-side outbound (MCP only)

Modify agent/src/channel_mcp.py:

  • Replace the if channel_source != "linear": return False gate with a small registry/dispatch:
    CHANNEL_MCP_BUILDERS = {
        "linear": _linear_server_entry,
        "jira": _jira_server_entry,
    }
  • Add _jira_server_entry() — writes Atlassian Remote MCP config (URL https://mcp.atlassian.com/v1/sse or current per Atlassian docs; OAuth token resolved from bgagent-jira-oauth-<slug> secret, env var name JIRA_API_TOKEN or per-Atlassian-MCP convention).
  • This is a small refactor, not a rewrite — keep the diff minimal.

No jira_reactions.py for now. If MCP coverage is insufficient (e.g. structured failure comments at orchestrator boundary), we can add one later, exactly mirroring linear_reactions.py.

6. CLI (cli/src/**)

Add subcommands by copying Linear's:

  • bgagent jira setup — populate JiraWebhookSecret, register the AgentCore credential provider, print webhook URL for the customer to paste into Atlassian's webhook config
  • bgagent jira link <code> — invoke POST /jira/link
  • bgagent jira map <project-key> <repo> — write to project mapping table

7. Docs

  • New docs/guides/JIRA_SETUP_GUIDE.md — port LINEAR_SETUP_GUIDE.md step-by-step, swapping Linear for Jira Cloud, including: creating the OAuth 3LO app in Atlassian developer console, scopes (read:jira-work, write:jira-work, read:jira-user), webhook URL registration, label setup, project mapping.
  • One-line addition to README.md under integrations.
  • Add decisions/ ADR if the repo uses them — capture "Jira Cloud only, OAuth 3LO, label trigger, MCP outbound."

Things to watch (non-obvious)

  1. Webhook signature: Atlassian uses X-Hub-Signature: sha256=<hex>; ensure constant-time compare and that the body is the raw unparsed bytes, not the parsed-and-restringified JSON.
  2. Label-add detection on update events: Jira's webhookEvent: jira:issue_updated payload has changelog.items[] with field: "labels", fromString, toString. Diff these — not current issue.fields.labels. Linear's processor does the equivalent diff using its data.updatedFrom; same idea, different shape.
  3. ADF to markdown: Jira description is Atlassian Document Format, not markdown. Use a small ADF→md helper or @atlaskit utility; don't roll a full converter — extract text + headings + lists is enough.
  4. cloudId is the tenant key, not domain or workspace name. Webhook payloads include it; OAuth flows return it. Index everything on cloudId.
  5. Webhook dedup key: Jira does retry on 5xx but less aggressively than Linear. Use {issueKey}#{timestamp} rather than {issueKey}#{eventType} so two distinct label-adds in quick succession aren't collapsed.
  6. Atlassian Remote MCP: confirm current public URL + auth contract before wiring. If it's still gated/preview, fall back to direct REST in jira_reactions.py (Plan B; do not start with this).

Verification plan

End-to-end smoke (after deploy):

  1. cd cdk && npm run build && cdk synth — no type errors, both LinearIntegration and JiraIntegration synthesize.
  2. cdk deploy to a dev account.
  3. Run bgagent jira setup, complete OAuth dance, paste webhook URL into a test Jira Cloud instance.
  4. bgagent jira link <code>, then bgagent jira map TEST <github-org/test-repo>.
  5. Create a Jira issue in TEST, add label bgagent → confirm task appears in bgagent list within ~30s.
  6. Watch the agent run; confirm it comments back on the Jira issue (proves outbound MCP works) and opens a PR.
  7. Negative paths: signed webhook with wrong secret → 401; duplicate webhook within 8h → { ok: true, deduped: true }; non-issue event → silent 200.

Unit-test surface (mirror Linear's tests, found under cdk/test/ and agent/tests/):

  • jira-webhook.test.ts — signature pass/fail, dedup, event filtering
  • jira-webhook-processor.test.ts — label-add-on-create vs label-add-on-update vs label-already-present-no-change
  • channel_mcp test — channel_source="jira" writes a jira-server entry; "slack" does not

Out of scope (explicit)

  • Jira Server / Data Center
  • Forge/Connect app distribution
  • Status-transition or comment-command triggers (label only for v1)
  • Outbound REST module (jira_reactions.py) — only add if MCP gaps emerge
  • Bidirectional sync of issue state (status, assignee) beyond what the agent posts via MCP

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions