Skip to content

fix(jira): validate repo onboarding at map time (#369)#374

Open
mayakost wants to merge 1 commit into
mainfrom
fix/jira-map-onboarding-validation
Open

fix(jira): validate repo onboarding at map time (#369)#374
mayakost wants to merge 1 commit into
mainfrom
fix/jira-map-onboarding-validation

Conversation

@mayakost

Copy link
Copy Markdown
Contributor

Summary

Closes #369.

bgagent jira map <cloud-id> <PROJECT-KEY> --repo owner/repo accepted and persisted a project→repo mapping for a repo with no active Blueprint. Every subsequent label trigger then failed at task creation with 422 REPO_NOT_ONBOARDED — and, because the failure-feedback comment also fails, the operator saw nothing. There was no signal at map time that the repo would never work.

This adds an onboarding check at map/onboard-project time so the misconfiguration surfaces immediately, with actionable guidance.

Changes

  • New shared helper cli/src/repo-onboarding.ts:
    • checkRepoOnboarding() reads the RepoTableName stack output and does a GetItem on the RepoTable keyed by owner/repo, mirroring the runtime gate's exact semantics (repo-config.ts lookupRepo). Returns onboarded / not-onboarded (missing vs inactive) / unverifiable.
    • notOnboardedGuidance() builds the shared remediation message (deploy a Blueprint → re-run; mentions the escape hatch).
    • Inconclusive checks (no RepoTable output, IAM gap) warn and proceed rather than blocking a valid mapping.
  • Wired into both map paths (jira map, linear onboard-project): validate before the PutItem; fail loudly when not onboarded. Added --skip-onboarding-check for the "deploying the Blueprint momentarily" case.
  • Docs: JIRA_SETUP_GUIDE.md and LINEAR_SETUP_GUIDE.md now state the Blueprint-onboarding prerequisite (with the 422 REPO_NOT_ONBOARDED explanation + onboarding steps linking to QUICK_START) and document the new flag. Starlight mirrors regenerated via sync-starlight.mjs.
  • Tests: new repo-onboarding.test.ts (all verdicts + error/rethrow paths) plus map-guard cases in jira.test.ts and linear.test.ts.

Acceptance criteria

  • bgagent jira map rejects a non-onboarded repo with actionable guidance instead of silently persisting a dead mapping.
  • Setup guides state the Blueprint-onboarding prerequisite and link to onboarding steps; Starlight mirrors regenerated.
  • Tests cover the validation path in cli/.
  • Decision: Linear's onboard-project gets the same guard — it shares the create-task-core onboarding 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

  • Hooks were bypassed (--no-verify) on commit/push because core.hooksPath is set in this environment, which blocks prek install. CI hooks still run.
  • Could not run the masking semgrep locally (semgrep not installed). The new catch blocks return informative unverifiable results surfaced to the operator (not silent empty defaults), and getStackOutput rethrows unexpected errors — consistent with the existing pattern in jira.ts/linear.ts.

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).
@mayakost mayakost marked this pull request as ready for review June 17, 2026 19:43
@mayakost mayakost requested review from a team as code owners June 17, 2026 19:43

@ayushtr-aws ayushtr-aws left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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's Key: { repo: opts.repo } matches.
  • Stack output keyRepoTableName exists (cdk/src/stacks/agent.ts:564).
  • Gate semanticslookupRepo in repo-config.ts treats missing and status≠active as not-onboarded, active as onboarded. The CLI's missing/inactive/onboarded verdicts mirror this exactly.
  • Dev-fallback parity — runtime returns onboarded:true when REPO_TABLE_NAME is unset; the CLI returns unverifiable→warn-and-proceed when there's no RepoTableName output. Same net effect (allow). Good alignment.
  • Error handlinggetStackOutput only swallows ValidationError/"does not exist"; other AWS errors rethrow and surface as unverifiable. --repo is validated before the check, so no malformed key reaches DynamoDB.

Suggestions (nits — none blocking)

  • Duplicated getStackOutput + double DescribeStacks. repo-onboarding.ts introduces a getStackOutput that is a near-verbatim copy of the one already in jira.ts:309 (same ValidationError/"does not exist" handling). At runtime the map action now calls DescribeStacks twice on the same stack — once for JiraProjectMappingTableName, once inside checkRepoOnboarding for RepoTableName. 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. map now needs dynamodb:GetItem on the RepoTable in addition to its prior CFN-read + mapping-table-write. The unverifiable→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 scottschreckengaust left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 approved label (+ bug, adapters, cli). Author mayakost is the assignee. Implementation matches the acceptance criteria: (1) map rejects non-onboarded repos with actionable guidance ✅; (2) both setup guides state the Blueprint prerequisite + Starlight mirrors regenerated ✅; (3) tests cover the validation path in cli/ ✅; (4) Linear's onboard-project gets the same guard, documented ✅.
  • Branch fix/jira-map-onboarding-validation carries 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-smoke correctly skipped — no cdk/** or agent/** 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) and cli/src/commands/linear.ts (net −86, a refactor) both diverged, as did the Jira/Linear guides + their Starlight mirrors. The map action on current main gained --status-on-start / --status-on-pr options and status-trimming logic (#605), and reordered validation. git merge-tree confirms real conflict markers in these files — this will not merge or build as-is.
  • Fix: git fetch origin main && git rebase origin/main, resolve the jira.ts / linear.ts / docs conflicts (the onboarding-check block still has a valid home right after the --repo validation and before the PutCommand), then re-run mise //cli:build and cd 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' ? … : undefinednot-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 PK repo, RepoTableName output, runtime-gate semantics parity, no types.ts drift.
  • pr-review-toolkit:silent-failure-hunter — ran. Surfaced N1 (live-error/absent-output collapse) and N2 (message-substring fragility); confirmed the status guard 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-reviewomitted: no IAM policy, Cedar, network, secret, or input-gateway change (the one new permission need — dynamodb:GetItem on RepoTable for the CLI principal — degrades gracefully via unverifiable, and no CDK IAM is defined here).
  • type-design-analyzer / comment-analyzeromitted as low-yield: the one new type (RepoOnboardingResult) is a clean discriminated union already reviewed inline; comments are accurate (verified against repo-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/onboarded mirror the runtime gate's vocabulary; shared notOnboardedGuidance keeps 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 unverifiable warning 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)}` };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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):

Suggested change
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)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

Suggested change
if (name === 'ValidationError' && /does not exist/i.test(message)) {
if (name === 'ValidationError' && /does not exist|not found/i.test(message)) {
return null;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(jira): jira map accepts repos with no Blueprint → every trigger 422 REPO_NOT_ONBOARDED

3 participants