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, (