diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f652763d4..3d462bb097 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2184,6 +2184,13 @@ jobs: # also absent from branch protection (dev requires exactly "CI Summary"), # so those checks previously blocked nothing. EVENT_NAME: ${{ github.event_name }} + # OMN-15532 — PR author login. Drops ONLY the contexts that this + # author's PRs structurally cannot produce (ACTOR_CONDITIONAL_CONTEXTS: + # cr-thread-gate-caller.yml skips dependabot[bot], so + # "gate / CodeRabbit Thread Check" is never created on its PRs and + # would otherwise burn the full deadline then fail closed). Empty on + # non-PR events, which drops nothing. + PR_AUTHOR: ${{ github.event.pull_request.user.login }} HEAD_SHA: >- ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha @@ -2233,7 +2240,8 @@ jobs: fi fi python3 scripts/ci/ci_summary_gate.py --jobs-file jobs.json --run-attempt "${RUN_ATTEMPT}" \ - --check-runs-file check_runs.json --event-name "${EVENT_NAME}" + --check-runs-file check_runs.json --event-name "${EVENT_NAME}" \ + --pr-author "${PR_AUTHOR:-}" rc=$? case "${rc}" in 0) echo "::notice::CI Summary = SUCCESS (all gating jobs and asserted external contexts provably passed)"; exit 0 ;; diff --git a/scripts/ci/ci_summary_gate.py b/scripts/ci/ci_summary_gate.py index 0158668699..19ae395aa7 100644 --- a/scripts/ci/ci_summary_gate.py +++ b/scripts/ci/ci_summary_gate.py @@ -247,6 +247,44 @@ ), } +# OMN-15532 — contexts whose PRODUCER structurally does not report for a given +# PR author, so "absent" carries no information and must not burn the poll +# deadline. This is an *applicability* rule, not a bypass: the context stays +# fail-closed for every author not named here. +# +# ADMISSION RULE — an entry is justified only by a producer-side condition that +# makes the check-run impossible to create, quoted with the workflow file and +# the live readback that shows it absent. "It was red and I wanted it green" is +# never a reason. Keys must be members of EXPECTED_EXTERNAL_CONTEXTS and actors +# must be concrete logins (no wildcards) — both pinned by tests. +ACTOR_CONDITIONAL_CONTEXTS: dict[str, tuple[str, ...]] = { + # .github/workflows/cr-thread-gate-caller.yml gates the `gate` job on + # `(github.event_name == 'pull_request' && github.actor != 'dependabot[bot]')`. + # The context name is the `caller-job / reusable-job` form, so when the + # caller job is skipped the reusable's inner job never materialises and NO + # check-run is created — the context is absent, not `skipped`. + # + # Live readback 2026-07-30, infra dev Dependabot batch: + # #2522 2cdf352d actor=dependabot[bot] CR-gate run conclusion=skipped -> ABSENT + # #2521 841c292f actor=dependabot[bot] CR-gate run conclusion=skipped -> ABSENT + # #2520 feb6627b actor=jonahgabriel -> present, success + # #2519 08e356cc actor=jonahgabriel -> present, success + # #2518 e2d38605 actor=jonahgabriel -> present, success + # All five are `pull_request`, run_attempt=1: the actor is the discriminator. + # + # The OMN-15496 seed measured this context 16/16 present over #2546…#2567 — + # a window containing NO Dependabot PR, which is exactly how a 16/16 context + # can still be absent in production. + # + # Fixed consumer-side, not producer-side: OMN-10276 removed this actor skip + # on omnimemory, but omnimemory calls a LOCAL reusable and passes no secrets, + # whereas this caller invokes omniclaude's reusable with `secrets: + # CROSS_REPO_PAT`. Dependabot `pull_request` runs do not receive regular repo + # secrets, so dropping the skip here risks trading an absent-wedge for a + # red-wedge. See OMN-15532. + "gate / CodeRabbit Thread Check": ("dependabot[bot]",), +} + # Conclusions that count as "provably passed". GOOD_CONCLUSIONS: frozenset[str] = frozenset({"success", "skipped"}) @@ -373,6 +411,26 @@ def latest_check_run_by_name( return latest +def applicable_external_contexts( + expected: tuple[str, ...], + pr_author: str | None, +) -> tuple[str, ...]: + """Drop contexts whose producer cannot report for ``pr_author`` (OMN-15532). + + Order preserved. An unknown/empty ``pr_author`` drops NOTHING — the fail- + closed default — so a missing ``--pr-author`` argument enforces the full set + rather than silently exempting it. + """ + + if not pr_author: + return expected + return tuple( + context + for context in expected + if pr_author not in ACTOR_CONDITIONAL_CONTEXTS.get(context, ()) + ) + + def evaluate_external_contexts( check_runs: list[dict[str, object]] | None, expected: tuple[str, ...], @@ -423,6 +481,7 @@ def evaluate( allowlist: frozenset[str] = SOFT_ALLOWLIST, check_runs: list[dict[str, object]] | None = None, external_contexts: tuple[str, ...] = (), + pr_author: str | None = None, ) -> tuple[int, str]: """Return ``(exit_code, human_report)`` for the current job snapshot. @@ -431,8 +490,12 @@ def evaluate( exists — are not wedged. The CLI supplies :data:`EXPECTED_EXTERNAL_CONTEXTS` and its ``--event-name`` defaults to ``pull_request``, so a *forgotten* argument enforces rather than skips. + + ``pr_author`` drops only the contexts that :data:`ACTOR_CONDITIONAL_CONTEXTS` + marks unreportable for that author (OMN-15532). ``None`` drops nothing. """ + external_contexts = applicable_external_contexts(external_contexts, pr_author) latest = dedup_latest(jobs, run_attempt=run_attempt) gate_names = frozenset(strict_gates) | frozenset(skippable_gates) @@ -637,6 +700,14 @@ def main(argv: list[str] | None = None) -> int: "context set. Defaults to 'pull_request' so a FORGOTTEN argument " "enforces rather than silently skips.", ) + parser.add_argument( + "--pr-author", + default=None, + help="Login of the PR author. Drops ONLY the ACTOR_CONDITIONAL_CONTEXTS " + "entries that this author's PRs structurally cannot produce (OMN-15532). " + "Omitted/empty drops nothing, so a forgotten argument enforces the full " + "set rather than exempting it.", + ) args = parser.parse_args(argv) jobs = _load_jobs(args.jobs_file) @@ -648,6 +719,7 @@ def main(argv: list[str] | None = None) -> int: run_attempt=args.run_attempt, check_runs=_load_check_runs(args.check_runs_file), external_contexts=external_contexts, + pr_author=args.pr_author, ) print(report) if args.report_only: diff --git a/tests/ci/fixtures/omn15532_dependabot_pr2522_check_runs.json b/tests/ci/fixtures/omn15532_dependabot_pr2522_check_runs.json new file mode 100644 index 0000000000..dc95861361 --- /dev/null +++ b/tests/ci/fixtures/omn15532_dependabot_pr2522_check_runs.json @@ -0,0 +1,694 @@ +{ + "_source": "live gh api repos/OmniNode-ai/omnibase_infra/commits/2cdf352d5577ac2e533da11ebdf38941697fb7ad/check-runs, captured 2026-07-30 for OMN-15532; PR #2522, author dependabot[bot]", + "head_sha": "2cdf352d5577ac2e533da11ebdf38941697fb7ad", + "pr": 2522, + "pr_author": "dependabot[bot]", + "check_runs": [ + { + "conclusion": "skipped", + "id": 90196373842, + "name": "auto-tag", + "started_at": "2026-07-28T06:20:39Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90196373499, + "name": "Publish PR Merged Event", + "started_at": "2026-07-28T06:20:41Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90196373180, + "name": "Trigger node_redeploy Start", + "started_at": "2026-07-28T06:20:41Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90196373143, + "name": "TODO Audit", + "started_at": "2026-07-28T06:20:40Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90196373123, + "name": "Publish PR Webhook Event", + "started_at": "2026-07-28T06:20:41Z", + "status": "completed" + }, + { + "conclusion": "skipped", + "id": 90195249939, + "name": "Runtime Boot Smoke (compose)", + "started_at": "2026-07-28T06:13:45Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90195170999, + "name": "CI Tests Gate", + "started_at": "2026-07-28T06:13:42Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90195170992, + "name": "Test-Failure Ratchet Gate", + "started_at": "2026-07-28T06:13:30Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90194904062, + "name": "Tests (Split 1/1)", + "started_at": "2026-07-28T06:11:49Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90194655992, + "name": "Detect Changes", + "started_at": "2026-07-28T06:11:09Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193931522, + "name": "Hostile Review Gate", + "started_at": "2026-07-28T06:06:07Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193907946, + "name": "Integration Silent-Skip Guard (OMN-14172)", + "started_at": "2026-07-28T06:10:57Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193907771, + "name": "Migration Integration Test", + "started_at": "2026-07-28T06:10:48Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193617388, + "name": "Omni Standards Gate", + "started_at": "2026-07-28T06:05:31Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193587246, + "name": "reason-graph", + "started_at": "2026-07-28T06:05:24Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193560221, + "name": "Version Pin Compliance", + "started_at": "2026-07-28T06:10:48Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193560005, + "name": "Kafka Boundary Compat (OMN-3256)", + "started_at": "2026-07-28T06:10:08Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559971, + "name": "zone-filter / Zone Filter (docs-only check)", + "started_at": "2026-07-28T06:05:17Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559964, + "name": "Cross-Repo Migration Conflicts", + "started_at": "2026-07-28T06:10:10Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559953, + "name": "Topic Enum Drift Check", + "started_at": "2026-07-28T06:08:27Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559919, + "name": "Arch Invariants (OMN-3343)", + "started_at": "2026-07-28T06:05:30Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559891, + "name": "OCC Born-Path Trigger Coverage (OMN-14987)", + "started_at": "2026-07-28T06:06:25Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559873, + "name": "Contract Sync Gate (Wave C) [OMN-8915]", + "started_at": "2026-07-28T06:07:32Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559866, + "name": "Contract Compliance Check", + "started_at": "2026-07-28T06:08:58Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559863, + "name": "AI-Slop Pattern Check (strict, PR diff)", + "started_at": "2026-07-28T06:07:30Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559862, + "name": "Topic Drift Check", + "started_at": "2026-07-28T06:07:15Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559861, + "name": "Plugin ENV->Service Completeness (OMN-4313)", + "started_at": "2026-07-28T06:08:42Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559855, + "name": "Contract Path Pre-Flight", + "started_at": "2026-07-28T06:06:25Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559854, + "name": "Contract Compliance", + "started_at": "2026-07-28T06:07:37Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559849, + "name": "Topic Naming Lint", + "started_at": "2026-07-28T06:07:26Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559846, + "name": "Compose Required-Env Coverage (OMN-5439)", + "started_at": "2026-07-28T06:07:15Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559843, + "name": "Kafka Schema Handshake (OMN-3411)", + "started_at": "2026-07-28T06:08:37Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559841, + "name": "Demo Loop Gate", + "started_at": "2026-07-28T06:06:49Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559831, + "name": "Writer-Migration Coupling Check", + "started_at": "2026-07-28T06:06:55Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559815, + "name": "Migration Freeze Check", + "started_at": "2026-07-28T06:05:48Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559693, + "name": "Lint", + "started_at": "2026-07-28T06:06:10Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559681, + "name": "Effect-Assertion Gate (RT-5)", + "started_at": "2026-07-28T06:05:52Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559673, + "name": "Infra Node Handler Ownership", + "started_at": "2026-07-28T06:05:35Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559649, + "name": "ONEX Validators", + "started_at": "2026-07-28T06:05:29Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193559635, + "name": "Fingerprint Check", + "started_at": "2026-07-28T06:04:50Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193547415, + "name": "Hostile Reviewer (adversarial gate)", + "started_at": "2026-07-28T06:04:33Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90193452529, + "name": "call-reject-skip-token / scan / reject-skip-gate-token", + "started_at": "2026-07-28T06:05:14Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192737115, + "name": "call-reject-skip-token / occ-preflight / eligibility", + "started_at": "2026-07-28T06:02:34Z", + "status": "completed" + }, + { + "conclusion": "skipped", + "id": 90192736937, + "name": "occ-autobind-manual-replay", + "started_at": "2026-07-28T05:58:28Z", + "status": "completed" + }, + { + "conclusion": "skipped", + "id": 90192736644, + "name": "occ-autobind", + "started_at": "2026-07-28T05:58:28Z", + "status": "completed" + }, + { + "conclusion": "skipped", + "id": 90192736600, + "name": "occ-companion-effect", + "started_at": "2026-07-28T05:58:28Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192736470, + "name": "call / validate-docs", + "started_at": "2026-07-28T06:02:54Z", + "status": "completed" + }, + { + "conclusion": "skipped", + "id": 90192736416, + "name": "occ-companion-effect-manual-replay", + "started_at": "2026-07-28T05:58:28Z", + "status": "completed" + }, + { + "conclusion": "skipped", + "id": 90192736346, + "name": "gate", + "started_at": "2026-07-28T05:58:28Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192736344, + "name": "occ-preflight / eligibility", + "started_at": "2026-07-28T06:03:16Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192736335, + "name": "no-noncanonical-lifecycle-classes", + "started_at": "2026-07-28T06:03:32Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192736334, + "name": "CI Summary", + "started_at": "2026-07-28T06:03:26Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192736296, + "name": "Runtime Profiles / validate", + "started_at": "2026-07-28T06:03:18Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192736292, + "name": "Enforce validator-requirements.yaml (OMN-13291)", + "started_at": "2026-07-28T06:03:24Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192736273, + "name": "OCC Companion Merged Gate (OMN-15214)", + "started_at": "2026-07-28T06:02:49Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192736194, + "name": "deploy-gate / deploy-gate", + "started_at": "2026-07-28T06:03:37Z", + "status": "completed" + }, + { + "conclusion": "skipped", + "id": 90192736166, + "name": "verify", + "started_at": "2026-07-28T05:58:28Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192736165, + "name": "pr-title / check-title", + "started_at": "2026-07-28T06:02:09Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192736082, + "name": "imperative-contract-guard / Imperative Contract Guard", + "started_at": "2026-07-28T06:02:21Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192736072, + "name": "verify / verify", + "started_at": "2026-07-28T06:03:08Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735916, + "name": "required-check-skip-guard / check-skip-vectors", + "started_at": "2026-07-28T06:03:27Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735849, + "name": "occ-preflight / eligibility", + "started_at": "2026-07-28T06:03:10Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735577, + "name": "pinned-wheel-skew", + "started_at": "2026-07-28T06:02:48Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735470, + "name": "dispatcher-route-coverage", + "started_at": "2026-07-28T06:03:21Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735431, + "name": "URL Authority Gate", + "started_at": "2026-07-28T06:03:13Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735417, + "name": "Integration Test Coverage", + "started_at": "2026-07-28T06:02:38Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735383, + "name": "non-dev-base-guard", + "started_at": "2026-07-28T06:02:15Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735355, + "name": "Stale TODO Gate", + "started_at": "2026-07-28T06:02:10Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735347, + "name": "main-target-guard", + "started_at": "2026-07-28T06:02:13Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735343, + "name": "Integration Test Removal Gate", + "started_at": "2026-07-28T06:02:03Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735311, + "name": "sibling-lock-pins", + "started_at": "2026-07-28T06:02:46Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735310, + "name": "Runner Disk Preflight", + "started_at": "2026-07-28T06:02:00Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735309, + "name": "No bare compose teardown", + "started_at": "2026-07-28T06:02:22Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735294, + "name": "CodeQL", + "started_at": "2026-07-28T06:02:11Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735271, + "name": "shell-hygiene", + "started_at": "2026-07-28T06:02:04Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735267, + "name": "context-field-presence", + "started_at": "2026-07-28T06:02:12Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735265, + "name": "skill-node-mapping-sync", + "started_at": "2026-07-28T06:02:24Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735259, + "name": "release-identity", + "started_at": "2026-07-28T06:02:24Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735254, + "name": "Duplication Sweep", + "started_at": "2026-07-28T06:02:38Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735249, + "name": "terminal-cost-completeness", + "started_at": "2026-07-28T06:02:06Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735214, + "name": "Handler Contract Compliance", + "started_at": "2026-07-28T06:02:21Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735201, + "name": "Type Safety Validation", + "started_at": "2026-07-28T06:01:56Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735187, + "name": "CI Naming Convention", + "started_at": "2026-07-28T06:02:03Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735144, + "name": "PEP 604 Type Union Check (UP007)", + "started_at": "2026-07-28T06:01:57Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735137, + "name": "Check architecture handshake", + "started_at": "2026-07-28T06:01:53Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735113, + "name": "lint (shadow)", + "started_at": "2026-07-28T06:02:35Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735110, + "name": "contract-validation", + "started_at": "2026-07-28T06:01:49Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735099, + "name": "receipt-honesty", + "started_at": "2026-07-28T06:02:24Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735093, + "name": "Runner Routing Audit", + "started_at": "2026-07-28T06:02:52Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735090, + "name": "tests+coverage (shadow)", + "started_at": "2026-07-28T06:01:58Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735058, + "name": "deployed-migration-tree-sync-logic", + "started_at": "2026-07-28T06:03:56Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735057, + "name": "Canonical Inference Gate", + "started_at": "2026-07-28T06:01:57Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735054, + "name": "node-migration-sync", + "started_at": "2026-07-28T06:03:55Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192735029, + "name": "typecheck (shadow)", + "started_at": "2026-07-28T06:01:58Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192734994, + "name": "check-env-reads-gate", + "started_at": "2026-07-28T06:02:25Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192734979, + "name": "Precommit Parity Gate", + "started_at": "2026-07-28T06:01:42Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192734958, + "name": "Dep Provenance Gate", + "started_at": "2026-07-28T06:03:03Z", + "status": "completed" + }, + { + "conclusion": "success", + "id": 90192734893, + "name": "Publish PR Webhook Event", + "started_at": "2026-07-28T06:02:48Z", + "status": "completed" + } + ] +} diff --git a/tests/ci/test_ci_summary_gate.py b/tests/ci/test_ci_summary_gate.py index 4207a7fca8..d7a47d8588 100644 --- a/tests/ci/test_ci_summary_gate.py +++ b/tests/ci/test_ci_summary_gate.py @@ -19,6 +19,7 @@ import yaml from scripts.ci.ci_summary_gate import ( + ACTOR_CONDITIONAL_CONTEXTS, EXIT_FAILURE, EXIT_PENDING, EXIT_SUCCESS, @@ -26,6 +27,7 @@ MEASURED_NOT_ENFORCED_CONTEXTS, SKIPPABLE_GATE_JOBS, STRICT_GATE_JOBS, + applicable_external_contexts, evaluate, evaluate_external_contexts, latest_check_run_by_name, @@ -643,3 +645,138 @@ def test_failed_check_runs_fetch_removes_the_file(self) -> None: ) run = str(poll["run"]) assert run.count("rm -f check_runs.json") >= 2 + + +# -------------------------------------------------------------------------- +# OMN-15532 — actor-conditional external contexts. +# +# OMN-15496 admitted `gate / CodeRabbit Thread Check` as a fail-closed external +# context after measuring it 16/16 present over #2546…#2567. That window held no +# Dependabot PR. cr-thread-gate-caller.yml skips the caller job when +# `github.actor == 'dependabot[bot]'`, and because the context is the +# `caller-job / reusable-job` form the inner job never materialises — the +# check-run is ABSENT, not `skipped`. Absent burns the 90 min deadline and then +# fails closed against the SOLE required context on infra dev. +# -------------------------------------------------------------------------- + +DEPENDABOT_FIXTURE = ( + REPO_ROOT + / "tests" + / "ci" + / "fixtures" + / "omn15532_dependabot_pr2522_check_runs.json" +) +CR_THREAD_CONTEXT = "gate / CodeRabbit Thread Check" + + +def _dependabot_check_runs() -> list[dict[str, Any]]: + """Real, unedited check-runs from Dependabot PR #2522 (head 2cdf352d).""" + return list(json.loads(DEPENDABOT_FIXTURE.read_text())["check_runs"]) + + +class TestActorConditionalExternalContexts: + def test_fixture_is_the_real_absent_case(self) -> None: + """Guard the premise: the fixture must genuinely lack ONLY this context.""" + rows = _dependabot_check_runs() + names = {str(r["name"]) for r in rows} + absent = [c for c in EXPECTED_EXTERNAL_CONTEXTS if c not in names] + # If this ever changes, the exemption below is no longer justified. + assert absent == [CR_THREAD_CONTEXT], ( + "fixture no longer isolates the CR-thread-gate absence; re-measure " + "before trusting the exemption" + ) + + def test_dependabot_pr_is_not_wedged(self) -> None: + """AC1 — the real absent-context payload resolves for dependabot[bot].""" + code, report = evaluate( + _all_gates("success"), + check_runs=_dependabot_check_runs(), + external_contexts=EXPECTED_EXTERNAL_CONTEXTS, + pr_author="dependabot[bot]", + ) + assert code == EXIT_SUCCESS, report + + def test_same_payload_still_blocks_a_human_author(self) -> None: + """AC2 — the control that matters: the exemption must not leak.""" + code, report = evaluate( + _all_gates("success"), + check_runs=_dependabot_check_runs(), + external_contexts=EXPECTED_EXTERNAL_CONTEXTS, + pr_author="jonahgabriel", + ) + assert code == EXIT_PENDING, report + assert CR_THREAD_CONTEXT in report + + def test_absent_pr_author_enforces_everything(self) -> None: + """A forgotten --pr-author must enforce, never exempt.""" + code, _ = evaluate( + _all_gates("success"), + check_runs=_dependabot_check_runs(), + external_contexts=EXPECTED_EXTERNAL_CONTEXTS, + pr_author=None, + ) + assert code == EXIT_PENDING + + def test_exemption_entry_is_load_bearing( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """AC3 — falsification control. + + Genuinely REMOVE the registry entry and confirm the very same fixture + and author go back to blocking. Without this, the AC1 green could come + from the fixture rather than from the exemption. + """ + import scripts.ci.ci_summary_gate as gate_mod + + monkeypatch.setattr(gate_mod, "ACTOR_CONDITIONAL_CONTEXTS", {}) + code, report = evaluate( + _all_gates("success"), + check_runs=_dependabot_check_runs(), + external_contexts=EXPECTED_EXTERNAL_CONTEXTS, + pr_author="dependabot[bot]", + ) + assert code == EXIT_PENDING, report + assert CR_THREAD_CONTEXT in report + + def test_exemption_drops_only_that_one_context(self) -> None: + """The dependabot exemption must not quietly widen.""" + pruned = applicable_external_contexts( + EXPECTED_EXTERNAL_CONTEXTS, "dependabot[bot]" + ) + assert set(EXPECTED_EXTERNAL_CONTEXTS) - set(pruned) == {CR_THREAD_CONTEXT} + + def test_registry_keys_are_asserted_contexts(self) -> None: + """AC4 — cannot exempt a context that was never asserted.""" + for context in ACTOR_CONDITIONAL_CONTEXTS: + assert context in EXPECTED_EXTERNAL_CONTEXTS, context + + def test_registry_actors_are_concrete_logins(self) -> None: + """AC4 — no wildcard/empty actor may blanket-disable a context.""" + for context, actors in ACTOR_CONDITIONAL_CONTEXTS.items(): + assert actors, f"{context} declares an empty actor tuple" + for actor in actors: + assert actor.strip(), f"{context} declares a blank actor" + assert actor not in {"*", "all"}, f"{context} declares wildcard {actor}" + + def test_a_failing_context_still_fails_for_dependabot(self) -> None: + """The exemption is applicability, not a bypass: reds still block.""" + rows = _dependabot_check_runs() + for row in rows: + if row["name"] == "deploy-gate / deploy-gate": + row["conclusion"] = "failure" + code, report = evaluate( + _all_gates("success"), + check_runs=rows, + external_contexts=EXPECTED_EXTERNAL_CONTEXTS, + pr_author="dependabot[bot]", + ) + assert code == EXIT_FAILURE, report + + def test_ci_yml_passes_pr_author(self) -> None: + """AC5 — a rule is not a mechanism: the wiring must exist in ci.yml.""" + job = _load_workflow(CI_WORKFLOW)["jobs"]["ci-summary"] + poll = next( + s for s in job["steps"] if "Poll run jobs" in str(s.get("name", "")) + ) + assert '--pr-author "${PR_AUTHOR:-}"' in str(poll["run"]) + assert "PR_AUTHOR" in poll["env"]