From 9b4462bfc5e59cd36da8eee5bdbf12760628dbd7 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Wed, 30 Sep 2026 14:46:38 -0700 Subject: [PATCH 1/2] ci: GEN-021 verified-reuse decision on main pushes, shadow mode (zackees/ci.yml#162) Adds a reuse_decision job to the generated ci-minimal.yml that runs `ci-lint reuse-check --mode shadow` on pushes to main and prints in the job summary whether the pushed tree is identical to a PR head whose ci-minimal run passed every required job. Shadow mode always reports reuse=false, nothing needs the job, and every step is continue-on-error, so no job is skipped and the run cannot fail because of it. --- .github/workflows/ci-minimal.yml | 30 ++++++++++++++++++ ci/render_workflows.py | 54 +++++++++++++++++++++++++++++++- ci/test_fractional_workflows.py | 9 +++++- 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-minimal.yml b/.github/workflows/ci-minimal.yml index b8060426..c4020284 100644 --- a/.github/workflows/ci-minimal.yml +++ b/.github/workflows/ci-minimal.yml @@ -92,6 +92,36 @@ jobs: checkout_ref: ${{ github.event.pull_request.head.sha }} fbuild-artifact: fbuild-bin-linux-debug toolchain-cache: ${{ matrix.toolchain_cache }} + reuse_decision: + name: Verified reuse decision (shadow) + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + actions: read + pull-requests: read + steps: + - uses: actions/checkout@v6 + continue-on-error: true + with: + repository: zackees/ci.yml + ref: b93b8c4a20ce3cd476c75a278ad01a31bde2438b + path: .ci-lint + persist-credentials: false + - uses: astral-sh/setup-uv@v3 + continue-on-error: true + - name: Reuse decision (GEN-021, shadow) + continue-on-error: true + working-directory: .ci-lint + env: + GITHUB_TOKEN: ${{ github.token }} + run: >- + uv run --no-project python -m ci_lint reuse-check + --workflow ci-minimal.yml --mode shadow + --required-job "linux / Check (ubuntu-latest)" + --required-job "linux / Python facade tests (ubuntu-latest)" + --required-job "CI selected coverage" test: if: github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ci-test') && !contains(github.event.pull_request.labels.*.name, 'ci-full') uses: ./.github/workflows/ci-test.yml diff --git a/ci/render_workflows.py b/ci/render_workflows.py index d3a3c12f..209ce5a0 100644 --- a/ci/render_workflows.py +++ b/ci/render_workflows.py @@ -306,6 +306,58 @@ def render_pr_boards() -> str: ) +# zackees/ci.yml commit that provides `ci-lint reuse-check` (stdlib-only). +CI_LINT_SHA = "b93b8c4a20ce3cd476c75a278ad01a31bde2438b" +# Job names green on both the default PR tier and main pushes (zackees/ci.yml#162). +REUSE_REQUIRED_JOBS = ( + "linux / Check (ubuntu-latest)", + "linux / Python facade tests (ubuntu-latest)", + "CI selected coverage", +) + + +def render_reuse_decision() -> str: + """GEN-021 verified-reuse decision on main pushes, SHADOW mode only. + + Records in the job summary whether this push's tree is identical to a PR + head whose ci-minimal run passed every REUSE_REQUIRED_JOBS job + (zackees/ci.yml#162). `--mode shadow` always reports reuse=false, nothing + `needs:` this job, and every step is continue-on-error, so it can neither + skip a job nor fail the run. + """ + required = "".join(f" --required-job \"{job}\"\n" for job in REUSE_REQUIRED_JOBS) + return ( + " reuse_decision:\n" + " name: Verified reuse decision (shadow)\n" + " if: github.event_name == 'push' && github.ref == 'refs/heads/main'\n" + " runs-on: ubuntu-latest\n" + " timeout-minutes: 5\n" + " permissions:\n" + " contents: read\n" + " actions: read\n" + " pull-requests: read\n" + " steps:\n" + " - uses: actions/checkout@v6\n" + " continue-on-error: true\n" + " with:\n" + " repository: zackees/ci.yml\n" + f" ref: {CI_LINT_SHA}\n" + " path: .ci-lint\n" + " persist-credentials: false\n" + " - uses: astral-sh/setup-uv@v3\n" + " continue-on-error: true\n" + " - name: Reuse decision (GEN-021, shadow)\n" + " continue-on-error: true\n" + " working-directory: .ci-lint\n" + " env:\n" + " GITHUB_TOKEN: ${{ github.token }}\n" + " run: >-\n" + " uv run --no-project python -m ci_lint reuse-check\n" + " --workflow ci-minimal.yml --mode shadow\n" + + required + ) + + def render_ci(boards: list[dict], tier: str, families: dict) -> str: full = tier == "full" minimal = tier == "minimal" @@ -516,7 +568,7 @@ def render_ci(boards: list[dict], tier: str, families: dict) -> str: + " done\n" + " echo 'complete=true' >> \"$GITHUB_OUTPUT\"\n" ) - + (render_pr_boards() if minimal else "") + + (render_pr_boards() + render_reuse_decision() if minimal else "") + (" test:\n" " if: github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'ci-test') && !contains(github.event.pull_request.labels.*.name, 'ci-full')\n" " uses: ./.github/workflows/ci-test.yml\n" diff --git a/ci/test_fractional_workflows.py b/ci/test_fractional_workflows.py index fba041b0..dd310ca4 100644 --- a/ci/test_fractional_workflows.py +++ b/ci/test_fractional_workflows.py @@ -169,9 +169,16 @@ def test_ordinary_minimal_and_opt_in_test_are_distinct(self): self.assertIn("push", minimal[True]) self.assertIn("pull_request", minimal[True]) self.assertEqual( - {"linux", "board_plan", "fbuild_bin", "pr_boards", "test", "full", "selected-coverage"}, + {"linux", "board_plan", "fbuild_bin", "pr_boards", "reuse_decision", "test", "full", "selected-coverage"}, set(minimal["jobs"]), ) + # GEN-021 shadow decision (zackees/ci.yml#162): main pushes only, never + # consumed by another job, and unable to fail the run. + reuse = minimal["jobs"]["reuse_decision"] + self.assertIn("github.event_name == 'push'", reuse["if"]) + self.assertTrue(all(step.get("continue-on-error") for step in reuse["steps"])) + self.assertIn("--mode shadow", reuse["steps"][-1]["run"]) + self.assertFalse(any("reuse_decision" in str(job.get("needs", "")) for job in minimal["jobs"].values())) # Path-selected boards run only on the default PR tier. plan_if = minimal["jobs"]["board_plan"]["if"] self.assertIn("pull_request", plan_if) From 9b49b520596b23874cfdf5b68600b1df0cdf76e1 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Wed, 30 Sep 2026 14:54:04 -0700 Subject: [PATCH 2/2] ci: make GEN-021 shadow reuse job fail-open against timeouts (CodeRabbit #1604) Add job-level continue-on-error so a job timeout/failure cannot fail the main run, and per-step timeouts (1+1+2 min) below the 5-min job cap so a stall ends as a tolerated step timeout. Test asserts both. --- .github/workflows/ci-minimal.yml | 4 ++++ ci/render_workflows.py | 16 +++++++++++++--- ci/test_fractional_workflows.py | 4 ++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-minimal.yml b/.github/workflows/ci-minimal.yml index c4020284..01c5db6e 100644 --- a/.github/workflows/ci-minimal.yml +++ b/.github/workflows/ci-minimal.yml @@ -97,6 +97,7 @@ jobs: if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 5 + continue-on-error: true permissions: contents: read actions: read @@ -104,6 +105,7 @@ jobs: steps: - uses: actions/checkout@v6 continue-on-error: true + timeout-minutes: 1 with: repository: zackees/ci.yml ref: b93b8c4a20ce3cd476c75a278ad01a31bde2438b @@ -111,8 +113,10 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@v3 continue-on-error: true + timeout-minutes: 1 - name: Reuse decision (GEN-021, shadow) continue-on-error: true + timeout-minutes: 2 working-directory: .ci-lint env: GITHUB_TOKEN: ${{ github.token }} diff --git a/ci/render_workflows.py b/ci/render_workflows.py index 209ce5a0..ea02748b 100644 --- a/ci/render_workflows.py +++ b/ci/render_workflows.py @@ -314,6 +314,9 @@ def render_pr_boards() -> str: "linux / Python facade tests (ubuntu-latest)", "CI selected coverage", ) +REUSE_JOB_TIMEOUT = 5 +# Per-step minutes: checkout, setup-uv, reuse-check. Sum stays below the job cap. +REUSE_STEP_TIMEOUTS = (1, 1, 2) def render_reuse_decision() -> str: @@ -322,16 +325,20 @@ def render_reuse_decision() -> str: Records in the job summary whether this push's tree is identical to a PR head whose ci-minimal run passed every REUSE_REQUIRED_JOBS job (zackees/ci.yml#162). `--mode shadow` always reports reuse=false, nothing - `needs:` this job, and every step is continue-on-error, so it can neither - skip a job nor fail the run. + `needs:` this job, and the job and every step are continue-on-error, so it + can neither skip a job nor fail the run. REUSE_STEP_TIMEOUTS sum below + REUSE_JOB_TIMEOUT, so a stall + ends as a (tolerated) step timeout rather than a job cancellation. """ required = "".join(f" --required-job \"{job}\"\n" for job in REUSE_REQUIRED_JOBS) + checkout_t, uv_t, check_t = REUSE_STEP_TIMEOUTS return ( " reuse_decision:\n" " name: Verified reuse decision (shadow)\n" " if: github.event_name == 'push' && github.ref == 'refs/heads/main'\n" " runs-on: ubuntu-latest\n" - " timeout-minutes: 5\n" + f" timeout-minutes: {REUSE_JOB_TIMEOUT}\n" + " continue-on-error: true\n" " permissions:\n" " contents: read\n" " actions: read\n" @@ -339,6 +346,7 @@ def render_reuse_decision() -> str: " steps:\n" " - uses: actions/checkout@v6\n" " continue-on-error: true\n" + f" timeout-minutes: {checkout_t}\n" " with:\n" " repository: zackees/ci.yml\n" f" ref: {CI_LINT_SHA}\n" @@ -346,8 +354,10 @@ def render_reuse_decision() -> str: " persist-credentials: false\n" " - uses: astral-sh/setup-uv@v3\n" " continue-on-error: true\n" + f" timeout-minutes: {uv_t}\n" " - name: Reuse decision (GEN-021, shadow)\n" " continue-on-error: true\n" + f" timeout-minutes: {check_t}\n" " working-directory: .ci-lint\n" " env:\n" " GITHUB_TOKEN: ${{ github.token }}\n" diff --git a/ci/test_fractional_workflows.py b/ci/test_fractional_workflows.py index dd310ca4..30dd2125 100644 --- a/ci/test_fractional_workflows.py +++ b/ci/test_fractional_workflows.py @@ -176,7 +176,11 @@ def test_ordinary_minimal_and_opt_in_test_are_distinct(self): # consumed by another job, and unable to fail the run. reuse = minimal["jobs"]["reuse_decision"] self.assertIn("github.event_name == 'push'", reuse["if"]) + self.assertTrue(reuse.get("continue-on-error")) self.assertTrue(all(step.get("continue-on-error") for step in reuse["steps"])) + # A stall must end as a tolerated step timeout, never a job cancellation. + step_budget = sum(step["timeout-minutes"] for step in reuse["steps"]) + self.assertLess(step_budget, reuse["timeout-minutes"]) self.assertIn("--mode shadow", reuse["steps"][-1]["run"]) self.assertFalse(any("reuse_decision" in str(job.get("needs", "")) for job in minimal["jobs"].values())) # Path-selected boards run only on the default PR tier.