From 693e23868491862bc92a2c6ac9d4857cdd3bb522 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sun, 11 Oct 2026 17:17:06 -0700 Subject: [PATCH 1/2] feat(ci): open an issue and a revert PR when develop CI goes red (PRD-550) ci-janitor.yml gains revert-red-develop: one issue per failed develop push commit, and a revert PR only for a single-parent head that is still the tip of develop. Contract test red before (1 failed / 207 passed), green after (208/208). Live only once the file reaches main (workflow_run). --- .github/workflows/ci-janitor.yml | 77 ++++++++++++++++++- ...D-550-a-change-merges-within-30-minutes.md | 3 +- scripts/__tests__/ci-structure.spec.ts | 29 +++++++ 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-janitor.yml b/.github/workflows/ci-janitor.yml index cc74e1ce53..f266b0bed1 100644 --- a/.github/workflows/ci-janitor.yml +++ b/.github/workflows/ci-janitor.yml @@ -53,6 +53,81 @@ jobs: gh run rerun "$RUN_ID" --failed --repo "$GITHUB_REPOSITORY" echo "Reran the failed jobs of run $RUN_ID once: every failure matched a known flake." >> "$GITHUB_STEP_SUMMARY" + revert-red-develop: + if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.head_branch == 'develop' && github.event.workflow_run.event == 'push' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + pull-requests: write + issues: write + actions: read + env: + GH_TOKEN: ${{ github.token }} + RUN_ID: ${{ github.event.workflow_run.id }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + RUN_URL: ${{ github.event.workflow_run.html_url }} + steps: + - name: Open an issue once for the failed develop commit + id: issue + shell: bash + run: | + set -euo pipefail + short_sha="${HEAD_SHA:0:7}" + title="develop CI red at $short_sha" + existing="$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "$title in:title" \ + --limit 100 --json title,url | jq -r --arg title "$title" '.[] | select(.title == $title) | .url')" + if [ -n "$existing" ]; then + printf 'Open issue already exists for %s; skipping: %s\n' "$HEAD_SHA" "$existing" >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + failed="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/jobs?filter=latest&per_page=100" --paginate \ + --jq '.jobs[] | select(.conclusion == "failure") | select(.name | test("^(ci-required|golden-path|run-summary)$") | not) | .name')" + body="$(printf 'Qualifying develop CI failed at %s.\n\nRun: %s\n\nFailed primary jobs:\n%s\n' "$HEAD_SHA" "$RUN_URL" "$failed")" + issue_url="$(gh issue create --repo "$GITHUB_REPOSITORY" --title "$title" --body "$body")" + printf 'url=%s\n' "$issue_url" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@v7 + if: steps.issue.outputs.url != '' + with: + ref: ${{ github.event.workflow_run.head_sha }} + fetch-depth: 0 + + - name: Revert only the current single-parent develop commit + if: steps.issue.outputs.url != '' + shell: bash + env: + ISSUE_URL: ${{ steps.issue.outputs.url }} + run: | + set -euo pipefail + git fetch origin develop + if [ "$(git rev-parse origin/develop)" != "$HEAD_SHA" ]; then + printf 'Develop has moved past %s; opened only %s.\n' "$HEAD_SHA" "$ISSUE_URL" >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + read -ra parents <<< "$(git rev-list --parents -n 1 "$HEAD_SHA")" + if [ "${#parents[@]}" -ne 2 ]; then + gh issue comment "$ISSUE_URL" --repo "$GITHUB_REPOSITORY" \ + --body 'Automatic revert skipped: the failed head is not a single-parent non-merge commit.' + exit 0 + fi + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + short_sha="${HEAD_SHA:0:7}" + branch="revert/ci-$short_sha" + subject="$(git log -1 --format=%s "$HEAD_SHA")" + git switch -c "$branch" + if ! git revert --no-edit "$HEAD_SHA"; then + git revert --abort + gh issue comment "$ISSUE_URL" --repo "$GITHUB_REPOSITORY" \ + --body 'Automatic revert skipped: git revert failed or conflicted; no revert PR was opened.' + exit 0 + fi + git push origin "$branch" + body="$(printf 'Reverts the commit that failed develop qualification.\n\nIssue: %s\nRun: %s\n' "$ISSUE_URL" "$RUN_URL")" + gh pr create --repo "$GITHUB_REPOSITORY" --base develop --head "$branch" \ + --title "Revert ${subject:0:72} (develop CI red at $short_sha)" --body "$body" + cancel: # A fork's runs live in another repository, and its runs cannot be cancelled from here. Trying # would put a red check on every fork PR close, so the guard is the whole job. @@ -104,4 +179,4 @@ jobs: printf 'Run %s finished before the cancel; nothing to stop.\n' "$run_id" fi done <<< "$runs" - printf 'Janitor finished for %s: %s run(s) cancelled.\n' "$HEAD_REF" "$cancelled" \ No newline at end of file + printf 'Janitor finished for %s: %s run(s) cancelled.\n' "$HEAD_REF" "$cancelled" diff --git a/docs/PRDs/CI/PRD-550-a-change-merges-within-30-minutes.md b/docs/PRDs/CI/PRD-550-a-change-merges-within-30-minutes.md index a499bf2c45..4b9f07d416 100644 --- a/docs/PRDs/CI/PRD-550-a-change-merges-within-30-minutes.md +++ b/docs/PRDs/CI/PRD-550-a-change-merges-within-30-minutes.md @@ -140,7 +140,8 @@ qualifying run. A PR can still opt in to qualifying on the merge path with a lab **Shared assertion (2026-10-10):** the pull-request base/head parent assertion moved out of `ci-required.mjs` into `scripts/ci-change-scope.mjs` as `assertMergeParentCandidate`, called by `ci-required` in the same place with every condition, error code and error behavior preserved. The shared helper lets an early classifier reject a wrong-parent candidate before the fanout instead of after it. The guard runs only for the live GitHub projection (`--format github`, `pull_request`, not `--local`); local and offline classification keep their existing behavior. Proof: the focused scope, template-selection, efficiency, qualification, structure, needs, runners and dashboard specs pass 431/431 in 8 files (`/tmp/pr476-preflight-focused.log`). Red before green: against the original unguarded classifier, before the guard existed, the new stale-base test failed (1 failed, 53 skipped; the classifier exited 0) (`/tmp/opencode/ci-stale-base-red.log`); the added guard makes it pass. Biome reports 0 errors on the 3 changed code files (4 pre-existing complexity warnings), and `node --check` passes on both JS files. Actual gate note: `pnpm lint` and `pnpm typecheck` passed after rebuilding stale core declarations and running the shipped strata bake; the full local suite remains unqualified; the live CI outcome is pending. The live early-rejection outcome and the phase latency boxes stay open. - [x] `main` (bare/prefixed) and unknown/missing queue targets reject review plans; `develop` bare/prefixed preserves the valid reduced review. proof: `pnpm exec vitest run scripts/__tests__/ci-qualification.spec.ts` 19/19 after fix; new test was red before fix (18 passed, 1 failed). Profile caller/receipt/template selection specs 281/281 and `ci-structure.spec.ts` 159/159. Broader local gates: `pnpm lint` and `pnpm typecheck` passed; `pnpm test` failed (491 s) in 22 unchanged native cases, including an unexplained 120 s dry-run timeout, missing host binaries, and a missing mailbox response. Fresh shared CI passed on candidate `77bb0ee577ba39c514cd1262c48c8291f2210de2`: [run 38033595736, attempt 1](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38033595736), 69 successful jobs including `ci-required`, completed 2026-10-10T07:56:51Z. This single PR run does not close the phase or its repeated merge-group requirements. - [ ] A merge group runs only gating jobs and finishes in ≤ 15 min wall. proof: CI run id. -- [ ] A red qualifying run on `develop` opens an issue and a revert PR. proof: `ci-janitor.yml` run id on a seeded red branch. +**Landing (2026-10-12):** PR 476 squash-merged as `4d40a486e` with the admin bypass at the owner's instruction, so it produced no merge-group run and no queue evidence. The `revert-red-develop` job in `ci-janitor.yml` (issue once per failed `develop` push commit, revert PR only for a single-parent head that is still `develop`'s tip) is committed in the follow-up PR; contract test red before (1 failed / 207 passed) and green after (`ci-structure` + `ci-needs` 208/208). `workflow_run` executes the default-branch copy, so it is live only after `develop → main`; PRs it opens with `GITHUB_TOKEN` start no CI of their own. +- [ ] A red qualifying run on `develop` opens an issue and a revert PR. proof: `ci-janitor.yml` run id on a seeded red branch. Open: needs the job on `main` and a seeded red run. #### Phase 3: Capacity and the latency audit **Status:** IN PROGRESS diff --git a/scripts/__tests__/ci-structure.spec.ts b/scripts/__tests__/ci-structure.spec.ts index 7e5cf38589..6cff69a606 100644 --- a/scripts/__tests__/ci-structure.spec.ts +++ b/scripts/__tests__/ci-structure.spec.ts @@ -1553,6 +1553,35 @@ describe("CI pipeline structure", () => { expect(janitor).toContain("not a known flake; leaving the run red"); }); + it("opens an issue and safely reverts a failed develop push", async () => { + const janitor = await readFile(path.join(repo, ".github/workflows/ci-janitor.yml"), "utf8"); + const revert = jobSections(janitor).find(([name]) => name === "revert-red-develop")?.[1] ?? ""; + expect(revert, "ci-janitor declares no revert-red-develop job").not.toBe(""); + expect(revert).toContain( + "github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.head_branch == 'develop' && github.event.workflow_run.event == 'push'", + ); + for (const permission of [ + "contents: write", + "pull-requests: write", + "issues: write", + "actions: read", + ]) { + expect(revert).toContain(permission); + } + expect(revert).toContain("RUN_ID: ${{ github.event.workflow_run.id }}"); + expect(revert).toContain("HEAD_SHA: ${{ github.event.workflow_run.head_sha }}"); + for (const block of janitor.matchAll(/\x20{8}run: \|\n((?:\x20{10}.*\n|\n)*)/gu)) { + expect(block[1]).not.toContain("${{ github.event.workflow_run"); + } + expect(revert).toContain("gh issue create"); + expect(revert).toContain('git revert --no-edit "$HEAD_SHA"'); + expect(revert).toContain("git rev-parse origin/develop"); + expect(revert).toContain('git rev-list --parents -n 1 "$HEAD_SHA"'); + expect(revert).toContain("gh issue comment"); + expect(revert).toContain("gh pr create"); + expect(revert).toContain("fetch-depth: 0"); + }); + it("cancels a closed pull request's runs without injecting its head ref into a shell", async () => { const janitor = await readFile(path.join(repo, ".github/workflows/ci-janitor.yml"), "utf8"); // Merged or closed, nothing the head ref is still doing can change the outcome, and those From 7b45cbce75e5806d4379cf6abd809d902841e132 Mon Sep 17 00:00:00 2001 From: Joao Paulo Furtado Date: Sun, 11 Oct 2026 17:46:09 -0700 Subject: [PATCH 2/2] docs(prd-550): drop the auto-revert janitor job, record the decision The owner reads a red qualifying develop run and fixes forward. ci-janitor.yml and the structure spec are unchanged from develop. --- .github/workflows/ci-janitor.yml | 77 +------------------ ...D-550-a-change-merges-within-30-minutes.md | 21 ++--- scripts/__tests__/ci-structure.spec.ts | 29 ------- 3 files changed, 13 insertions(+), 114 deletions(-) diff --git a/.github/workflows/ci-janitor.yml b/.github/workflows/ci-janitor.yml index f266b0bed1..cc74e1ce53 100644 --- a/.github/workflows/ci-janitor.yml +++ b/.github/workflows/ci-janitor.yml @@ -53,81 +53,6 @@ jobs: gh run rerun "$RUN_ID" --failed --repo "$GITHUB_REPOSITORY" echo "Reran the failed jobs of run $RUN_ID once: every failure matched a known flake." >> "$GITHUB_STEP_SUMMARY" - revert-red-develop: - if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.head_branch == 'develop' && github.event.workflow_run.event == 'push' - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: write - pull-requests: write - issues: write - actions: read - env: - GH_TOKEN: ${{ github.token }} - RUN_ID: ${{ github.event.workflow_run.id }} - HEAD_SHA: ${{ github.event.workflow_run.head_sha }} - RUN_URL: ${{ github.event.workflow_run.html_url }} - steps: - - name: Open an issue once for the failed develop commit - id: issue - shell: bash - run: | - set -euo pipefail - short_sha="${HEAD_SHA:0:7}" - title="develop CI red at $short_sha" - existing="$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "$title in:title" \ - --limit 100 --json title,url | jq -r --arg title "$title" '.[] | select(.title == $title) | .url')" - if [ -n "$existing" ]; then - printf 'Open issue already exists for %s; skipping: %s\n' "$HEAD_SHA" "$existing" >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - failed="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/jobs?filter=latest&per_page=100" --paginate \ - --jq '.jobs[] | select(.conclusion == "failure") | select(.name | test("^(ci-required|golden-path|run-summary)$") | not) | .name')" - body="$(printf 'Qualifying develop CI failed at %s.\n\nRun: %s\n\nFailed primary jobs:\n%s\n' "$HEAD_SHA" "$RUN_URL" "$failed")" - issue_url="$(gh issue create --repo "$GITHUB_REPOSITORY" --title "$title" --body "$body")" - printf 'url=%s\n' "$issue_url" >> "$GITHUB_OUTPUT" - - - uses: actions/checkout@v7 - if: steps.issue.outputs.url != '' - with: - ref: ${{ github.event.workflow_run.head_sha }} - fetch-depth: 0 - - - name: Revert only the current single-parent develop commit - if: steps.issue.outputs.url != '' - shell: bash - env: - ISSUE_URL: ${{ steps.issue.outputs.url }} - run: | - set -euo pipefail - git fetch origin develop - if [ "$(git rev-parse origin/develop)" != "$HEAD_SHA" ]; then - printf 'Develop has moved past %s; opened only %s.\n' "$HEAD_SHA" "$ISSUE_URL" >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - read -ra parents <<< "$(git rev-list --parents -n 1 "$HEAD_SHA")" - if [ "${#parents[@]}" -ne 2 ]; then - gh issue comment "$ISSUE_URL" --repo "$GITHUB_REPOSITORY" \ - --body 'Automatic revert skipped: the failed head is not a single-parent non-merge commit.' - exit 0 - fi - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - short_sha="${HEAD_SHA:0:7}" - branch="revert/ci-$short_sha" - subject="$(git log -1 --format=%s "$HEAD_SHA")" - git switch -c "$branch" - if ! git revert --no-edit "$HEAD_SHA"; then - git revert --abort - gh issue comment "$ISSUE_URL" --repo "$GITHUB_REPOSITORY" \ - --body 'Automatic revert skipped: git revert failed or conflicted; no revert PR was opened.' - exit 0 - fi - git push origin "$branch" - body="$(printf 'Reverts the commit that failed develop qualification.\n\nIssue: %s\nRun: %s\n' "$ISSUE_URL" "$RUN_URL")" - gh pr create --repo "$GITHUB_REPOSITORY" --base develop --head "$branch" \ - --title "Revert ${subject:0:72} (develop CI red at $short_sha)" --body "$body" - cancel: # A fork's runs live in another repository, and its runs cannot be cancelled from here. Trying # would put a red check on every fork PR close, so the guard is the whole job. @@ -179,4 +104,4 @@ jobs: printf 'Run %s finished before the cancel; nothing to stop.\n' "$run_id" fi done <<< "$runs" - printf 'Janitor finished for %s: %s run(s) cancelled.\n' "$HEAD_REF" "$cancelled" + printf 'Janitor finished for %s: %s run(s) cancelled.\n' "$HEAD_REF" "$cancelled" \ No newline at end of file diff --git a/docs/PRDs/CI/PRD-550-a-change-merges-within-30-minutes.md b/docs/PRDs/CI/PRD-550-a-change-merges-within-30-minutes.md index 4b9f07d416..3b949ac503 100644 --- a/docs/PRDs/CI/PRD-550-a-change-merges-within-30-minutes.md +++ b/docs/PRDs/CI/PRD-550-a-change-merges-within-30-minutes.md @@ -71,7 +71,7 @@ flowchart LR R -- no --> G["gating board
≤ 15 min"] G --> M M --> D["develop push:
native matrix, all templates,
perf lanes (post-merge)"] - D -- red --> I["issue + revert PR
(ci-janitor)"] + D -- red --> I["owner reads the red run
and fixes forward"] D --> N["develop → main:
full qualification (unchanged)"] ``` @@ -85,8 +85,8 @@ repeated `test-unit` red from the window as code or load; fix the load reds at t **Phase 2 — one gating board, run once.** Split the board into **gating** (the core above plus the affected template and Linux native contract legs that `scope` selects) and **qualifying** (native platform legs, all 13 templates, perf integration lanes). Merge groups run only gating. Develop pushes, -the nightly run and `develop → main` run qualifying. A red qualifying run on `develop` opens an issue -and a revert PR through the existing `ci-janitor.yml`. Prove `test-unit` shard expansion so it leaves +the nightly run and `develop → main` run qualifying. A red qualifying run on `develop` is read and fixed forward by the owner +(see Decisions). Prove `test-unit` shard expansion so it leaves `UNPROVEN_REUSE_BOARDS`; then an exact-tree merge group reuses the PR verdict (PRD-481's mechanism). **Phase 3 — capacity for the gating board.** The gating board needs about 10 free slots for 15 min. @@ -95,7 +95,7 @@ qualifying legs. Add a small latency audit (`scripts/ci-merge-latency.mjs`) that PRs in a window: last push → merge, enqueue → merge, attempts, and first failing job. It replaces the ad-hoc `gh` queries used for this Context. -Risks: a qualifying red lands on `develop` before it is seen. The revert PR bounds that window to one +Risks: a qualifying red lands on `develop` before it is seen. Fix-forward bounds that window to one qualifying run. A PR can still opt in to qualifying on the merge path with a label for native-only changes. ## Acceptance Criteria @@ -115,7 +115,7 @@ qualifying run. A PR can still opt in to qualifying on the merge path with a lab |---|---|---|---| | Gating board on merge groups | `merge_group` event → `.github/workflows/ci.yml` → `scope` job → `scripts/ci-change-scope.mjs` | Replaces the exhaustive merge-group board | AC-1, AC-2 | | Exact-tree reuse in the queue | `scope` job → `currentRun`/`coverageMiss` (`scripts/ci-change-scope.mjs:584`) | `test-unit` leaves `UNPROVEN_REUSE_BOARDS` | AC-3 | -| Post-merge qualification | `push` to `develop`, `schedule` → `ci.yml`; red → `ci-janitor.yml` | Takes the legs removed from merge groups | Phase 2 box | +| Post-merge qualification | `push` to `develop`, `schedule` → `ci.yml` | Takes the legs removed from merge groups | Phase 2 box | | Merge latency audit | `node scripts/ci-merge-latency.mjs` | Replaces ad-hoc `gh` queries | AC-1, AC-2 | ## Execution Phases @@ -134,14 +134,13 @@ qualifying run. A PR can still opt in to qualifying on the merge path with a lab #### Phase 2: One gating board, run once **Status:** IN PROGRESS -**Files:** `.github/workflows/ci.yml`, `scripts/ci-change-scope.mjs`, `scripts/ci-required.mjs`, `.github/workflows/ci-janitor.yml`, root `AGENTS.md` (+ `pnpm sync:agents`), `scripts/__tests__/ci-structure.spec.ts`. -**Implementation:** Add a `gating` scope for `merge_group`; keep `full` for develop push, schedule, `workflow_dispatch` and `main`. Prove `test-unit` shard expansion from the run's job list and drop it from `UNPROVEN_REUSE_BOARDS`. Extend `ci-janitor.yml` to open an issue and a revert PR on a red qualifying `develop` run. Committed so far: a `merge_group` on develop selects the same narrowed plan a pull request earns (it reviews the exact tree a merge would produce) and a develop push selects `full`, so the exhaustive board moved from the queue to the promotion. Both outcome boxes below stay open until a CI run proves them. +**Files:** `.github/workflows/ci.yml`, `scripts/ci-change-scope.mjs`, `scripts/ci-required.mjs`, root `AGENTS.md` (+ `pnpm sync:agents`), `scripts/__tests__/ci-structure.spec.ts`. +**Implementation:** Add a `gating` scope for `merge_group`; keep `full` for develop push, schedule, `workflow_dispatch` and `main`. Prove `test-unit` shard expansion from the run's job list and drop it from `UNPROVEN_REUSE_BOARDS`. Committed so far: a `merge_group` on develop selects the same narrowed plan a pull request earns (it reviews the exact tree a merge would produce) and a develop push selects `full`, so the exhaustive board moved from the queue to the promotion. Both outcome boxes below stay open until a CI run proves them. **Repair (2026-10-09):** `validationProfile` compared a raw `baseRef` against `main` and then ORed any `merge_group` to `develop`, so an under-scoped review plan was accepted for `refs/heads/main` and for unknown or missing queue targets. The shared fix normalizes `refs/heads/` once inside `validationProfile` and classifies only explicit `main`/`develop`, removing the blanket `eventName === "merge_group"` fallback so an unproven target stays `other`. Reproduction: the committed regression test in `scripts/__tests__/ci-qualification.spec.ts`. The gate callers (`validateEventPlan`, `sourceVerdict`, `currentRun`) keep explicit develop/main behavior. **Shared assertion (2026-10-10):** the pull-request base/head parent assertion moved out of `ci-required.mjs` into `scripts/ci-change-scope.mjs` as `assertMergeParentCandidate`, called by `ci-required` in the same place with every condition, error code and error behavior preserved. The shared helper lets an early classifier reject a wrong-parent candidate before the fanout instead of after it. The guard runs only for the live GitHub projection (`--format github`, `pull_request`, not `--local`); local and offline classification keep their existing behavior. Proof: the focused scope, template-selection, efficiency, qualification, structure, needs, runners and dashboard specs pass 431/431 in 8 files (`/tmp/pr476-preflight-focused.log`). Red before green: against the original unguarded classifier, before the guard existed, the new stale-base test failed (1 failed, 53 skipped; the classifier exited 0) (`/tmp/opencode/ci-stale-base-red.log`); the added guard makes it pass. Biome reports 0 errors on the 3 changed code files (4 pre-existing complexity warnings), and `node --check` passes on both JS files. Actual gate note: `pnpm lint` and `pnpm typecheck` passed after rebuilding stale core declarations and running the shipped strata bake; the full local suite remains unqualified; the live CI outcome is pending. The live early-rejection outcome and the phase latency boxes stay open. - [x] `main` (bare/prefixed) and unknown/missing queue targets reject review plans; `develop` bare/prefixed preserves the valid reduced review. proof: `pnpm exec vitest run scripts/__tests__/ci-qualification.spec.ts` 19/19 after fix; new test was red before fix (18 passed, 1 failed). Profile caller/receipt/template selection specs 281/281 and `ci-structure.spec.ts` 159/159. Broader local gates: `pnpm lint` and `pnpm typecheck` passed; `pnpm test` failed (491 s) in 22 unchanged native cases, including an unexplained 120 s dry-run timeout, missing host binaries, and a missing mailbox response. Fresh shared CI passed on candidate `77bb0ee577ba39c514cd1262c48c8291f2210de2`: [run 38033595736, attempt 1](https://github.com/ThreeNativeHQ/threenative-engine/actions/runs/38033595736), 69 successful jobs including `ci-required`, completed 2026-10-10T07:56:51Z. This single PR run does not close the phase or its repeated merge-group requirements. - [ ] A merge group runs only gating jobs and finishes in ≤ 15 min wall. proof: CI run id. -**Landing (2026-10-12):** PR 476 squash-merged as `4d40a486e` with the admin bypass at the owner's instruction, so it produced no merge-group run and no queue evidence. The `revert-red-develop` job in `ci-janitor.yml` (issue once per failed `develop` push commit, revert PR only for a single-parent head that is still `develop`'s tip) is committed in the follow-up PR; contract test red before (1 failed / 207 passed) and green after (`ci-structure` + `ci-needs` 208/208). `workflow_run` executes the default-branch copy, so it is live only after `develop → main`; PRs it opens with `GITHUB_TOKEN` start no CI of their own. -- [ ] A red qualifying run on `develop` opens an issue and a revert PR. proof: `ci-janitor.yml` run id on a seeded red branch. Open: needs the job on `main` and a seeded red run. +**Landing (2026-10-12):** PR 476 squash-merged as `4d40a486e` with the admin bypass at the owner's instruction, so it produced no merge-group run and no queue evidence. #### Phase 3: Capacity and the latency audit **Status:** IN PROGRESS @@ -157,3 +156,7 @@ qualifying run. A PR can still opt in to qualifying on the merge path with a lab - [x] `release-reports` joins the light lane while native build and parity legs keep their heavy routing. proof: `pnpm exec vitest run scripts/__tests__/ci-structure.spec.ts` — contract red before the one-line workflow change (`release-reports is off the light lane`, 1 failed / 159 passed) and green after (160 passed); live queue-wait effect unverified. **Placement (2026-10-10, collector coverage):** Live run `38069329212` showed the native `performance-coverage` job still queued on `tn-local` at 17:29:25 for at least 7 min while all five heavy slots were busy and three light slots idle, though it only checks out, `pnpm install`s, downloads the `native-*` artifacts and runs `tsx scripts/performance-regression/ci-summary.ts` — a JSON aggregator with no compile or GPU leg. It now selects `TN_RUNNER_LIGHT`; its condition, timeout, steps, receipts, coverage list and every native/parity build label are unchanged, and no variable, input or helper is added. Test-first: the exact routing contract was red before the one-line change (`performance-coverage is off the light lane`, 1 failed / 159 skipped, `/tmp/pr476-collector-route-red.log`) and the five focused CI specs run 331/331 after (`/tmp/pr476-collector-route-green.log`); `biome check scripts/__tests__/ci-structure.spec.ts` exit 0 (pre-existing complexity warnings only) and `git diff --check` clean. New-CI outcome pending, so no box is closed. - [ ] Gating jobs in a merge group wait ≤ 3 min p90 for a runner. proof: `node scripts/ci-merge-latency.mjs` queue column. + +## Decisions + +- 2026-10-12, João: no auto-revert job. A red qualifying `develop` run is read and fixed forward by the owner. Reason: most reds in the window were load reds, so a bot would revert good commits, and a `GITHUB_TOKEN` revert PR starts no CI. The phase 2 box and the `ci-janitor.yml` change were removed; `ci-janitor.yml` is unchanged. diff --git a/scripts/__tests__/ci-structure.spec.ts b/scripts/__tests__/ci-structure.spec.ts index 6cff69a606..7e5cf38589 100644 --- a/scripts/__tests__/ci-structure.spec.ts +++ b/scripts/__tests__/ci-structure.spec.ts @@ -1553,35 +1553,6 @@ describe("CI pipeline structure", () => { expect(janitor).toContain("not a known flake; leaving the run red"); }); - it("opens an issue and safely reverts a failed develop push", async () => { - const janitor = await readFile(path.join(repo, ".github/workflows/ci-janitor.yml"), "utf8"); - const revert = jobSections(janitor).find(([name]) => name === "revert-red-develop")?.[1] ?? ""; - expect(revert, "ci-janitor declares no revert-red-develop job").not.toBe(""); - expect(revert).toContain( - "github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.head_branch == 'develop' && github.event.workflow_run.event == 'push'", - ); - for (const permission of [ - "contents: write", - "pull-requests: write", - "issues: write", - "actions: read", - ]) { - expect(revert).toContain(permission); - } - expect(revert).toContain("RUN_ID: ${{ github.event.workflow_run.id }}"); - expect(revert).toContain("HEAD_SHA: ${{ github.event.workflow_run.head_sha }}"); - for (const block of janitor.matchAll(/\x20{8}run: \|\n((?:\x20{10}.*\n|\n)*)/gu)) { - expect(block[1]).not.toContain("${{ github.event.workflow_run"); - } - expect(revert).toContain("gh issue create"); - expect(revert).toContain('git revert --no-edit "$HEAD_SHA"'); - expect(revert).toContain("git rev-parse origin/develop"); - expect(revert).toContain('git rev-list --parents -n 1 "$HEAD_SHA"'); - expect(revert).toContain("gh issue comment"); - expect(revert).toContain("gh pr create"); - expect(revert).toContain("fetch-depth: 0"); - }); - it("cancels a closed pull request's runs without injecting its head ref into a shell", async () => { const janitor = await readFile(path.join(repo, ".github/workflows/ci-janitor.yml"), "utf8"); // Merged or closed, nothing the head ref is still doing can change the outcome, and those