5.6 KiB
ccam ci status — unblock Stage 13's CI-wait half
Status: approved 2026-08-25.
Problem
ship-feature-lane's Stage 13 (post-merge verification) is hardcoded
skipped. Two integrations gate it: dev-CI-wait (watch CI on development
after a PR merges, before QCing the deployed site) and dev-QC (QC the
deployed site). Neither exists yet.
This spec covers only the CI-wait half. ccam lanes integration ci_wait
already exists (server/lib/lane-profile.js:isIntegrationEnabled,
bin/ccam.js:cmdLanesIntegration) and reports whether a profile wants the
integration on — but nothing acts on it. ccam ci doesn't exist
(skills/ship-feature-lane/SKILL.md:167 notes this explicitly at Stage 10,
where the skill falls back to raw gh pr checks/gh run view).
Dev-QC (QCing the deployed site) is a separate, later task — out of scope here. Stage 13's dev-QC half stays noted as unavailable until that lands.
Approach
A read-only status snapshot, not a wait loop. Per the project's
orchestration doctrine (docs/LANES.md § "Orchestration: what CCAM does NOT
do" — no chaining, no queue, no retry logic), ccam ci status reports a
fact once; it does not poll, block, or retry. The driving skill owns the
poll loop and the fix/escalate decision, the same shape Stage 12 already
uses for watching a PR (sleep 300 backgrounded, re-invoked per iteration).
This also means ccam ci status needs no flake classification. Stage 10
already does that triage (infra/flake signature vs. real failure) reading
gh output directly — Stage 13 reuses the same triage, now reading this
command's structured JSON instead of raw gh.
Server-backed, following the sync-base pattern, not a CLI-local gh
spawn. Every existing ccam command is a thin HTTP client — no command in
bin/ccam.js spawns a child process directly (worktree.js's git()
helper is the only child-process boundary CCAM has today, always
server-side). A new server/lib/gh.js mirrors that helper:
async function gh(cwd, args) { /* execFile("gh", args, { cwd }) */ }
New route: GET /api/lanes/:id/ci/status?ref=<branch-or-sha>
- Resolves the lane's
cwd(same lookup every other lane route uses). - Runs
gh run list --branch <ref> --json databaseId,conclusion,status,url --limit 1to find the latest run for that ref, thengh run view <id> --json jobsfor per-job conclusions. (Asharef:gh run listaccepts a commit SHA via--commit, not--branch— the route tries--branchfirst and falls back to--commitif the ref doesn't look like a branch name a lane would use, i.e. it's a 7-40 char hex string.) - No run found for the ref at all →
state: "pending"(CI hasn't started yet, not an error). ghmissing or not authenticated →{ error: { message } }, HTTP 500.
New CLI: ccam lanes ci status <branch-or-sha> [<lane-id>]
- GETs the route above, prints the JSON, sets
process.exitCode. - Resolves the lane the same way every other
ccam lanes <verb>command does (resolveLaneArg), so it works with an implicit lane-from-cwd or an explicit id, matchingccam lanes integration.
Response shape
{
"state": "success",
"ref": "a1b2c3d",
"checks": [
{ "name": "server-tests", "conclusion": "failure", "url": "https://github.com/.../runs/123" }
]
}
state is derived, never taken from a single field: any job still
in_progress/queued → pending; else any failure/cancelled/
timed_out → failure; else (success/skipped only) → success.
Exit codes
CLI mirrors the response into a process exit code, same convention as
sync-base's distinct exit codes per outcome:
0—state: "success"1—state: "failure", OR any execution error (ghmissing, lane not found, route error) — printed as✖ ci status → <message>on stderr3—state: "pending"(covers both "checks running" and "no run found yet for this ref")
Skill changes
skills/ship-feature-lane/SKILL.md Stage 13: replace the hardcoded-skip
note with a poll loop mirroring Stage 12's:
- Get the merge SHA:
gh pr view <pr_url> --json mergeCommit -q .mergeCommit.oid. - Loop (
sleep 300,run_in_background: true, bump heartbeat each iteration):ccam lanes ci status <sha>.pending→ keep waiting.failure→ triage like Stage 10 (gh run viewfor job logs on the same run id returned inchecks[].url), fix + re-enter from Stage 2, or escalate.success→ dev-QC half is still unavailable (separate task) — note that explicitly and proceed to Stage 14, same as today but now honest that CI-wait specifically passed.
- Update
docs/LANES.md: the "Stages currently hardcoded skipped" list (Stage 13 becomes "CI-wait works; dev-QC still off"), theccam lanes integrationsection's claim thatci_wait's consumer isn't built, and addccam ci statusto the command reference.
Non-goals
- No flake auto-classification or auto-rerun inside
ccam ci status— stays a dumb fact-reporter; the skill keeps owning that judgment, per CCAM's no-retry-logic rule. - No change to Stage 10 (PR CI watch) — it keeps using raw
ghdirectly. Reusing this command there is a plausible future cleanup, not required now. - No dev-QC implementation — separate task, tracked independently.
Testing
server/tests— new test for the/api/lanes/:id/ci/statusroute, mockingexecFile/ghoutput for: pending (no run yet), pending (in-progress run), success, failure, andgh-not-found — pattern after the existingsync-baseroute tests.npm run test:serverbefore considering this done.