diff --git a/.github/workflows/cli-ci.yml b/.github/workflows/cli-ci.yml index 9d223e5ac..3799a9e56 100644 --- a/.github/workflows/cli-ci.yml +++ b/.github/workflows/cli-ci.yml @@ -21,14 +21,20 @@ concurrency: cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} permissions: + actions: read contents: read jobs: changes: name: cli / changes + permissions: + actions: read + contents: read + pull-requests: read runs-on: ubuntu-latest outputs: relevant: ${{ steps.filter.outputs.relevant }} + trusted_promotion: ${{ steps.promotion.outputs.trusted }} mutation_scopes: ${{ steps.mutation.outputs.scopes }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -81,25 +87,120 @@ jobs: echo "relevant=$relevant" >> "$GITHUB_OUTPUT" + # A promotion branch is a snapshot of next, not the live next branch. Reuse only proves + # a tree already gated on next: every missing Git or API proof falls back to mutations. + - name: Check whether a promotion snapshot or main merge passed next + id: promotion + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + EVENT_NAME: ${{ github.event_name }} + GITHUB_REF: ${{ github.ref }} + CURRENT_SHA: ${{ github.sha }} + BASE_REF: ${{ github.event.pull_request.base.ref }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + run: | + set -euo pipefail + trusted=false + reason="not a reusable promotion snapshot" + + has_successful_next_gate() { + local run_ids run_id gate + reason="no successful cli CI push run found for the snapshot SHA" + if run_ids="$(gh api --paginate "/repos/$REPO/actions/workflows/cli-ci.yml/runs?branch=next&event=push&status=completed&head_sha=$HEAD_SHA&per_page=100" \ + --jq '.workflow_runs[] | select(.conclusion == "success") | .id' 2>/dev/null)"; then + while IFS= read -r run_id; do + [[ -z "$run_id" ]] && continue + if gate="$(gh api "/repos/$REPO/actions/runs/$run_id/jobs?filter=latest&per_page=100" \ + --jq 'any(.jobs[]; .name == "cli / gate" and .conclusion == "success")' 2>/dev/null)"; then + if [[ "$gate" == "true" ]]; then + reason="reusing the successful cli CI gate from next run $run_id" + return 0 + fi + reason="cli / gate did not pass in next run $run_id" + else + reason="could not inspect cli CI run $run_id" + fi + done <<< "$run_ids" + else + reason="could not list successful cli CI push runs" + fi + return 1 + } + + # GitHub tests pull requests at a synthetic merge ref. The source gate may be reused + # only if main is already in the snapshot and that merge ref has the same file tree. + if [[ "$EVENT_NAME" == "pull_request" && "$BASE_REF" == "main" && "$HEAD_REF" =~ ^promote/next-to-main-[0-9]+$ && "$HEAD_REPO" == "$REPO" ]]; then + if ! git merge-base --is-ancestor "$BASE_SHA" "$HEAD_SHA" 2>/dev/null; then + reason="main base is not an ancestor of the promotion snapshot" + elif ! snapshot_tree="$(git rev-parse "$HEAD_SHA^{tree}" 2>/dev/null)" || ! merge_tree="$(git rev-parse "$CURRENT_SHA^{tree}" 2>/dev/null)"; then + reason="could not prove the promotion merge tree" + elif [[ "$merge_tree" != "$snapshot_tree" ]]; then + reason="promotion merge tree differs from the snapshot" + elif has_successful_next_gate; then + trusted=true + fi + # A main push is reusable only for the exact merge of a numbered, same-repository + # promotion PR. Parent two is its source snapshot; neither a squash nor an unrelated + # merge can satisfy this proof. + elif [[ "$EVENT_NAME" == "push" && "$GITHUB_REF" == "refs/heads/main" ]]; then + if ! parents="$(git rev-list --parents -n 1 "$CURRENT_SHA" 2>/dev/null)" || ! read -r merge_sha first_parent snapshot_sha extra_parent <<< "$parents" || [[ "$merge_sha" != "$CURRENT_SHA" || -z "$first_parent" || -z "$snapshot_sha" || -n "${extra_parent:-}" ]]; then + reason="main commit is not a two-parent merge" + elif ! snapshot_tree="$(git rev-parse "$snapshot_sha^{tree}" 2>/dev/null)" || ! main_tree="$(git rev-parse "$CURRENT_SHA^{tree}" 2>/dev/null)"; then + reason="could not read the main merge or snapshot tree" + elif [[ "$main_tree" != "$snapshot_tree" ]]; then + reason="main merge tree differs from the promotion snapshot" + elif ! prs="$(gh api "/repos/$REPO/commits/$CURRENT_SHA/pulls" \ + --jq '.[] | select(.base.ref == "main" and (.head.ref | test("^promote/next-to-main-[0-9]+$")) and .merged_at != null) | [.base.repo.full_name, .head.repo.full_name, .head.ref, .head.sha, .merged_at, .merge_commit_sha] | @tsv' 2>/dev/null)"; then + reason="could not inspect pull requests associated with the main commit" + else + matching_prs=0 + while IFS=$'\t' read -r base_repo head_repo head_ref pr_head_sha merged_at merge_commit_sha; do + if [[ "$base_repo" == "$REPO" && "$head_repo" == "$REPO" && "$head_ref" =~ ^promote/next-to-main-[0-9]+$ && "$pr_head_sha" == "$snapshot_sha" && -n "$merged_at" && "$merge_commit_sha" == "$CURRENT_SHA" ]]; then + ((matching_prs += 1)) + fi + done <<< "$prs" + + if [[ "$matching_prs" -ne 1 ]]; then + reason="no unique matching promotion pull request proved this main merge" + else + HEAD_SHA="$snapshot_sha" + if has_successful_next_gate; then + trusted=true + fi + fi + fi + fi + + echo "promotion mutation reuse: $reason" + echo "trusted=$trusted" >> "$GITHUB_OUTPUT" + - name: Decide which mutation scopes a change can move id: mutation run: | set -euo pipefail - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - BASE="${{ github.event.pull_request.base.sha }}" - HEAD="${{ github.event.pull_request.head.sha }}" - else - BASE="${{ github.event.before }}" - HEAD="${{ github.sha }}" - fi - if [[ -z "$BASE" || "$BASE" =~ ^0+$ ]]; then - changed="" - all=true + if [[ "${{ steps.promotion.outputs.trusted }}" == "true" ]]; then + scopes='[]' else - changed="$(git diff --name-only "$BASE" "$HEAD")" - all=false + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + BASE="${{ github.event.pull_request.base.sha }}" + HEAD="${{ github.event.pull_request.head.sha }}" + else + BASE="${{ github.event.before }}" + HEAD="${{ github.sha }}" + fi + if [[ -z "$BASE" || "$BASE" =~ ^0+$ ]]; then + changed="" + all=true + else + changed="$(git diff --name-only "$BASE" "$HEAD")" + all=false + fi + scopes="$(ALL="$all" CHANGED="$changed" node cli/scripts/mutation-scopes-to-run.mjs)" fi - scopes="$(ALL="$all" CHANGED="$changed" node cli/scripts/mutation-scopes-to-run.mjs)" echo "mutation scopes: $scopes" echo "scopes=$scopes" >> "$GITHUB_OUTPUT" diff --git a/aidd_docs/memory/deployment.md b/aidd_docs/memory/deployment.md index c7a486b95..c11fbe972 100644 --- a/aidd_docs/memory/deployment.md +++ b/aidd_docs/memory/deployment.md @@ -9,7 +9,7 @@ Where the project runs and how it ships: CI/CD, environments, and release. | Workflow | Runs | | --- | --- | | `ci.yml` | commitlint on pull requests and on `main`'s tip, plus the PR title itself — the subject a squash merge uses — then release-please on `main` and the release jobs | -| `cli-ci.yml` | the `cli` and `kanban` gates — job list in the CLI bank. No `paths:` filter, deliberately: it runs on every push and pull request, and a `changes` job decides in bash whether the rest has anything to do — `cli/**`, `kanban/**`, `scripts/__tests__/**`, `README.md`, the workflow file itself, and `plugins/aidd-telemetry/**` except its `*.md` prose | +| `cli-ci.yml` | the `cli` and `kanban` gates — job list in the CLI bank. No `paths:` filter, deliberately: it runs on every push and pull request, and a `changes` job decides in bash whether the rest has anything to do — `cli/**`, `kanban/**`, `scripts/__tests__/**`, `README.md`, the workflow file itself, and `plugins/aidd-telemetry/**` except its `*.md` prose. Mutations skip only for a same-repository numeric `promote/next-to-main-*` snapshot whose `cli / gate` passed in a successful `next` push and whose PR merge tree equals that snapshot with `main` already its ancestor; the resulting `main` push reuses it only for that exact two-parent promotion merge when its tree, associated merged PR, and source snapshot all match. Missing, failed, unreadable, or mismatched Git/API proof keeps normal mutation scopes. All non-mutation checks still run on the current PR merge ref or `main` commit. | | `validate.yml` | plugin and marketplace manifests against their schemas, plus the whole pre-commit over the whole tree | | `codeql.yml` | code scanning | | `promote.yml` | opens the `next` to `main` promote PR, merge auto-merge | diff --git a/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/phase-1.md b/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/phase-1.md new file mode 100644 index 000000000..b3c42f37d --- /dev/null +++ b/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/phase-1.md @@ -0,0 +1,68 @@ +--- +status: done +--- + +# Instruction: Reuse a validated promotion snapshot + +## Architecture projection + +> Tree of the final files. ✅ create · ✏️ modify · ❌ delete + +```txt +. +└── .github/ + └── workflows/ + └── cli-ci.yml ✏️ classify promotion snapshots, verify a prior next gate, and skip only the duplicate mutation matrix +``` + +## User Journey + +```mermaid +flowchart TD + A[system: bot opens promote/next-to-main snapshot PR] --> B{same SHA has successful push gate on next?} + B -- yes --> C[run normal merge-ref checks without mutations] + C --> D[cli / gate passes] + B -- no --> E[run normal CLI and selected mutation jobs] + E --> D +``` + +## Test Scope + +```mermaid +--- +title: Test scope +--- +journey + section Setup + promotion snapshot with a successful next gate => workflow receives its head SHA: 5: system + section Happy path + trigger promotion PR => mutations skip while normal checks validate the merge ref: 5: system + section Edge case - missing proof + no successful next gate for the SHA => normal CLI and mutation jobs remain required: 5: system +``` + +## Tasks to do + +### `1)` Classify a trusted promotion snapshot + +> Prove the snapshot was already gated on `next`, fail closed otherwise. + +1. Add the least privilege needed to read workflow runs. +2. Query successful `push` runs on `next` for the exact PR head SHA and require the `cli / gate` job to have succeeded. +3. Expose an empty mutation scope list only after that proof; preserve normal scope selection otherwise. + +### `2)` Keep merge integration coverage + +> Remove only repeated source mutation testing from the PR merge ref. + +1. Skip only `cli-mutation` for a trusted promotion snapshot. +2. Preserve all existing non-mutation jobs and their gate wiring. + +## Test acceptance criteria + +| Task | Acceptance criteria | +| --- | --- | +| 1 | Only a `promote/next-to-main-` PR whose exact head SHA already has a successful `push` `cli / gate` on `next` may set mutation scopes to empty. | +| 1 | Missing, failed, or unreadable validation proof does not skip mutations. | +| 2 | A trusted promotion still runs coverage, smoke, build, platform, and other non-mutation checks against GitHub's PR merge ref. | +| 2 | Ordinary pull requests retain their existing job and mutation behavior. | diff --git a/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/phase-2.md b/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/phase-2.md new file mode 100644 index 000000000..e015bb948 --- /dev/null +++ b/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/phase-2.md @@ -0,0 +1,66 @@ +--- +status: done +--- + +# Instruction: Lock the workflow contract + +## Architecture projection + +> Tree of the final files. ✅ create · ✏️ modify · ❌ delete + +```txt +. +├── scripts/ +│ └── __tests__/ +│ └── cli-ci-gate-covers-every-job.test.js ✏️ assert promotion reuse and required-gate coverage +└── aidd_docs/ + └── memory/ + └── deployment.md ✏️ describe promotion-specific mutation reuse +``` + +## User Journey + +```mermaid +flowchart TD + A[system: workflow configuration changes] --> B[static workflow contract test] + B --> C{promotion safety and gate fan-in hold?} + C -- yes --> D[CI configuration is valid] + C -- no --> E[descriptive test failure] +``` + +## Test Scope + +```mermaid +--- +title: Test scope +--- +journey + section Setup + CI workflow YAML and its contract test => workflow parsed: 5: system + section Happy path + run the workflow contract test => trusted promotion, fallback, retained merge checks, and gate wiring are asserted: 5: system + section Edge case - future job + add an ungated job => gate-coverage test fails: 5: system +``` + +## Tasks to do + +### `1)` Assert promotion safety structurally + +> Make regressions in the promotion fast path visible before merge. + +1. Extend the existing workflow contract test with the trusted-promotion and fallback invariants. +2. Assert a trusted promotion empties only mutation scopes and retains the existing gate fan-in. + +### `2)` Record the operating model + +> Keep deployment memory aligned with the workflow. + +1. Replace the generic CLI CI description with its promotion reuse rule and fail-closed fallback. + +## Test acceptance criteria + +| Task | Acceptance criteria | +| --- | --- | +| 1 | The contract test fails if trusted promotion detection, mutation fallback, retained checks, or gate fan-in is removed. | +| 2 | Deployment memory accurately states when promotion skips mutations and what still runs. | diff --git a/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/phase-3.md b/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/phase-3.md new file mode 100644 index 000000000..d85d6e8e5 --- /dev/null +++ b/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/phase-3.md @@ -0,0 +1,69 @@ +--- +status: done +--- + +# Instruction: Bind promotion reuse to the tested merge tree + +## Architecture projection + +> Tree of the final files. ✅ create · ✏️ modify · ❌ delete + +```txt +. +├── .github/ +│ └── workflows/ +│ └── cli-ci.yml ✏️ fail closed when main is not already contained by the promotion snapshot +└── scripts/ + └── __tests__/ + └── cli-ci-gate-covers-every-job.test.js ✏️ lock the ancestry requirement +``` + +## User Journey + +```mermaid +flowchart TD + A[promotion snapshot PR to main] --> B{main base is ancestor of snapshot?} + B -- yes, next gate passed --> C[skip duplicate mutations] + B -- no --> D[run selected mutation scopes] + C --> E[run all merge-ref checks] + D --> E +``` + +## Test Scope + +```mermaid +flowchart TD + A[workflow contract] --> B{ancestry guard present before empty scopes?} + B -- yes --> C[contract passes] + B -- no --> D[contract fails] +``` + +## Wireframe + +```txt +No UI: GitHub Actions workflow behavior only. +``` + +## Tasks to do + +### `1)` Prove promotion content is unchanged + +> Reuse a `next` mutation result only when the PR merge cannot add untested main content. + +1. Read the promotion PR base and snapshot SHAs from the event. +2. Require the base SHA to be an ancestor of the snapshot before marking a promotion trusted. +3. Keep every failed or unreadable Git proof on the normal mutation path. + +### `2)` Lock the fail-closed guard + +> Make a future removal of the ancestry check fail locally. + +1. Extend the workflow contract test with the base-to-snapshot proof and fallback expectation. + +## Test acceptance criteria + +| Task | Acceptance criteria | +| --- | --- | +| 1 | A promotion with uncontained `main` content cannot set `mutation_scopes=[]`. | +| 1 | A promotion whose base is contained by its exact snapshot and whose `next` gate passed retains the mutation skip. | +| 2 | The contract test fails when the ancestry proof or fail-closed fallback is removed. | diff --git a/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/phase-4.md b/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/phase-4.md new file mode 100644 index 000000000..7eeb9eea7 --- /dev/null +++ b/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/phase-4.md @@ -0,0 +1,76 @@ +--- +status: done +--- + +# Instruction: Reuse the proven promotion merge on main + +## Architecture projection + +> Tree of the final files. ✅ create · ✏️ modify · ❌ delete + +```txt +. +├── .github/ +│ └── workflows/ +│ └── cli-ci.yml ✏️ recognize a proven promotion merge on main and skip only duplicate mutations +├── scripts/ +│ └── __tests__/ +│ └── cli-ci-gate-covers-every-job.test.js ✏️ lock main-merge proof and unchanged fallback +└── aidd_docs/ + └── memory/ + └── deployment.md ✏️ document both trusted reuse paths +``` + +## User Journey + +```mermaid +flowchart TD + A[push merge commit to main] --> B{matching promotion PR?} + B -- no --> C[run selected mutations] + B -- yes --> D{merge tree equals proven snapshot and next gate passed?} + D -- no --> C + D -- yes --> E[skip duplicate mutations] + C --> F[run normal non-mutation checks] + E --> F +``` + +## Test Scope + +```mermaid +flowchart TD + A[workflow contract] --> B{promotion PR identity, tree equality, and next gate required?} + B -- yes --> C[contract passes] + B -- no --> D[contract fails] +``` + +## Wireframe + +```txt +No UI: GitHub Actions workflow behavior only. +``` + +## Tasks to do + +### `1)` Classify a trusted main promotion merge + +> Skip mutations on main only for the exact content already gated on next. + +1. Identify the merged promotion PR and its snapshot with read-only repository data. +2. Require the final main commit tree to equal that snapshot's tree. +3. Reuse only the successful `cli / gate` run for the snapshot's exact SHA on `next`. + +### `2)` Preserve normal behavior and explain it + +> Keep every ambiguous, failed, or unrelated main push fully protected. + +1. Route missing PR identity, unavailable API data, mismatched trees, and failed gates to normal mutation scope selection. +2. Assert the trusted-main contract structurally and update deployment memory. + +## Test acceptance criteria + +| Task | Acceptance criteria | +| --- | --- | +| 1 | Only a merge from the numbered promotion branch whose final tree equals the snapshot may skip mutations on `main`. | +| 1 | The matching snapshot must have a successful `push` `cli / gate` on `next`. | +| 2 | Every missing, unreadable, mismatched, or unrelated proof preserves normal mutation execution. | +| 2 | Non-mutation jobs and gate fan-in remain unchanged for all events. | diff --git a/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/plan.md b/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/plan.md new file mode 100644 index 000000000..ffc8c9e04 --- /dev/null +++ b/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/plan.md @@ -0,0 +1,39 @@ +--- +objective: "A next-to-main promotion and its resulting main merge reuse an already-passing mutation gate only when the exact tested tree is proven unchanged." +status: implemented +--- + +# Plan: Reuse validated next CI through promotion + +## Overview + +| Field | Value | +| --- | --- | +| **Goal** | Remove duplicate mutation matrices from promotion and its resulting `main` merge without weakening any gate. | +| **Source** | User request: strict SDLC after the CI-wide challenge. | + +## Phases + +| # | Phase | File | +| --- | --- | --- | +| 1 | Reuse a validated promotion snapshot | [`phase-1.md`](./phase-1.md) | +| 2 | Lock the workflow contract | [`phase-2.md`](./phase-2.md) | +| 3 | Bind promotion reuse to the tested merge tree | [`phase-3.md`](./phase-3.md) | +| 4 | Reuse the proven promotion merge on `main` | [`phase-4.md`](./phase-4.md) | + +## Resources + +| Source | Verified | +| --- | --- | +| https://docs.github.com/en/rest/actions/workflow-runs | Workflow runs can be filtered by branch, event, and head SHA with Actions read permission. | +| https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows | A pull-request checkout uses the synthetic merge ref, so a promotion-specific smoke can validate the merged result. | +| Repository evidence: PR #809 | Its merge tree equalled the promoted `next` snapshot, but that invariant was convention-only rather than enforced. | + +## Decisions + +| Decision | Why | +| --- | --- | +| Reuse only a successful `push` run for the exact snapshot SHA on `next`. | The promotion source has already satisfied `next`'s required gate; absent or unsuccessful proof must not bypass mutations. | +| Keep all non-mutation jobs on the promotion PR merge ref. | Coverage, smoke, build, and platform checks still validate the merge of release metadata from `main`. | +| Require the promotion PR base to be an ancestor of its snapshot before reuse. | A previously green `next` run is insufficient if `main` contributes untested content to the PR merge tree. | +| On a `main` push, require both the matching promotion PR and tree equality with its snapshot. | A matching source SHA alone cannot prove that the merge commit carries the same content. | diff --git a/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/review.md b/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/review.md new file mode 100644 index 000000000..52d18a63d --- /dev/null +++ b/aidd_docs/tasks/2026_09/2026_09_09_promotion-ci-reuse/review.md @@ -0,0 +1,49 @@ +# Review: Reuse validated next CI through promotion + +- **Verdict**: approve +- **Diff**: `origin/next...92941865` +- **Axes run**: code, functional, relevancy +- **Date**: 2026_09_10 +- **Findings**: 0 critical, 0 warning, 0 minor + +## Phases + +### Phase 1 — Reuse a validated promotion snapshot + +- [x] Only a same-repository `promote/next-to-main-` PR whose exact head SHA has a successful `push` `cli / gate` on `next` may set mutation scopes to empty. — `.github/workflows/cli-ci.yml:110-145,185-202` +- [x] Missing, failed, or unreadable validation proof does not skip mutations. — `.github/workflows/cli-ci.yml:107-131,136-145,185-202` +- [x] A trusted promotion still runs coverage, smoke, build, platform, and other non-mutation checks against GitHub's PR merge ref. — `scripts/__tests__/cli-ci-gate-covers-every-job.test.js:120-140` +- [x] Ordinary pull requests retain their existing job and mutation behavior. — `.github/workflows/cli-ci.yml:136-145,185-202` + +### Phase 2 — Lock the workflow contract + +- [x] The contract test fails if trusted promotion detection, mutation fallback, retained checks, or gate fan-in is removed. — `scripts/__tests__/cli-ci-gate-covers-every-job.test.js:63-140` +- [x] Deployment memory accurately states when promotion skips mutations and what still runs. — `aidd_docs/memory/deployment.md:12` + +### Phase 3 — Bind promotion reuse to the tested merge tree + +- [x] A promotion with uncontained `main` content cannot set `mutation_scopes=[]`. — `.github/workflows/cli-ci.yml:136-145,185-202` +- [x] A promotion whose base is contained by its exact snapshot and whose `next` gate passed retains the mutation skip. — `.github/workflows/cli-ci.yml:136-145` +- [x] The contract test fails when the ancestry proof or fail-closed fallback is removed. — `scripts/__tests__/cli-ci-gate-covers-every-job.test.js:84,115-117` + +### Phase 4 — Reuse the proven promotion merge on `main` + +- [x] Only a two-parent merge from the numbered, same-repository promotion branch whose final tree equals the snapshot may skip mutations on `main`. — `.github/workflows/cli-ci.yml:149-175,185-202` +- [x] The matching snapshot must have a successful `push` `cli / gate` on `next`. — `.github/workflows/cli-ci.yml:110-131,170-172` +- [x] Every missing, unreadable, mismatched, or unrelated proof preserves normal mutation execution. — `.github/workflows/cli-ci.yml:107-108,149-175,185-202` +- [x] Non-mutation jobs and gate fan-in remain unchanged for all events. — `scripts/__tests__/cli-ci-gate-covers-every-job.test.js:12-27,120-140` + +## Findings + +| Sev | Kind | Phase | Location | Issue | Fix | +| --- | --- | --- | --- | --- | --- | +| — | — | — | — | None. | — | + +## Verification + +| Metric | Value | +| --- | --- | +| Verified | 100% (13/13) | +| Files checked | `.github/workflows/cli-ci.yml`, `scripts/__tests__/cli-ci-gate-covers-every-job.test.js`, `aidd_docs/memory/deployment.md`, `.github/workflows/promote.yml`, `.github/rulesets/main.json`, `.github/rulesets/next.json`, `phase-1.md`, `phase-2.md`, `phase-3.md`, `phase-4.md` | +| Unchecked | none | +| Unplanned | none | diff --git a/scripts/__tests__/cli-ci-gate-covers-every-job.test.js b/scripts/__tests__/cli-ci-gate-covers-every-job.test.js index eac6ce08f..d67cee4ab 100644 --- a/scripts/__tests__/cli-ci-gate-covers-every-job.test.js +++ b/scripts/__tests__/cli-ci-gate-covers-every-job.test.js @@ -59,3 +59,83 @@ test("the changes filter names the workflow file itself as relevant", () => { `${ownPath} must be a case of the relevance filter, on its own line` ); }); + +test("only a proven next snapshot tree skips the promotion or main mutation matrix", () => { + const workflow = cliCiWorkflow(); + const changes = workflow.jobs.changes; + const promotion = changes.steps.find((step) => step.id === "promotion"); + const mutation = changes.steps.find((step) => step.id === "mutation"); + + assert.equal(workflow.permissions.actions, "read"); + assert.equal(workflow.permissions.contents, "read"); + assert.equal(workflow.permissions["pull-requests"], undefined); + assert.deepEqual(changes.permissions, { + actions: "read", + contents: "read", + "pull-requests": "read", + }); + assert.equal(changes.outputs.trusted_promotion, "${{ steps.promotion.outputs.trusted }}"); + assert.equal(promotion.name, "Check whether a promotion snapshot or main merge passed next"); + assert.match(promotion.run, /EVENT_NAME.*pull_request/); + assert.match(promotion.run, /BASE_REF.*main/); + assert.equal(promotion.env.HEAD_REPO, "${{ github.event.pull_request.head.repo.full_name }}"); + assert.match(promotion.run, /HEAD_REF.*\^promote\/next-to-main-\[0-9\]\+\$/); + assert.match(promotion.run, /HEAD_REPO" == "\$REPO/); + assert.match(promotion.run, /git merge-base --is-ancestor "\$BASE_SHA" "\$HEAD_SHA"/); + assert.match(promotion.run, /git rev-parse "\$CURRENT_SHA\^\{tree\}"/); + assert.match(promotion.run, /"\$merge_tree" != "\$snapshot_tree"/); + assert.match( + promotion.run, + /actions\/workflows\/cli-ci\.yml\/runs\?branch=next&event=push&status=completed&head_sha=\$HEAD_SHA/ + ); + assert.match(promotion.run, /\.conclusion == "success"/); + assert.match(promotion.run, /\.name == "cli \/ gate" and \.conclusion == "success"/); + assert.match(promotion.run, /reason="could not list successful cli CI push runs"/); + assert.match(promotion.run, /reason="could not inspect cli CI run \$run_id"/); + assert.match(promotion.run, /echo "promotion mutation reuse: \$reason"/); + + // Main can skip mutations only for the exact two-parent promotion merge whose tree is the + // snapshot already gated on next. Any git or associated-PR proof mismatch remains untrusted. + assert.match(promotion.run, /EVENT_NAME" == "push" && "\$GITHUB_REF" == "refs\/heads\/main"/); + assert.match(promotion.run, /git rev-list --parents -n 1 "\$CURRENT_SHA"/); + assert.match(promotion.run, /-z "\$snapshot_sha" \|\| -n "\$\{extra_parent:-\}"/); + assert.match(promotion.run, /git rev-parse "\$snapshot_sha\^\{tree\}"/); + assert.match(promotion.run, /"\$main_tree" != "\$snapshot_tree"/); + assert.match(promotion.run, /\/repos\/\$REPO\/commits\/\$CURRENT_SHA\/pulls/); + assert.match(promotion.run, /\.base\.ref == "main"/); + assert.match(promotion.run, /\.head\.ref \| test\("\^promote\/next-to-main-\[0-9\]\+\$"\)/); + assert.match(promotion.run, /\.merged_at != null/); + assert.match(promotion.run, /"\$base_repo" == "\$REPO"/); + assert.match(promotion.run, /"\$head_repo" == "\$REPO"/); + assert.match(promotion.run, /"\$pr_head_sha" == "\$snapshot_sha"/); + assert.match(promotion.run, /"\$merge_commit_sha" == "\$CURRENT_SHA"/); + assert.match(promotion.run, /"\$matching_prs" -ne 1/); + assert.match(promotion.run, /HEAD_SHA="\$snapshot_sha"/); + + assert.match(mutation.run, /steps\.promotion\.outputs\.trusted.*== "true"/); + assert.match(mutation.run, /scopes='\[\]'/); + assert.match(mutation.run, /else[\s\S]*mutation-scopes-to-run\.mjs/); + assert.equal(workflow.jobs["cli-mutation"].if, "needs.changes.outputs.mutation_scopes != '[]'"); + + // These checks validate the current PR merge ref or main commit; reuse never turns them off. + for (const name of [ + "cli-typecheck", + "cli-lint", + "cli-architecture", + "cli-coverage", + "cli-smoke", + "cli-build", + "cli-knip", + "identifier-join", + "cli-jscpd", + "kanban-checks", + "windows", + ]) { + assert.deepEqual(workflow.jobs[name].needs, ["changes"], `${name} must still depend on changes`); + assert.equal( + workflow.jobs[name].if, + "needs.changes.outputs.relevant == 'true'", + `${name} must still run for a relevant promotion PR` + ); + } +});