fix(jira): validate repo onboarding at map time (#369)#374
Conversation
bgagent jira map (and linear onboard-project) accepted and persisted a project->repo mapping for a repo with no active Blueprint. Every label trigger then failed at task creation with 422 REPO_NOT_ONBOARDED, with no visible operator feedback. Add a shared checkRepoOnboarding helper (cli/src/repo-onboarding.ts) that reads the RepoTable via the RepoTableName stack output and reports whether the repo has a status='active' row -- mirroring the runtime gate in repo-config.ts. Both map commands now refuse (with actionable guidance) to persist a mapping for a non-onboarded repo, unless --skip-onboarding-check is passed. An inconclusive check (missing output / IAM gap) warns and proceeds rather than blocking a valid mapping. Linear's onboard-project gets the same guard since it shares the create-task-core onboarding gate. Docs: JIRA_SETUP_GUIDE.md and LINEAR_SETUP_GUIDE.md state the Blueprint-onboarding prerequisite and document --skip-onboarding-check; Starlight mirrors regenerated. Tests: new repo-onboarding.test.ts plus map-guard cases in jira/linear command tests (423 pass).
ayushtr-aws
left a comment
There was a problem hiding this comment.
Code Review: PR #374 — fix(jira): validate repo onboarding at map time (#369)
Overview
Adds a pre-write onboarding check to bgagent jira map and bgagent linear onboard-project. Before persisting a project→repo mapping, the command confirms the target repo has a status='active' row in the RepoTable — the same condition the runtime task-submit gate enforces (422 REPO_NOT_ONBOARDED). Inconclusive checks (no stack output, IAM gap) warn and proceed rather than block. Adds a shared cli/src/repo-onboarding.ts helper, a --skip-onboarding-check escape hatch, docs updates with regenerated Starlight mirrors, and tests.
Well-scoped, well-reasoned fix. "Fail loud at config time instead of silently at trigger time" is the right call, and the design decisions (Linear shares the gate; warn-and-proceed on inconclusive) are documented in both code and the PR body.
Correctness — verified against the codebase
- ✅ RepoTable key schema — partition key is
repo(cdk/src/constructs/repo-table.ts:65); the helper'sKey: { repo: opts.repo }matches. - ✅ Stack output key —
RepoTableNameexists (cdk/src/stacks/agent.ts:564). - ✅ Gate semantics —
lookupRepoinrepo-config.tstreats missing and status≠active as not-onboarded, active as onboarded. The CLI'smissing/inactive/onboardedverdicts mirror this exactly. - ✅ Dev-fallback parity — runtime returns
onboarded:truewhenREPO_TABLE_NAMEis unset; the CLI returnsunverifiable→warn-and-proceed when there's noRepoTableNameoutput. Same net effect (allow). Good alignment. - ✅ Error handling —
getStackOutputonly swallowsValidationError/"does not exist"; other AWS errors rethrow and surface asunverifiable.--repois validated before the check, so no malformed key reaches DynamoDB.
Suggestions (nits — none blocking)
- Duplicated
getStackOutput+ doubleDescribeStacks.repo-onboarding.tsintroduces agetStackOutputthat is a near-verbatim copy of the one already injira.ts:309(sameValidationError/"does not exist" handling). At runtime themapaction now callsDescribeStackstwice on the same stack — once forJiraProjectMappingTableName, once insidecheckRepoOnboardingforRepoTableName. Consider exporting the existing helper (or fetching all outputs once and passing them in) to remove both the code duplication and the redundant API call. Minor latency/cost. - New IAM requirement.
mapnow needsdynamodb:GetItemon the RepoTable in addition to its prior CFN-read + mapping-table-write. Theunverifiable→warn-and-proceed path degrades gracefully when the grant is missing, which is the right behavior, and the docs already note "your IAM principal cannot read the table." Fully covered — just flagging the expanded permission surface.
Test coverage — strong
repo-onboarding.test.ts exercises all four verdicts plus both rethrow/IAM-error paths. Command tests assert GetItem-then-PutItem ordering, that --skip-onboarding-check avoids the read, and that the guidance contains REPO_NOT_ONBOARDED. PR reports 423 tests pass with the coverage threshold met.
Security
No injection surface (key is the pre-validated owner/repo); read-only DynamoDB/CFN access; errors surface as informative unverifiable strings, not silent empties or leaked secrets. Author flagged they couldn't run the masking semgrep locally — the catch blocks return detail messages, not credentials, so no masking concern.
Verdict
Approve. Faithful to the runtime gate, good tests and docs, sound design. The two nits (collapse the duplicated getStackOutput / redundant DescribeStacks; note the expanded IAM need) are optional polish, not merge blockers.
scottschreckengaust
left a comment
There was a problem hiding this comment.
Verdict: Request changes — staleness only (the code is sound)
The implementation is correct, well-tested, and faithfully mirrors the runtime onboarding gate. I am requesting changes for one reason: the branch is materially stale and mergeable=CONFLICTING. This is not a rejection of the design — rebase onto main, re-run mise //cli:build + mise //docs:sync, and I expect this to be an approve. The inline notes below are non-blocking nits.
Governance (ADR-003) — PASS
- Backing issue #369 exists, is OPEN, and carries the
approvedlabel (+bug,adapters,cli). Authormayakostis the assignee. Implementation matches the acceptance criteria: (1)maprejects non-onboarded repos with actionable guidance ✅; (2) both setup guides state the Blueprint prerequisite + Starlight mirrors regenerated ✅; (3) tests cover the validation path incli/✅; (4) Linear'sonboard-projectgets the same guard, documented ✅. - Branch
fix/jira-map-onboarding-validationcarries no issue number. Per repo convention this is a soft miss; the real gate — an approved backing issue — is satisfied. Non-blocking. - CI: all required checks pass (
integ-smokecorrectly skipped — nocdk/**oragent/**changes).
Vision alignment — PASS
Advances bounded blast radius and reviewable outcomes (VISION.md tenet: fail-fast on misconfiguration instead of silently swallowing every trigger with an invisible 422 REPO_NOT_ONBOARDED). It does not turn the fire-and-forget async task path into anything interactive — the change is entirely at config time (bgagent jira map / linear onboard-project), which is already an interactive operator command. Tenet-neutral, no ADR needed.
Blocking
B1. Stale branch / merge conflict — mergeable=CONFLICTING, 49 commits behind origin/main.
- Risk:
cli/src/commands/jira.ts(net +335 lines on main since the merge-base) andcli/src/commands/linear.ts(net −86, a refactor) both diverged, as did the Jira/Linear guides + their Starlight mirrors. Themapaction on currentmaingained--status-on-start/--status-on-proptions and status-trimming logic (#605), and reordered validation.git merge-treeconfirms real conflict markers in these files — this will not merge or build as-is. - Fix:
git fetch origin main && git rebase origin/main, resolve thejira.ts/linear.ts/ docs conflicts (the onboarding-check block still has a valid home right after the--repovalidation and before thePutCommand), then re-runmise //cli:buildandcd docs && node scripts/sync-starlight.mjs, and re-verify the mirror is clean. Re-request review after.
Non-blocking suggestions / nits
N1. unverifiable collapses live retryable AWS errors into the same warn-and-proceed path as a structurally-absent stack output (cli/src/repo-onboarding.ts:95-97, :113-115; callers jira.ts / linear.ts). A throttle or AccessDeniedException on the RepoTable GetItem yields the identical single ⚠ … proceeding without the check. line as "stack predates the output," and the mapping is persisted. Because the authoritative gate is the runtime 422 (which still fails closed), this is not a security fail-open — hence a nit, not a blocker — but the operator can't tell "check disabled, ignore" from "check errored, retryable." Consider distinguishing a live-error verdict and either making it require --skip-onboarding-check or at least naming the error class as likely-transient. Inline suggestion below.
N2. getStackOutput "not deployed" detection depends on the English substring does not exist (repo-onboarding.ts:72). If AWS rewords the ValidationError message (e.g. "not found"), a benign not-deployed case starts propagating as a hard error → unverifiable. Fails-toward-warn (safe), so a nit; broaden the regex (/does not exist|not found/i) and add a reworded-message unit test. Note this pattern is copy-pasted from the existing getStackOutput in jira.ts/linear.ts, so it's a pre-existing convention.
N3. Fifth copy of getStackOutput + a redundant DescribeStacks. map now calls DescribeStacks twice on the same stack (once for the mapping-table name, once inside checkRepoOnboarding for RepoTableName). The helper is a near-verbatim copy of the one in jira.ts/linear.ts/github.ts/slack.ts. Consolidating (or fetching outputs once and passing them in) removes the duplication and the extra API call. Minor latency/cost; out of this PR's scope but worth a follow-up. Also: the sibling copies carry an inline nosemgrep: ts-silent-success-masking justification on their return null; the new one (repo-onboarding.ts:73) does not — confirm mise run security:sast:masking stays clean or add the same justified comment.
N4. Non-string status type guard is untested (repo-onboarding.ts:120, :134). A row shaped { repo } (status absent) or { repo, status: 5 } flows through typeof item.status === 'string' ? … : undefined → not-onboarded/inactive with 'unknown' guidance. This guard is CLI-specific (the runtime gate at repo-config.ts:132-134 casts and compares directly, no guard), so nothing upstream protects it. Add a repo-onboarding.test.ts case for absent/non-string status.
N5. Linear command-suite asymmetry. jira.test.ts covers the inactive-row rejection and the warn-and-proceed (no-output) command paths; linear.test.ts covers neither directly (only missing-row reject + skip-flag). Since the command bodies are copy-pasted rather than sharing a branch helper, add the two mirrored Linear cases for symmetry.
JSON-parsing safety (the AttributeError class of bug) — CLEAR
I verified this by reading repo-onboarding.ts, not by trusting a plugin. The item comes from the AWS SDK v3 DynamoDBDocumentClient GetCommand (result.Item, typed Record<string, …> | undefined) — always an object or undefined, never a top-level null/[]/scalar. Line 120 accesses .status behind typeof item.status === 'string', and if (!item) handles undefined. No uncaught AttributeError/null.get()-equivalent is reachable. This PR adds no resp.json()-style HTTP-body parsing (unlike the sibling jira_reactions._get_transitions bug in #605) — it is DynamoDB only.
Documentation — PASS
JIRA_SETUP_GUIDE.md and LINEAR_SETUP_GUIDE.md add the Blueprint-onboarding prerequisite, the 422 REPO_NOT_ONBOARDED explanation, and the --skip-onboarding-check flag; the guidance links to a real QUICK_START.md onboarding section (verified). Starlight mirrors (docs/src/content/docs/using/*.md) are included and re-running sync-starlight.mjs produced no drift — CI's "Fail build on mutation" will pass on the docs. (Re-run after rebase since the Jira guide also changed on main.)
Tests & CI — Strong
repo-onboarding.test.ts covers all verdicts + both error/rethrow paths with realistic SDK-shaped mocks (AI001 satisfied). Command tests assert GetItem-before-PutItem ordering, no-Put-on-reject, --skip-onboarding-check issues no GetItem, and warn-and-proceed still writes. Gaps are N4/N5 (nits). Bootstrap synth-coverage: N/A — no CDK constructs/stacks/CFN resource types changed (CLI + docs only), so ADR-002 bootstrap-policy checks do not apply.
Review agents run
pr-review-toolkit:code-reviewer— ran. Verdict approve; confirmed RepoTable PKrepo,RepoTableNameoutput, runtime-gate semantics parity, notypes.tsdrift.pr-review-toolkit:silent-failure-hunter— ran. Surfaced N1 (live-error/absent-output collapse) and N2 (message-substring fragility); confirmed thestatusguard cannot throw (N4 is a test gap, not a bug) and warn output is visible.pr-review-toolkit:pr-test-analyzer— ran. Confirmed strong coverage; flagged N4 (non-string status untested) and N5 (Linear suite asymmetry)./security-review— omitted: no IAM policy, Cedar, network, secret, or input-gateway change (the one new permission need —dynamodb:GetItemon RepoTable for the CLI principal — degrades gracefully viaunverifiable, and no CDK IAM is defined here).type-design-analyzer/comment-analyzer— omitted as low-yield: the one new type (RepoOnboardingResult) is a clean discriminated union already reviewed inline; comments are accurate (verified againstrepo-config.ts).
Human heuristics
- Proportionality — PASS. A 150-line helper + thin wiring for a real, repeatedly-hit misconfiguration; no over-abstraction.
- Coherence — PASS.
unverifiable/not-onboarded/onboardedmirror the runtime gate's vocabulary; sharednotOnboardedGuidancekeeps Jira/Linear identical. Minor: command wiring is duplicated rather than shared (N3/N5). - Clarity — PASS. Names communicate intent; errors surface (not silently swallowed). One clarity gap: the
unverifiablewarning conflates two operator situations (N1). - Appropriateness — PASS. Mirrors real runtime behavior (
repo-config.ts) and real SDK response shapes; tests assert should-do (AI005) except the untested status guard (N4).
🤖 Generated with Claude Code
| })); | ||
| item = result.Item; | ||
| } catch (err) { | ||
| return { kind: 'unverifiable', detail: `could not read RepoTable '${repoTableName}': ${err instanceof Error ? err.message : String(err)}` }; |
There was a problem hiding this comment.
N1 (nit, non-blocking): this catch maps a live, retryable RepoTable error (throttling, AccessDeniedException, a network blip) into the same unverifiable verdict as a structurally-absent stack output (line 98-102). The callers respond to both identically — one ⚠ … proceeding without the check. line, then persist the mapping. So a throttle on this GetItem can silently persist a genuinely not-onboarded mapping — the exact outcome this feature exists to prevent. It's not a security fail-open (the runtime 422 still fails closed), which is why this is a nit, but the operator can't distinguish "check disabled, ignore" from "check errored, retry." Consider splitting the verdict so a live error is named as likely-transient (or requires --skip-onboarding-check):
| return { kind: 'unverifiable', detail: `could not read RepoTable '${repoTableName}': ${err instanceof Error ? err.message : String(err)}` }; | |
| } catch (err) { | |
| return { kind: 'unverifiable', detail: `could not read RepoTable '${repoTableName}' (likely transient — throttling/IAM; retry or pass --skip-onboarding-check): ${err instanceof Error ? err.message : String(err)}` }; | |
| } |
| } catch (err) { | ||
| const name = (err as Error)?.name ?? ''; | ||
| const message = (err as Error)?.message ?? ''; | ||
| if (name === 'ValidationError' && /does not exist/i.test(message)) { |
There was a problem hiding this comment.
N2 (nit, non-blocking): the "stack not deployed" detection hinges on the English substring does not exist in a non-contractual message field. If AWS rewords the ValidationError (e.g. "Stack [X] not found"), a benign not-deployed case stops matching and propagates as a hard error → unverifiable. Fails-toward-warn, so safe, but brittle. Broaden the match and add a reworded-message unit test:
| if (name === 'ValidationError' && /does not exist/i.test(message)) { | |
| if (name === 'ValidationError' && /does not exist|not found/i.test(message)) { | |
| return null; | |
| } |
Summary
Closes #369.
bgagent jira map <cloud-id> <PROJECT-KEY> --repo owner/repoaccepted and persisted a project→repo mapping for a repo with no active Blueprint. Every subsequent label trigger then failed at task creation with422 REPO_NOT_ONBOARDED— and, because the failure-feedback comment also fails, the operator saw nothing. There was no signal atmaptime that the repo would never work.This adds an onboarding check at
map/onboard-projecttime so the misconfiguration surfaces immediately, with actionable guidance.Changes
cli/src/repo-onboarding.ts:checkRepoOnboarding()reads theRepoTableNamestack output and does aGetItemon the RepoTable keyed byowner/repo, mirroring the runtime gate's exact semantics (repo-config.tslookupRepo). Returnsonboarded/not-onboarded(missing vs inactive) /unverifiable.notOnboardedGuidance()builds the shared remediation message (deploy a Blueprint → re-run; mentions the escape hatch).jira map,linear onboard-project): validate before thePutItem; fail loudly when not onboarded. Added--skip-onboarding-checkfor the "deploying the Blueprint momentarily" case.JIRA_SETUP_GUIDE.mdandLINEAR_SETUP_GUIDE.mdnow state the Blueprint-onboarding prerequisite (with the422 REPO_NOT_ONBOARDEDexplanation + onboarding steps linking to QUICK_START) and document the new flag. Starlight mirrors regenerated viasync-starlight.mjs.repo-onboarding.test.ts(all verdicts + error/rethrow paths) plus map-guard cases injira.test.tsandlinear.test.ts.Acceptance criteria
bgagent jira maprejects a non-onboarded repo with actionable guidance instead of silently persisting a dead mapping.cli/.onboard-projectgets the same guard — it shares thecreate-task-coreonboarding gate. Documented in code comments and both guides.Testing
cd cli && mise run build→ 423 tests pass, eslint clean, coverage threshold met.cd docs && node scripts/sync-starlight.mjs→ stable (idempotent on re-run).Notes
--no-verify) on commit/push becausecore.hooksPathis set in this environment, which blocksprek install. CI hooks still run.unverifiableresults surfaced to the operator (not silent empty defaults), andgetStackOutputrethrows unexpected errors — consistent with the existing pattern injira.ts/linear.ts.