feat(jira): Jira Cloud integration -- parity with Linear (#288)#302
feat(jira): Jira Cloud integration -- parity with Linear (#288)#302mayakost wants to merge 23 commits into
Conversation
Phase 1 of Jira Cloud integration (aws-samples#288). Extends the ChannelSource discriminant on both sides of the wire and updates the agent-side comment so the runtime knows 'jira' is a recognized channel value; no behavior changes yet.
Phase 2 of Jira Cloud integration (aws-samples#288). Mirrors the Linear constructs file-for-file. Composite PKs use cloudId as the tenant prefix (`{cloudId}#{projectKey}`, `{cloudId}#{accountId}`) so the same project key or account id stays unambiguous across distinct Atlassian tenants. Tables are unwired until Phase 4 — JiraIntegration instantiates and grants them.
Phase 3 of Jira Cloud integration (aws-samples#288). Mirrors Linear's adapter shape: per-tenant OAuth resolver (auth.atlassian.com), X-Hub-Signature HMAC verify with per-tenant + stack-wide fallback, REST-based feedback poster (ADF-wrapped, no reaction primitive — marker folded into text), and three Lambdas (webhook, processor, link). Non-trivial bit: the processor diffs `changelog.items[]` where `field === 'labels'` and tokenizes the space-separated `fromString` / `toString` to detect a label add — Atlassian's diff format differs from Linear's `updatedFrom.labelIds`. Includes a minimal ADF→markdown walker for issue descriptions. Handlers reference JIRA_* env vars set by the JiraIntegration construct in Phase 4; they don't deploy yet.
Phase 4 of Jira Cloud integration (aws-samples#288). Mirrors LinearIntegration: 3 DDB tables, dedup table (8h TTL), 3 Lambdas (webhook/processor/link), API routes under /jira/*, per-tenant `bgagent-jira-oauth-*` IAM grants, cdk-nag suppressions. Stack wiring grants the agent runtime GetSecretValue on the per-tenant prefix and pipes the workspace registry table + Get/Put grant into the orchestrator (matches Linear's path for pre-container failure feedback). Synth confirms clean CloudFormation + no nag findings.
Phase 5 of Jira Cloud integration (aws-samples#288). Refactors channel_mcp.py from a single-channel gate to a CHANNEL_MCP_BUILDERS dispatch dict so adding future channels stays one-entry. Adds resolve_jira_oauth_token() to config.py mirroring the Linear resolver — same race-handling, same fail-closed semantics; only differences are the endpoint (auth.atlassian.com, JSON body) and the env-var name (JIRA_API_TOKEN). Pipeline now dispatches to the right resolver based on channel_source. JIRA_MCP_URL is flagged in-source as needs-verification — Atlassian's Remote MCP may still be preview-gated; if so, fall back to a REST shim in a future jira_reactions.py module (Plan B). Tests: 6 new Jira test cases in test_channel_mcp.py; full agent suite remains green (825 passed).
Phase 6 of Jira Cloud integration (aws-samples#288). Minimal v1 surface (4 of 10 Linear subcommands), per scoping decision. Mirrors the Linear CLI shape where the contracts are similar: - jira-oauth.ts ports linear-oauth.ts. Atlassian's token endpoint takes JSON (Linear takes form-encoded). offline_access scope is required for a refresh_token. fetchAccessibleResources() resolves cloudId + siteUrl post-consent. - commands/jira.ts: app-template prints dev-console values; setup drives the OAuth dance + writes the per-tenant secret + registry row + webhook signing secret; link does dry-run preview UX; map writes the project → repo row. Deferred to follow-ups: add-workspace, update-webhook-secret, invite-user (with self-link picker), list-projects.
Covers signature verify pass/fail, dedup, event filtering, label-add detection (create vs update changelog), and Cognito-authenticated linking. 56 tests, mirrors the Linear handler test surface.
- docs/guides/JIRA_SETUP_GUIDE.md — OAuth 3LO app, scopes, webhook registration, label trigger, project mapping, troubleshooting - docs/decisions/ADR-014-jira-integration.md — Jira Cloud only, OAuth 3LO, label trigger, MCP outbound; documents the Jira-vs-Linear divergences - README, USER_GUIDE, ROADMAP — add Jira to channel listings - sync-starlight.mjs + astro.config.mjs — register the Jira guide mirror; regenerate Starlight content under docs/src/content/docs/ Completes the docs phase of aws-samples#288.
Bring `mise run build` green on the jira integration branch: - check-types-sync: allowlist JiraLinkResponse as CLI-only, matching SlackLinkResponse/LinearLinkResponse (link responses are inlined server-side; no CDK source-of-truth type) - channel_mcp.py: move Callable into a TYPE_CHECKING block (ruff TC003; safe under `from __future__ import annotations`) - agent.test.ts: bump expected DynamoDB table count 13 -> 17 for the four new Jira tables (project/user/workspace-registry/webhook-dedup) - test_config.py: cover resolve_jira_oauth_token (cache, fallback, refresh, concurrent-refresh, malformed/expiry paths); agent coverage 70.41% -> 72.91% - jira-oauth-resolver.test.ts: new suite (32 tests) mirroring the Linear resolver tests; clears the CDK statement/line/function/branch gates - jira.ts / jira-oauth.ts: ESLint --fix cosmetic edits (quote-props, redundant template literals) Tests: 294 CLI + 837 agent + 1896 CDK, all passing.
Jira webhooks created via the Settings → System → Webhooks UI do not include a top-level `cloudId` in their payload (only app/OAuth-registered dynamic webhooks do). Without it the processor can't resolve the tenant, so it dropped the event and never created a task — the inbound trigger silently failed for the common single-tenant, UI-webhook setup. Add a safe fallback: when `payload.cloudId` is absent, scan the workspace registry and use the sole `active` tenant. Deliberately refuses to guess when zero or multiple active tenants exist (returns undefined → event dropped), so the multi-tenant design is preserved — a multi-tenant operator must use a webhook that carries its own cloudId. `grantReadData` on the registry table already covers the Scan, so no IAM change is needed. Adds tests for: sole-tenant recovery (task created), empty registry (drop), and multiple active tenants (ambiguous → drop).
Jira-origin tasks now comment on the originating issue at start
("🤖 picked up…") and on completion ("✅ finished — PR: <url>" / "❌ …"),
matching the Linear integration's progress UX.
Why a REST shim instead of the Atlassian Remote MCP: the hosted MCP
(mcp.atlassian.com) requires an interactive, browser-based OAuth 2.1 flow
with dynamic client registration — it does NOT accept the stored Jira REST
OAuth token as a Bearer header, so it fails to connect from a headless
agent ("claude mcp list" → Failed to connect; no mcp__jira-server__* tools
load). The Jira REST API accepts the same stored token (it carries
write:jira-work), so comments go via POST /rest/api/3/issue/{key}/comment
on the cross-region api.atlassian.com/ex/jira/{cloudId} base.
- New `jira_reactions.py`: gated by channel_source=='jira' + required
metadata; swallows all network/auth errors (comments are advisory, never
gate the pipeline); auth circuit-breaker mirrors linear_reactions.
- Wired into pipeline.py at task start, normal finish (with PR url), and
the crash path — parallel to the existing Linear reaction hooks.
- prompt_builder: Jira tasks now get NO MCP-comment addendum (the earlier
Linear-only gate already skipped them); instructing the agent to use the
non-loading MCP tools would just waste turns. Comments are out-of-band.
Adds test_jira_reactions.py (gate, ADF body, success/failure/PR variants,
error-swallowing, auth circuit breaker) and channel-addendum tests.
The 'Merge branch main' commit (c84cc66) left an invalid import block: a missing comma after PR_WORKFLOW_IDS (syntax error) plus a stale PR_TASK_TYPES import that main's aws-samples#248 removed from config.py. ruff rejected the file, aborting the agentcore build. Drop the orphaned PR_TASK_TYPES import and fix the comma.
krokoko
left a comment
There was a problem hiding this comment.
Thanks for this PR — it's an impressively thorough port of the Linear template. The OAuth refresh/rotation handling in jira-oauth-resolver.ts (including the Atlassian rotated-refresh-token race) and the four-way verifyJiraRequestForTenant outcome union are genuinely excellent, and the divergences from the Linear copy (changelog diffing, cloudId tenancy, ADF flattening) are all well-justified. The review below is detailed because the PR is large and security-relevant, not because the foundation is weak.
Verdict: Request changes — CI is currently red, the docs mirror will fail the mutation gate, there's a coupled multi-tenant signature-binding gap, and the ADR/docs still describe the pre-pivot MCP outbound design.
Blocking issues
-
CI is red —
agent/tests/test_config.py:11is missing a comma afterPR_WORKFLOW_IDS. This is aSyntaxErrorat pytest collection time, so the entire agent suite (including the newtest_jira_reactions.pycircuit-breaker tests and the 12-caseTestResolveJiraOauthTokensuite) is currently not running. Once fixed, those suites look well-designed. -
Stale Starlight mirrors — the "Fail build on mutation" gate will reject. Running
node docs/scripts/sync-starlight.mjsat PR HEAD dirtiesdocs/src/content/docs/using/Overview.mdandgetting-started/Quick-start.md(the latter inherited via themainmerge, but it needs a re-sync here). Please runmise //docs:syncand commit the regenerated mirrors. -
Multi-tenant signature binding (coupled pair):
cli/src/commands/jira.ts:539-547mirrors the stack-wide signing secret into every per-tenant OAuth bundle, so one shared secret effectively verifies for all tenants.- A payload verified via the stack-wide fallback carries no binding between the verified secret and
payload.cloudId— the processor then trusts the body-suppliedcloudIdto select the tenant, project→repo mapping, and OAuth bundle (cdk/src/handlers/jira-webhook.ts:136-147,jira-webhook-processor.ts:208,234). A holder of the stack-wide secret can steer a webhook at any tenant's mappings. - Related fail-open:
jira-webhook.ts:152skips the replay-window check entirely whentimestampis absent (Linear rejects a missing timestamp), and timestamp-less deliveries collapse to a single…#unknowndedup key.
Suggested fix: don't mirror the stack-wide secret into per-tenant bundles; after a stack-wide-fallback verification, refuse any
cloudIdother than the sole-tenant fallback result; and log (or reject) when the replay check is skipped so the bypass is observable. Given the project's fail-closed tenet, I'd treat this as blocking. -
ADR-014 and several docs/docstrings still describe the abandoned MCP-outbound design. The final commit correctly pivoted outbound to REST (
jira_reactions.py) — andagent/src/prompt_builder.py:135-146documents the reality perfectly — but the following still claim MCP outbound:docs/decisions/ADR-014-jira-integration.md(still says "Nojira_reactions.pyREST module", statusproposed),JIRA_SETUP_GUIDE.md:31-40,163-168,USER_GUIDE.md:14, the newROADMAP.mdentry,agent/src/channel_mcp.py:55-96,agent/src/config.py:333-344,jira-webhook-processor.ts:172, andjira-integration.ts:70-76. Additionally,channel_mcp.pystill writes a Jira MCP entry that the PR's own docstrings say cannot connect from a headless agent (and emits the SSE URL with"type": "http") — please either drop the"jira"entry fromCHANNEL_MCP_BUILDERSor gate it with an in-band log explaining it's expected to fail. The ADR should also be updated for the actual dedup key ({issueKey}#{webhookEvent}#{timestamp}— the ADR says timestamp-only) and, since #296 already merged an ADR-014, renumbered to ADR-015. -
User-facing instructions reference a non-existent CLI command. The unmapped-project feedback comment (
jira-webhook-processor.ts:247) tells admins to runbgagent jira onboard-project …— the real command ismapand requires a<cloud-id>positional. The CLI's own "Next steps" hint (cli/src/commands/jira.ts:555) also omits the required<cloud-id>. Both printed commands fail verbatim. -
Dead orchestrator wiring with surplus IAM.
cdk/src/stacks/agent.ts:878-898grants the orchestrator registry read +GetSecretValue/PutSecretValueonbgagent-jira-oauth-*"so the concurrency-cap rejection path can post a Jira comment" — butorchestrate-task.tsonly implementsnotifyLinearOnConcurrencyCap; there is no Jira equivalent. Please either implementnotifyJiraOnConcurrencyCap(parity) or remove the grant until the feature lands — unused write-capable IAM on every tenant's credentials is worth avoiding.
Test coverage gaps (would love to see these here, given the surfaces they guard)
- No CLI Jira tests (~993 lines across
cli/src/jira-oauth.ts+commands/jira.ts) — Linear has bothlinear-oauth.test.tsand command tests. The token exchange/refresh-rotation and the missing-refresh_tokenbranch are the highest-consequence untested code in the PR. - No multi-tenant signature test — Linear has a 9-case
linear-webhook-multi-workspace.test.tscovering exactly themismatch/revoked/no-downgrade distinctions that matter for blocking issue 3. jira-feedback.tsis mocked in every test that touches it — the ADF body shape (which Jira REST v3 will 400 on if wrong), timeout, and non-2xx paths have zero direct coverage. Linear shipslinear-feedback.test.ts.- No construct test for
jira-integration.ts— the only construct-level coverage is the table-count bump inagent.test.ts; Linear's construct test asserts key schemas, the dedup TTL attribute, and env wiring. - Suggest parameterizing
cdk/test/contracts/stored-oauth-token-parity.test.tsto also cover the JiraStoredOauthToken↔StoredJiraOauthTokenpair — currently that cross-language contract is prose-only.
Non-blocking suggestions
jira-link.ts:96-112: the Put (active mapping) + Delete (pending row) aren't atomic — considerTransactWriteItems, or return 200 with a WARN if the delete fails after a successful Put (today the user is told linking failed when it actually succeeded).USER_GUIDE.md:7: "There are five ways to interact" — the list now has six.- Consider keeping
client_secretout of the per-tenant bundle the agent role can read — a prompt-injected agent could exfiltrateclient_secret+refresh_tokenand mint tokens outside AWS. Splitting client credentials into a separate secret would harden this. resolveSoleTenantCloudId(jira-webhook-processor.ts:89-109): theScanCommandis unguarded and the processor has no top-level catch/DLQ (parity with Linear, so platform-wide rather than this PR's regression) — a try/catch + a metric on fallback usage would make silent drops countable.- Tiny key-builder helpers for the composite keys (
{cloudId}#{projectKey},{cloudId}#{accountId},pending#{code}) would single-source formats currently interpolated independently at write and read sites (jira-webhook-processor.ts:234,585,jira-link.ts:69,99,111) — the existingjiraOauthSecretName()is the precedent. jira-webhook-processor.ts:123: the| stringin thewebhookEventunion collapses the literal narrowing; and thecloudIddoc comment atjira-webhook.ts:61sits abovematchedWebhookIdsrather than the field it describes.- The 24h replay freshness window is generous; ~1h matches Atlassian's actual retry behavior.
What's notably good
jira-oauth-resolver.test.ts(37 cases) is a model suite —invalid_grantwith rotated vs. same refresh token, concurrent-refresh recovery that skips the second POST, non-fatalPutSecretValuepersistence failure.- The strict (throwing) registry/secret lookups on the verification path, so a transient infra error can't silently downgrade a per-tenant-secured tenant to the stack-wide fallback — exactly the right fail-closed choice.
- Dedup rollback on processor-invoke failure (
jira-webhook.ts:200-227), including the don't-mask-the-original-error nested case. - The advisory comment swallowing in
jira_reactions.pyis logged, circuit-broken, and faithfully mirrorslinear_reactions.py. prompt_builder.py:135-146is the single source of truth on the REST pivot — every other doc just needs to be brought in line with it.
Happy to re-review quickly once the CI fix + mirror sync land; the security binding (issue 3) and the ADR reconciliation (issue 4) are the two I'd most like to see addressed before merge.
test_prompts.py:_config built base from a homogeneous str literal, so ty inferred dict[str, str] and rejected the spread into TaskConfig's bool/int/list fields (17 invalid-argument-type errors). Annotate base as dict[str, Any], matching the existing helper in test_runner.py. This failure was previously masked by the lint syntax error that aborted the build before typecheck ran.
Code Review — Jira Cloud IntegrationReviewed the security-critical paths (webhook HMAC verification, OAuth resolution/refresh, task creation, agent runtime). Overall this is solid, production-minded work that faithfully mirrors the Linear adapter with well-documented divergences. Below are the findings. Significant issues1. Agent-side token refresh can permanently break a tenant (Atlassian rotating refresh tokens)
This differs from Linear, whose copied trust model assumes the stored refresh token survives. It only triggers if the agent catches a token within 60s of expiry before any Lambda refreshes it, but when it hits it silently breaks the tenant. Consider having the agent never refresh — fail closed and let the orchestrator/Lambda path (which has 2. Image-attachment extraction is likely dead code for real Jira issues Minor issues3. ADF→markdown conversion runs twice per issue. 4. Multi-tenant security of the stack-wide fallback secret. When a 5. 6. 7. HMAC over Done well
Test coverageStrong — OAuth refresh state machine (concurrent-refresh race, invalid_grant, malformed expiry, network failure), circuit breaker, channel gating, dry-run-must-not-write, and case-sensitive link codes are all covered. Suggested additions: a test asserting the agent-side refresh's non-persistence behavior (issue # 1), and a processor test against a realistic ADF media node (would surface the dead-code behavior in # 2). RecommendationI'd treat # 1 (agent refresh burning the rotating refresh token) as a blocker pending confirmation of Atlassian's token semantics, and # 2 as should-fix-or-remove. The rest are fine as follow-ups. 🤖 Generated with Claude Code |
This comment was marked as resolved.
This comment was marked as resolved.
Three more issues that were masked behind the earlier lint/typecheck failures, all surfaced once the build progressed to its 'fail on mutation' gate: - ruff format reflowed two long boolean/string lines in jira_reactions.py and test_jira_reactions.py that were committed unformatted. - USER_GUIDE.md still referenced the retired `pr_review` task_type on the intro 'For example' line (a aws-samples#248 merge leftover); the rest of the guide uses `coding/pr-review-v1`. Fixed the source and regenerated the Starlight mirror (using/Overview.md). - Quick-start mirror was missing the Node.js prerequisite line present in the QUICK_START.md source; docs-sync adds it. Full `mise run build` now completes with no working-tree mutation.
…n refresh, ADR/docs
Blocking (krokoko):
- Multi-tenant signature binding: webhook receiver flags stack-wide
verification to the processor, which then ignores the body cloudId and
binds to the sole active tenant (drops when ambiguous). CLI no longer
mirrors the stack-wide secret into new per-tenant bundles; stack-wide is
seeded once from the first tenant. Missing-timestamp replay skip is logged.
- Renumber ADR-014 -> ADR-015 (collides with workflow-driven-tasks); rewrite
to the implemented REST-outbound reality (status accepted), correct dedup
key, binding + refresh-ownership sections. Reconcile JIRA_SETUP_GUIDE,
USER_GUIDE ("six ways"), ROADMAP, channel_mcp.py (placeholder + in-band
log), jira-webhook-processor.ts, jira-integration.ts, agent.ts.
- Fix non-existent CLI command in feedback: onboard-project -> map
<cloud-id> <project-key> --repo (processor + CLI next-steps hint).
- Implement notifyJiraOnConcurrencyCap (Linear parity) so the orchestrator
IAM grant is used and Jira users aren't silently dropped on the cap.
Significant (ayushtr):
- Agent never refreshes the Jira token (Atlassian rotates refresh_tokens;
agent has GetSecretValue only). Use the Lambda-written token verbatim and
fail closed when expiring; Lambda path owns all refreshes.
- ADF media nodes (external images) now render to markdown so attachment
extraction works; ADF->markdown computed once and reused.
Minor: one-sided clock-skew-tolerant timestamp freshness, base64-body guard,
replay window 24h->1h, resolveSoleTenantCloudId Scan comment.
Tests: agent no-refresh/fail-closed suite, multi-tenant binding tests,
base64/missing-timestamp/stack-wide-flag webhook tests, ADF media-node test,
notifyJiraOnConcurrencyCap parity suite. Docs mirrors regenerated.
Relates to aws-samples#288
|
Thanks both — this was an excellent, security-focused review. Pushed @krokoko (blocking)
@ayushtr-aws (significant)
Minor: timestamp freshness is now one-sided with a clock-skew allowance (#6); a base64-encoded body is rejected before verification (#7); Deferred (happy to do in a follow-up)The broader Linear-parity test suites — full CLI Verification: agent 1007 tests green; CDK Jira handler suites + synth suites pass under the pinned Node (22); |
The 'Merge branch main' commit (0f47343) dropped the closing ');' on the Jira mirrorMarkdownFile() call where it interleaved with main's new 'Deploy preview screenshots' mirror block, producing a SyntaxError that broke the //docs:sync build step (and thus the whole build job).
The 'Merge branch main' (0f47343) dropped the closing '});' on the JiraWorkspaceRegistryTableName CfnOutput where main's new GitHubScreenshotIntegration block was spliced in, cascading into ~40 TS1005 errors and breaking //cdk:compile.
Collapse the duplicated 17/14 table-count assertions left by an earlier botched main-merge into a single correct count of 18 (17 enumerated tables incl. the 4 Jira tables, plus github-webhook-dedup from GitHubScreenshotIntegration). Also fix import ordering (github before jira) flagged by eslint import/order.
|
Verdict: One blocker, then merge-ready. The inbound pipeline, multi-tenant routing, token model, IAM scoping, and OAuth dance are all well-engineered and correctly internalize the accumulated Linear-integration lessons. A single correctness bug breaks the Lambda-side user-feedback path; once fixed, this is a strong PR. Reviewed via the structured
🔴 Blocker1. Lambda-side Jira feedback uses the wrong REST base — every comment 401s silently
const base = siteUrl.replace(/\/+$/, ''); // → https://acme.atlassian.net
const url = `${base}/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/comment`;The OAuth token is minted with # agent/src/jira_reactions.py:42,111
JIRA_API_BASE = "https://api.atlassian.com/ex/jira"
url = f"{JIRA_API_BASE}/{cloud_id}/rest/api/3/issue/{issue_key}/comment" # ✅…but the Lambda path posts to the raw Blast radius — the entire Lambda-side outbound feedback surface, both callers:
Errors are swallowed by design (advisory comments must never gate the pipeline), so the user sees silence instead of the "here's why nothing happened" message. This is the valuable Linear v1.1 pre-container-feedback polish — built here but dead on arrival. Why it's an original bug, not template drift: Linear uses a fixed Fix (~3 lines): 🟡 Non-blocking (quality / efficiency)2. Dead Jira MCP entry written on every task hot path
3. Setup writes the OAuth secret twice; partial failure strands an active tenant
4. Wasted hot-path I/O before the trigger filter
What held up under scrutiny (the good)
Bottom line: fix the |
…ration # Conflicts: # cdk/src/stacks/agent.ts
The Lambda-side Jira feedback helper built its REST URL from the tenant site host (`*.atlassian.net`), but the per-tenant 3LO token is minted with `audience=api.atlassian.com` and is only valid against the gateway base `https://api.atlassian.com/ex/jira/{cloudId}/rest/...`. Every pre-container feedback comment (unmapped project, unlinked user, concurrency-cap rejection, createTaskCore non-201) therefore 401'd silently, since these comments are best-effort and swallow errors. Build the URL from `cloudId` (already on `JiraFeedbackContext`) against the gateway base — matching the agent-side path in jira_reactions.py — and drop the unused `siteUrl`. Correct the misleading jira-workspace-registry-table comment that called site_url a "REST base". Adds jira-feedback.test.ts asserting the request host is api.atlassian.com (never atlassian.net), plus encoding and never-throws coverage.
|
@isadeks thanks — great catch on the blocker. Confirmed and fixed in 9dd66b8. 🔴 #1 (REST base) — fixed. Added the dedicated test you suggested: CI verification (ran the
The three 🟡 non-blocking items (dead MCP entry, setup double-write ordering, hot-path I/O before |
Summary
Full Jira Cloud integration, bringing Jira to parity with the existing Linear adapter: a Jira issue gets the
bgagentlabel → ABCA picks it up → an agent run produces a PR → progress flows back as comments on the originating issue. Implements #288.The Linear adapter was the file-for-file template; this PR diverges only where Jira's API forces it (see Jira-specific divergences below). Design rationale is captured in ADR-014 (
docs/decisions/ADR-014-jira-integration.md).Scope: Jira Cloud only (REST v3 + Cloud webhooks), per-tenant OAuth 3LO, label-only trigger. Inbound-only adapter — no DynamoDB Streams consumer, no outbound-notify Lambda. Out of scope: Jira Server/Data Center, Forge/Connect distribution, status-transition/comment-command triggers, bidirectional state sync.
Related
This PR resolves issue #288 to add a Jira integration
Architecture
Inbound (Jira → ABCA):
Outbound (Agent → Jira): progress comments posted on the originating issue at task start and completion.
What's included
Contracts (Phase 1)
'jira'added to theChannelSourceunion on both sides of the wire (cdk/src/handlers/shared/types.ts,cli/src/types.ts) plus theagent/src/models.pydoc comment.check-types-sync.tsallowlistsJiraLinkResponseas CLI-only (parity withSlack/Linearlink responses).CDK constructs & DynamoDB (Phases 2 & 4)
JiraIntegrationconstruct (cdk/src/constructs/jira-integration.ts) — 3 mapping tables + an 8-hour-TTL webhook-dedup table, 3 Lambdas (webhook / processor / link),/jira/*API routes, per-tenantbgagent-jira-oauth-*IAM grants, and cdk-nag suppressions.cloudIdas the tenant prefix so the same project key / account id stays unambiguous across tenants:JiraProjectMappingTable{cloudId}#{projectKey}→owner/repoJiraUserMappingTable{cloudId}#{accountId}(+PlatformUserIndexGSI)JiraWorkspaceRegistryTablejira_cloud_id→ OAuth provider namecdk/src/stacks/agent.ts): grants the orchestrator read on the workspace registry +Get/PutSecretValueon the per-tenant prefix (mirrors Linear's pre-container failure-feedback path).Lambda handlers & shared helpers (Phase 3)
jira-webhook.ts— HMAC-SHA256 verify over the raw body (X-Hub-Signature: sha256=<hex>, constant-time compare), event filtering (jira:issue_created/jira:issue_updated), dedup, async invoke. Silent 200 for unsupported events.jira-webhook-processor.ts— label-add detection by diffingchangelog.items[](notissue.fields.labels), ADF→markdown for the task description,cloudId→tenant resolution,createTaskCorewithchannelSource: 'jira'.jira-link.ts— Cognito-authenticated Jira-account → platform-user linking with dry-run preview.jira-oauth-resolver.ts(per-tenant token resolve/refresh),jira-verify.ts(signature),jira-feedback.ts(REST-based failure comment at the orchestrator boundary).Agent runtime (Phase 5)
channel_mcp.pyrefactored from a single-channel gate to aCHANNEL_MCP_BUILDERSdispatch dict — adding a channel is now one entry, not a rewrite.resolve_jira_oauth_token()added toconfig.py, mirroring the Linear resolver's race-handling and fail-closed semantics (differs only in endpointauth.atlassian.com+ JSON body +JIRA_API_TOKEN).Outbound progress comments —
agent/src/jira_reactions.py(Phase 7)Jira-origin tasks comment on the issue at start (
🤖 picked up…) and completion (✅ finished — PR: <url>/❌ …), wired intopipeline.pyat start / finish / crash paths, parallel to the Linear hooks. Comments are advisory — all network/auth errors are swallowed and never gate the pipeline (with an auth circuit-breaker mirroringlinear_reactions).CLI (Phase 6)
bgagent jirawith 4 v1 subcommands (app-template,setup,link,map) +jira-oauth.ts(portslinear-oauth.ts; Atlassian uses a JSON token endpoint and requiresoffline_accessfor a refresh token). Deferred:add-workspace,update-webhook-secret,invite-user,list-projects.Docs
JIRA_SETUP_GUIDE.md, ADR-014, README / USER_GUIDE / ROADMAP channel listings, and regenerated Starlight mirrors.Jira-specific divergences from the Linear copy
changelog.items[](field: "labels",fromString/toString), not as a full label list; the processor diffs the changelog so re-saving an already-labeled issue doesn't re-trigger.bgagent jira setup. Stored on the per-tenant OAuth bundle with a stack-wide fallback.cloudIdis the tenant key everywhere — not domain or site name.cloudId— the processor falls back to the soleactivetenant in the registry, and deliberately drops (rather than guesses) when zero or multiple active tenants exist, preserving multi-tenant safety.{issueKey}#{webhookEventTimestamp}, 8-hour TTL.Testing
mise //cdk:compile— clean;check-types-sync.ts— passing (verified on this branch).mise run buildgreen.jira-webhook.test.ts,jira-webhook-processor.test.ts,jira-link.test.ts,jira-oauth-resolver.test.ts(32),test_jira_reactions.py,test_channel_mcp.py(jira cases), plusagent.test.tsupdated for the 4 new tables (13→17).Branch rebased onto the fork's
mainso the diff is Jira-only (46 files); the 4 unrelated upstream commits it was originally cut from have been dropped.Acknowledgment
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the project license.