Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/ci-minimal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,40 @@ 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
continue-on-error: true
permissions:
contents: read
actions: read
pull-requests: read
steps:
- uses: actions/checkout@v6
continue-on-error: true
timeout-minutes: 1
with:
repository: zackees/ci.yml
ref: b93b8c4a20ce3cd476c75a278ad01a31bde2438b
path: .ci-lint
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 }}
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
Expand Down
64 changes: 63 additions & 1 deletion ci/render_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,68 @@ 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",
)
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:
"""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 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"
f" timeout-minutes: {REUSE_JOB_TIMEOUT}\n"
" continue-on-error: true\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"
f" timeout-minutes: {checkout_t}\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"
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"
" 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"
Expand Down Expand Up @@ -516,7 +578,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"
Expand Down
13 changes: 12 additions & 1 deletion ci/test_fractional_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,20 @@ 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(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.
plan_if = minimal["jobs"]["board_plan"]["if"]
self.assertIn("pull_request", plan_if)
Expand Down
Loading