Update docker/build-push-action digest to 53b7df9 #235
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Required-check name canary | |
| # Branch protection on `main` requires `CI gate` and `review-bot-ack`. | |
| # This workflow verifies the in-tree `CI gate` emitters. The scheduled | |
| # branch-protection audit reads `BRANCH_PROTECTION_CONTEXTS_JSON` below | |
| # and compares it with live repository settings. | |
| # | |
| # This canary fails the PR (and the weekly scheduled run) if any of the | |
| # locked invariants drifts. It is intentionally narrow: it does not try | |
| # to read live branch-protection state, only the in-tree workflow files | |
| # the operator has aligned protection against. | |
| # | |
| # Invariants asserted: | |
| # 1. `.github/workflows/ci.yml` has a job whose key is `ci-gate`. | |
| # 2. That job's `name:` is exactly `CI gate`. | |
| # 3. The `test-macos` job's `name:` is a literal string (no `${{ ... }}` | |
| # template) and equals `Test (macos-latest, Python 3.13)`. Required | |
| # context names must resolve in every job state including `skipped`. | |
| # 4. Exactly two workflow files emit a check-run named `CI gate`: | |
| # `ci.yml::ci-gate` (real aggregator) and | |
| # `ci-gate-stub.yml::ci-gate` (synthetic emitter for PRs whose diff | |
| # is entirely paths-ignored by ci.yml). No other emitter allowed. | |
| # | |
| # Run modes: | |
| # * pull_request -- only when a workflow file under `.github/workflows/` | |
| # changes. Catches the regression at PR-review time. | |
| # * schedule -- weekly safety net for indirect drift. | |
| # * workflow_dispatch -- manual operator verification. | |
| on: | |
| pull_request: | |
| paths: | |
| - ".github/workflows/**" | |
| schedule: | |
| - cron: "23 7 * * 1" # Weekly, Monday 07:23 UTC | |
| workflow_dispatch: | |
| concurrency: | |
| group: required-check-canary-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| permissions: {} | |
| jobs: | |
| verify: | |
| name: Required-check name canary | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 3 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Harden runner (audit mode) | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| with: | |
| persist-credentials: false | |
| - name: Verify required-check invariants | |
| env: | |
| REQUIRED_CONTEXT: "CI gate" | |
| BRANCH_PROTECTION_CONTEXTS_JSON: '["CI gate","review-bot-ack"]' | |
| REQUIRED_JOB_KEY: "ci-gate" | |
| MACOS_JOB_KEY: "test-macos" | |
| MACOS_JOB_NAME: "Test (macos-latest, Python 3.13)" | |
| STUB_WORKFLOW: ".github/workflows/ci-gate-stub.yml" | |
| run: | | |
| python3 - <<'PY' | |
| """Assert that the in-tree workflow files still match the single | |
| required context the operator has pinned in branch protection. | |
| Prints a structured report; exits non-zero on any invariant break. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import sys | |
| from pathlib import Path | |
| try: | |
| import yaml | |
| except ModuleNotFoundError: | |
| print("::error::pyyaml is not available on this runner") | |
| sys.exit(2) | |
| REQUIRED_CONTEXT = os.environ["REQUIRED_CONTEXT"] | |
| REQUIRED_JOB_KEY = os.environ["REQUIRED_JOB_KEY"] | |
| MACOS_JOB_KEY = os.environ["MACOS_JOB_KEY"] | |
| MACOS_JOB_NAME = os.environ["MACOS_JOB_NAME"] | |
| STUB_WORKFLOW = os.environ["STUB_WORKFLOW"] | |
| CI = Path(".github/workflows/ci.yml") | |
| STUB = Path(STUB_WORKFLOW) | |
| WORKFLOWS_DIR = Path(".github/workflows") | |
| # Allow-listed (file, job-key) pairs that may emit the | |
| # `CI gate` check. See workflow header for rationale. | |
| ALLOWED_EMITTERS = { | |
| (CI, REQUIRED_JOB_KEY), | |
| (STUB, REQUIRED_JOB_KEY), | |
| } | |
| failures: list[str] = [] | |
| if not CI.exists(): | |
| print(f"::error file={CI}::ci.yml not found") | |
| sys.exit(2) | |
| ci_doc = yaml.safe_load(CI.read_text(encoding="utf-8")) | |
| jobs = ci_doc.get("jobs") if isinstance(ci_doc, dict) else None | |
| if not isinstance(jobs, dict): | |
| failures.append("ci.yml has no `jobs:` mapping") | |
| jobs = {} | |
| # Invariant 1: ci-gate job exists. | |
| ci_gate = jobs.get(REQUIRED_JOB_KEY) | |
| if not isinstance(ci_gate, dict): | |
| failures.append( | |
| f"ci.yml: job key `{REQUIRED_JOB_KEY}` missing. " | |
| "Branch protection requires this job to emit the " | |
| f"`{REQUIRED_CONTEXT}` check." | |
| ) | |
| ci_gate = {} | |
| # Invariant 2: ci-gate.name == "CI gate". | |
| ci_gate_name = ci_gate.get("name") | |
| if ci_gate_name != REQUIRED_CONTEXT: | |
| failures.append( | |
| f"ci.yml: job `{REQUIRED_JOB_KEY}` has name={ci_gate_name!r}; " | |
| f"required-context drift if not {REQUIRED_CONTEXT!r}." | |
| ) | |
| # Invariant 3: test-macos job has a literal name. | |
| # NOTE: the GitHub Actions expression markers are reconstructed | |
| # via string concatenation so the YAML parser does not try to | |
| # interpret the Python literals as workflow expressions. | |
| TEMPLATE_OPEN = "$" "{{" | |
| TEMPLATE_CLOSE = "}" "}" | |
| macos_job = jobs.get(MACOS_JOB_KEY) | |
| macos_name_seen = None | |
| if not isinstance(macos_job, dict): | |
| failures.append( | |
| f"ci.yml: job key `{MACOS_JOB_KEY}` missing." | |
| ) | |
| else: | |
| macos_name = macos_job.get("name", "") | |
| macos_name_seen = macos_name | |
| if not isinstance(macos_name, str): | |
| failures.append( | |
| f"ci.yml: `{MACOS_JOB_KEY}.name` is not a string" | |
| ) | |
| elif TEMPLATE_OPEN in macos_name or TEMPLATE_CLOSE in macos_name: | |
| failures.append( | |
| f"ci.yml: `{MACOS_JOB_KEY}.name` is templated " | |
| f"({macos_name!r}). Skip-state check runs would post " | |
| "the unresolved template, breaking any required-context " | |
| "rule keyed on the literal form." | |
| ) | |
| elif macos_name != MACOS_JOB_NAME: | |
| failures.append( | |
| f"ci.yml: `{MACOS_JOB_KEY}.name` is {macos_name!r}; " | |
| f"canary expects {MACOS_JOB_NAME!r}. If the rename is " | |
| "intentional, update both this canary and the operator " | |
| "runbook." | |
| ) | |
| # Invariant 4: only allow-listed workflow files emit a job | |
| # named "CI gate". Two emitters are intentional: ci.yml (real | |
| # aggregator) and ci-gate-stub.yml (synthetic success for | |
| # paths-ignored-only PRs). | |
| seen_emitters: set[tuple[Path, str]] = set() | |
| for wf_path in sorted(WORKFLOWS_DIR.glob("*.yml")): | |
| try: | |
| wf = yaml.safe_load(wf_path.read_text(encoding="utf-8")) | |
| except yaml.YAMLError: | |
| continue | |
| wf_jobs = wf.get("jobs") if isinstance(wf, dict) else None | |
| if not isinstance(wf_jobs, dict): | |
| continue | |
| for key, body in wf_jobs.items(): | |
| if not isinstance(body, dict): | |
| continue | |
| if body.get("name") != REQUIRED_CONTEXT: | |
| continue | |
| seen_emitters.add((wf_path, key)) | |
| unexpected = seen_emitters - ALLOWED_EMITTERS | |
| missing = ALLOWED_EMITTERS - seen_emitters | |
| if unexpected: | |
| failures.append( | |
| f"Unexpected emitters of `{REQUIRED_CONTEXT}` check: " | |
| + ", ".join(f"{p}:{k}" for p, k in sorted(unexpected)) | |
| + ". Allow-list is " | |
| + ", ".join(f"{p}:{k}" for p, k in sorted(ALLOWED_EMITTERS)) | |
| + "." | |
| ) | |
| if missing: | |
| failures.append( | |
| f"Missing required emitters of `{REQUIRED_CONTEXT}` check: " | |
| + ", ".join(f"{p}:{k}" for p, k in sorted(missing)) | |
| + ". Both ci.yml and ci-gate-stub.yml must keep their `ci-gate` job." | |
| ) | |
| # Report. | |
| print("Required-check name canary") | |
| print(f" required context : {REQUIRED_CONTEXT!r}") | |
| print(f" ci-gate job key : {REQUIRED_JOB_KEY!r}") | |
| print(f" ci-gate.name : {ci_gate_name!r}") | |
| print(f" test-macos.name : {macos_name_seen!r}") | |
| if failures: | |
| print("::error::Required-check name canary FAILED") | |
| for line in failures: | |
| print(f"::error::{line}") | |
| sys.exit(1) | |
| print("All invariants hold. Branch protection alignment intact.") | |
| PY |