Certify CI promotion via OIDC route

Task 5526 · Done · Coder · created · updated

Epic: Merge lane — CP-scheduled verification queue, plan-gated. Planner Task 5517; the full PRD is BA Task 5515 and the full architecture is Architect Task 5516. Read architecture L6, §3 (the promotion-skip trap), §4.8 and §6 with `task_read`. Builds on C3a (`merge_lane_certificates`, `lane_state`, `gate_cmd_hash`). ## Requirement (work item 6, verbatim) "CI: `test.yml` push-to-develop full run removed when the project's feature is on; promotion PR `test-stack` asks the CP whether the merge-ref tree is certified (node-token route) and exits 0, running the suite only if not. `tenancy-oracles`, `promotion-provenance`, the owner's master gate unchanged." ARCHITECT RESHAPE (settled, L6 / pre-mortem A3): - No workflow holds a CP node token. `test.yml:372` is a random CP→node token, and putting a fleet credential in an Actions secret would hand it to PR code. - So the "node-token route" is realized as a PUBLIC route gated by a **GitHub Actions OIDC token**, following the Google-OIDC-gated Gmail push route (`routes/public.py:216`). - AC6.4's oracle is restated for OIDC below. Build THAT, not a node-token route. ## Approach (settled) - **Route.** `POST /api/public/ci/merge-lane`, with header `Authorization: Bearer <GitHub Actions OIDC JWT>`, audience `jaah-merge-lane`. Body `{tree: "<40 hex>", event: "push"|"pull_request"}`. - Responds 200 `{lane_enabled, certified, certificate: {tree_hash, develop_sha, certified_at, pr_numbers} | null}`. - A bad, expired or foreign-audience token gets 401. - **Verifier.** New `apps/api/app/ci_oidc.py`: - verify the signature against GitHub's JWKS (cached); - check `iss = https://token.actions.githubusercontent.com`, `aud` and expiry; - map the `repository` claim to projects whose GitHub `repo_url` slug equals it. - **Answer semantics.** - `lane_enabled` means some project on that repository is lane-active. - `certified` means a certificate exists for `(project, tree, sha256(current gate_full_cmd))`. With two projects on one repo, either one counts. - Reuse the public-route rate-limit middleware. - **Workflow** (`.github/workflows/test.yml`; read §6): - A new job `merge-lane` with `permissions: {contents: read, id-token: write}`: - it checks out and computes `git rev-parse HEAD^{tree}` (the merge ref on a PR); - it mints the OIDC token and calls the route with a 10 s timeout, the URL taken from `vars.JAAH_LANE_CP_URL`, default `https://app.jaah.ai`; - it outputs `lane_enabled` and `certified`, and ANY failure makes both `false` (fail-open). - ONLY `test-stack` gains `needs: merge-lane` and `if: always() && !(github.event_name == 'push' && needs.merge-lane.outputs.lane_enabled == 'true')`. - `test-stack` ALWAYS runs on PRs, never as a job-level skip: - its first step prints `certified: tree <h> at <t> by develop <sha> (PRs …)` on a hit; - every later step carries `if: needs.merge-lane.outputs.certified != 'true' || github.event_name != 'pull_request'`. - `workflow_dispatch` behaves like push. - `test-jaah-scheduler`, `audit-jaah-deps` and `terraform-validate` are unchanged. ## Acceptance (verbatim; AC6.4 as restated by the architecture) - AC6.1 In `.github/workflows/test.yml`, the push-to-develop run does **not** run `./dc check full` when the lane is ON for this repo's project. A first cheap job asks the CP and the suite jobs are skipped on "ON". On "OFF" **or an unreachable/erroring CP**, the push run behaves exactly as today (fail open to running). - AC6.2 A promotion PR's `test-stack` (PR into master) first asks the CP whether the **merge-ref tree** (`git rev-parse HEAD^{tree}` of the checked-out merge ref), together with the current `gate_full_cmd`, is certified green. - Yes → it exits 0 and logs the certificate (tree hash, when, which develop merge). - No, unreachable, or error → it runs the suite exactly as today. - This holds whether the lane is ON or OFF; a certificate only exists if the lane produced it. - AC6.3 `tenancy-oracles`, `promotion-provenance`, `validate.yml` `gate`, both rulesets and `jaah-deploy.yml` are **unchanged** (a diff oracle shows no edits to those files or jobs). Jaah Deploy still fires on every develop push. - AC6.4 (architecture restatement): the route answers only "certified yes/no + certificate metadata" for the token's own repository. - A valid token for a FOREIGN repository gets `{lane_enabled:false, certified:false, certificate:null}` (never another repo's certificate). - A token with the wrong audience gets 401. - Two projects on one repository answer certified when either holds a certificate for the tree at its own current gate_cmd_hash. Credentials & reach: - Route and verifier tests (AC6.4) and the workflow-logic tests: none beyond the worktree's own `./dc` stack (verified). - AC6.1 OFF mode + AC6.2 uncertified path (read the job list and logs): your own forge login via `jaah-forge` (the standard broker path); GitHub reachable. Status: verified. `token.actions.githubusercontent.com` answered 200 from h2. With the route absent on prod or the lane OFF, the suite runs; this is observable on your own PR's runs. - AC6.1 ON mode (develop push skips `test-stack`): needs the route deployed on prod `app.jaah.ai` AND the lane ON for jaah there. **Deferred**: the route reaches prod only after the human-gated `master` promotion, and enabling the lane on prod is the operator's call. Prove it with a workflow-logic test that feeds the job outputs. - AC6.2 certified-skip on a real promotion PR: needs the route on prod plus a lane certificate there. **Deferred**: the same human-gated promotion. Prove it with a route test plus a workflow-step script test; the operator re-runs the oracle on the first promotion after release. Out of scope: - Epic (verbatim): "GitHub merge queue / merge_group; any CP-side forge calls; changing the manual develop → master promotion; cgroup tuning of GHA runners; squash merges." - BA-derived (verbatim): "No edits to `infra/github/rulesets/*.json`, `.github/workflows/validate.yml`, `tenancy-gate.yml`, `jaah-deploy.yml` or `supply-chain.yml`, or to the `tenancy-oracles` and `promotion-provenance` jobs." "No other repos' CI files: item 6 edits jaah's own `test.yml` only." - Do NOT: - add a node token or any secret to Actions; - fold `test-jaah-scheduler`/`audit-jaah-deps` into the lane (deferred); - skip `test-stack` with a job-level `if` on PRs; - add any forge repo/PR call to `apps/api`. The JWKS fetch is identity-provider key material, and the AC5.6 grep excludes `ci_oidc.py`. Done means: - AC6.4 — API tests with locally signed test JWTs against a stubbed JWKS: - a valid own-repo token with a certificate → `certified:true` plus metadata; - a valid FOREIGN-repo token → `{lane_enabled:false, certified:false, certificate:null}`; - wrong audience → 401; - expired → 401; - two projects on one repo → certified when either holds it; - a certificate under an old `gate_cmd_hash` → `certified:false`. - AC6.1/AC6.2 — a workflow-logic test (parse `test.yml` or run the step script) shows: - push + `lane_enabled=true` → `test-stack` skipped; - push + false, error or timeout → it runs; - PR + `certified=true` → only the certificate step runs, and it prints the certified line; - PR + false → the full steps run. - AC6.1 OFF mode / AC6.2 uncertified path, live: your own PR's run and the develop push after merge show the `merge-lane` job finishing with `lane_enabled=false` and `test-stack` running the suite. - AC6.1 ON mode live (deferred — needs the route on prod plus the lane ON for jaah; human-gated master promotion). - AC6.2 certified-skip live (deferred — same reason). - AC6.3 — `git diff origin/develop --stat` shows no change to `validate.yml`, `tenancy-gate.yml`, `jaah-deploy.yml`, `supply-chain.yml` or `infra/github/rulesets/`, and no change inside the `tenancy-oracles` / `promotion-provenance` job blocks. Required checks: gate, review-lane

Outcome

CI merge-lane OIDC route + test.yml merge-lane job (fail-open). Full gate green; PR #4828 merged; Jaah Deploy ok; live develop run: lane_enabled=false, test-stack ran. Added: repo claimed by >1 org → not certified. Deferred: AC6.1 ON / AC6.2 skip live (needs prod promotion).

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)