From 6c5336d2bfbe9bdd2f1608079a02f713e0e826d1 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Thu, 30 Jul 2026 10:30:48 -0500 Subject: [PATCH] =?UTF-8?q?ci(gate):=20require=20`CI=20gate`=20again=20?= =?UTF-8?q?=E2=80=94=20it=20is=20the=20only=20thing=20gating=20the=20path-?= =?UTF-8?q?gated=20legs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restored to branch protection 2026-07-30 (13 contexts, verified against the API before and after). This syncs the repo's checked-in claim and every prose statement that moves with the count. WHY IT BELONGS IN THE REQUIRED SET. `CI gate` is a ROLL-UP, not a test: `if: always()` over six legs that are path-gated or nightly -- sqlserver-store, postgres-store, load-test, load-test-sqlserver, windows-service-smoke, changes -- failing if any of them failed or was cancelled. Requiring the roll-up is the ONLY way to gate those six. They cannot be required directly: a path-gated job does not report on a PR that touches none of its paths, which is the required-but-absent trap and would wedge every such PR forever. The roll-up always reports, so it is safe to require -- verified on a real run where all six legs SKIPPED and `CI gate` still returned success. It matters because those six cover exactly what the three `test` legs SKIP: the store suites skip unless a leg supplies the database. While it was off the required set, a PR touching the SQL Server or Postgres store, the load path, or the Windows service could merge with those legs red and nothing on the merge path would notice. WHY THE OBVIOUS OBJECTION DOES NOT HOLD. Checked its record before recommending it: 18 success / 2 failure over 20 runs, and NEITHER failure would have blocked a PR. * run 30518291879 was a `schedule` (nightly) run whose sqlserver load-test legs failed. Not a PR context, so it gates nothing. * run 30510353692 was a superseded re-push where every job was CANCELLED. `CI gate` fails on `cancelled` by design, but branch protection gates on the LATEST head SHA, so a cancelled old run does not block the current head -- ci.yml's concurrency note already records that auto-merge tolerates cancelled contexts on superseded SHAs. The file's stale note (written when it was removed, saying it is NOT in the list) is replaced with the reasoning above, so the next reader learns why it is required rather than just that it is. Same four-file shape as the last sync, and the drift test named every one: the count pin in test_required_contexts.py, the job-collapse pin in test_security_posture.py (13 contexts -> 11 distinct jobs; the three `test` legs share one matrix job), docs/CI.md's bullet list, and the "12 status checks" prose in tests/test_push_guard.py and scripts/hooks/push_guard.py. Verified: the file parses to 13 and is IDENTICAL to what the API returns; 64 tests pass across the drift, posture, push-guard, ledger, backlog and worktree-constraint suites. SEPARATELY, and not fixed here: that nightly failure is real. `load test (smoke, sqlserver)` failed on both 2022 and 2025 against 9b3606ed. With `CI gate` required again, a PR touching the load path will block on it until it is fixed. --- .github/required-contexts.txt | 27 +++++++++++++++++++++------ docs/CI.md | 1 + scripts/hooks/push_guard.py | 2 +- tests/test_push_guard.py | 2 +- tests/test_required_contexts.py | 2 +- tests/test_security_posture.py | 6 +++--- 6 files changed, 28 insertions(+), 12 deletions(-) diff --git a/.github/required-contexts.txt b/.github/required-contexts.txt index 1b424322..3b46f603 100644 --- a/.github/required-contexts.txt +++ b/.github/required-contexts.txt @@ -44,12 +44,27 @@ # it, no `if:` gated on a non-PR event) BEFORE it lands here or in branch protection. # --- ci.yml --------------------------------------------------------------------------------------- -# NOTE: `CI gate` is NOT in this list. It was removed from branch protection on 2026-07-29 and the -# reason is not recorded here -- this file states what the server enforces, not why. It matters, -# because that roll-up is what gated ci.yml's CONDITIONAL/matrix legs (ci.yml, job `ci-gate`, -# `if: always()` over its `needs:`). The three `test` legs below are required in their own right and -# are unaffected; what no longer gates a merge is everything the roll-up covered beyond them. If the -# removal was incidental, restoring it is one POST to the contexts endpoint plus a line here. +# `CI gate` (ci.yml, job `ci-gate`) is a ROLL-UP, not a test. It is `if: always()` over six legs that +# are PATH-GATED or nightly -- sqlserver-store, postgres-store, load-test, load-test-sqlserver, +# windows-service-smoke, changes -- and fails if any of them failed or was cancelled. +# +# Requiring the roll-up is the only way to gate those six. They cannot be required directly: a +# path-gated job does not report on a PR that touches none of its paths, which is the +# required-but-absent trap and would wedge every such PR forever. The roll-up always reports, so it is +# safe to require -- verified on a run where all six legs SKIPPED and `CI gate` still returned success. +# +# It matters because those six cover exactly what the three `test` legs SKIP: the store suites skip +# unless a leg supplies the database. Without the roll-up required, a PR touching the SQL Server or +# Postgres store, the load path, or the Windows service could merge with those legs red and nothing on +# the merge path would notice. +# +# Removed from protection 2026-07-29 (reason unrecorded) and RESTORED 2026-07-30 after checking its +# record: 18 success / 2 failure over 20 runs, and neither failure would have blocked a PR -- +# * one was a `schedule` (nightly) run whose sqlserver load-test legs failed: not a PR context; +# * one was a superseded re-push where every job was CANCELLED. `CI gate` fails on `cancelled` by +# design, but branch protection gates on the LATEST head SHA, so a cancelled old run does not +# block the current head (see ci.yml's concurrency note). +CI gate test (ubuntu-latest, py3.14) test (windows-2022, py3.14) test (windows-2025, py3.14) diff --git a/docs/CI.md b/docs/CI.md index 01f8c228..c1ffe107 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -34,6 +34,7 @@ would otherwise wedge the PR — see the gotcha below). The stable contexts required on `main` are — mirroring [`.github/required-contexts.txt`](../.github/required-contexts.txt), which is the file to edit: +- `CI gate` - `test (ubuntu-latest, py3.14)` - `test (windows-2022, py3.14)` - `test (windows-2025, py3.14)` diff --git a/scripts/hooks/push_guard.py b/scripts/hooks/push_guard.py index 452ad8d9..adbf9193 100644 --- a/scripts/hooks/push_guard.py +++ b/scripts/hooks/push_guard.py @@ -6,7 +6,7 @@ publication, immediately and irreversibly (deleting a ref later does not un-publish content that was fetched, mirrored or indexed in between). -Branch protection on the server requires a PR and 12 status checks, and `strict` is ON (a PR must be +Branch protection on the server requires a PR and 13 status checks, and `strict` is ON (a PR must be up to date with ``main`` to merge). ``enforce_admins`` was enabled 2026-07-28 and DISABLED again on 2026-07-29 via the escape hatch in the HISTORY note below, so ``gh pr merge --admin`` works once more and a direct push to ``main`` is refused only by this hook. That makes it the ONLY guard again for diff --git a/tests/test_push_guard.py b/tests/test_push_guard.py index 0b15a188..f66720a0 100644 --- a/tests/test_push_guard.py +++ b/tests/test_push_guard.py @@ -2,7 +2,7 @@ Since the MEFORORG cutover this repository IS the published artifact — a push to ``main`` is publication, immediately, with no publish step left to catch anything. Server-side branch protection -requires a PR and 12 status checks. ``enforce_admins`` was ON from 2026-07-28 but is OFF again as of +requires a PR and 13 status checks. ``enforce_admins`` was ON from 2026-07-28 but is OFF again as of 2026-07-29 (the documented escape hatch in the hook's own HISTORY note), so the owner CAN bypass. This hook is now defence-in-depth: it fails FAST and LOCALLY with an explanation rather than after a round-trip, and it covers ``cla-signatures``, which branch protection does not. (It was written while ``enforce_admins`` was false and the hook WAS the only guard.) The diff --git a/tests/test_required_contexts.py b/tests/test_required_contexts.py index 94884aeb..4ea8c52b 100644 --- a/tests/test_required_contexts.py +++ b/tests/test_required_contexts.py @@ -98,7 +98,7 @@ def test_the_canonical_file_parses_and_names_the_live_set() -> None: ) # Pinned so that ADDING or REMOVING a required check is a deliberate, reviewed edit here rather # than a silent one. Verified against `gh api repos/MEFORORG/MessageFoundry/branches/main/protection`. - assert len(contexts) == 12, ( + assert len(contexts) == 13, ( f"the canonical required set changed to {len(contexts)} contexts. If branch protection really " "changed, update this count AND every claim this suite checks; if it did not, revert the file." ) diff --git a/tests/test_security_posture.py b/tests/test_security_posture.py index 06f854ee..0dc4e8ee 100644 --- a/tests/test_security_posture.py +++ b/tests/test_security_posture.py @@ -199,7 +199,7 @@ def test_required_jobs_carry_no_continue_on_error() -> None: name = (step or {}).get("name") or (step or {}).get("uses") or "" offenders.append(f"{wf}:{key} — step {name!r} has continue-on-error") # Liveness receipt. NOT `examined == len(required_contexts())`: the three `test (, py3.14)` - # contexts are ONE matrix job, so 12 contexts collapse to 10 jobs. Pinned rather than derived so + # contexts are ONE matrix job, so 13 contexts collapse to 11 jobs. Pinned rather than derived so # that a change in the collapse — a matrix split, or a context that quietly stops resolving — # forces a look here instead of passing on a self-consistent count. # @@ -211,8 +211,8 @@ def test_required_jobs_carry_no_continue_on_error() -> None: f"[security-posture] examined {examined} distinct jobs backing " f"{len(required_contexts())} required contexts" ) - assert examined == 10, ( - f"expected the 12 required contexts to resolve to 10 distinct jobs (the 3 `test` legs share one " + assert examined == 11, ( + f"expected the 13 required contexts to resolve to 11 distinct jobs (the 3 `test` legs share one " f"matrix job); got {examined}. If the workflow layout genuinely changed, update this count." ) assert not offenders, (