7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
166 lines
6.3 KiB
Python
Executable File
166 lines
6.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Pre-commit hook: keep release-gate.yml's two needs lists in sync.
|
|
|
|
release-gate.yml has TWO jobs that must wait on every security scan which
|
|
uploads a SARIF report to GitHub code scanning:
|
|
|
|
- ``release-gate-summary`` — fails the gate if any scan job failed.
|
|
- ``check-code-scanning-alerts`` — waits for the SARIF uploads to be
|
|
indexed, then fails the gate if any open critical/high/medium alert exists.
|
|
|
|
Both ``needs:`` lists are maintained by hand, with nothing keeping them in
|
|
sync. That drift is a real, shipped bug: ``grype-scan`` was in the summary's
|
|
needs but was omitted from ``check-code-scanning-alerts`` for ~1.5 years
|
|
(fixed in #4817). Because Grype runs ``fail-build: false`` (a findings-only run
|
|
"succeeds"), its findings silently bypassed the alert gate the whole time — the
|
|
alert query could run before Grype's SARIF was even uploaded.
|
|
|
|
This hook recomputes, from source, the set of release-gate jobs that upload a
|
|
SARIF to code scanning, and fails if any of them is missing from EITHER needs
|
|
list. So the next scanner added to one list but forgotten in the other is
|
|
caught at commit time instead of silently not gating a release.
|
|
|
|
A job is treated as a SARIF uploader if a ``SARIF_UPLOAD_MARKERS`` substring
|
|
appears in either its OWN inline steps or the LOCAL reusable workflow it calls
|
|
(``uses: ./…``). Limits worth knowing: a uploader pulled in via a REMOTE
|
|
reusable workflow (``uses: org/repo/…@ref``) cannot be inspected here, and a
|
|
new upload mechanism needs a new entry in ``SARIF_UPLOAD_MARKERS``. None exist
|
|
today; revisit this hook if either changes.
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
RELEASE_GATE = REPO_ROOT / ".github" / "workflows" / "release-gate.yml"
|
|
|
|
# Substrings whose presence means a job uploads a SARIF report to GitHub code
|
|
# scanning (so its findings become gating alerts).
|
|
SARIF_UPLOAD_MARKERS = (
|
|
"codeql-action/upload-sarif", # grype, trivy, bearer, semgrep, devskim, …
|
|
"codeql-action/analyze", # codeql (uploads internally)
|
|
"zizmor-action", # zizmor (uploads internally)
|
|
)
|
|
|
|
# The two jobs that must each `needs:` every SARIF-uploading scan.
|
|
CONSUMER_JOBS = ("check-code-scanning-alerts", "release-gate-summary")
|
|
|
|
|
|
def text_uploads_sarif(text: str) -> bool:
|
|
return any(marker in text for marker in SARIF_UPLOAD_MARKERS)
|
|
|
|
|
|
def job_uploads_sarif(job: dict, errors: list[str]) -> bool:
|
|
"""True if a job uploads SARIF via its own inline steps or a local
|
|
reusable workflow it calls.
|
|
|
|
A ``uses: ./…`` reference to a file that does not exist is recorded as a
|
|
loud error (appended to ``errors``) rather than silently treated as a
|
|
non-uploader — a typo'd workflow reference should fail the hook, not slip
|
|
a scanner past it.
|
|
"""
|
|
# Inline steps defined directly on the job.
|
|
if text_uploads_sarif(yaml.safe_dump(job)):
|
|
return True
|
|
# Local reusable workflow the job calls.
|
|
uses = job.get("uses")
|
|
if isinstance(uses, str) and uses.startswith("./"):
|
|
workflow = REPO_ROOT / uses[2:] # strip leading "./"
|
|
if not workflow.is_file():
|
|
errors.append(
|
|
f"missing reusable workflow referenced by a job: {uses}"
|
|
)
|
|
return False
|
|
return text_uploads_sarif(workflow.read_text(encoding="utf-8"))
|
|
return False
|
|
|
|
|
|
def needs_of(job: object) -> set[str]:
|
|
"""Normalize a job's `needs` (str | list | missing) to a set."""
|
|
if not isinstance(job, dict):
|
|
return set()
|
|
needs = job.get("needs", [])
|
|
if isinstance(needs, str):
|
|
return {needs}
|
|
if isinstance(needs, list):
|
|
return set(needs)
|
|
return set()
|
|
|
|
|
|
def main() -> int:
|
|
try:
|
|
gate = yaml.safe_load(RELEASE_GATE.read_text(encoding="utf-8"))
|
|
except (OSError, yaml.YAMLError) as exc:
|
|
print(f"❌ Could not parse {RELEASE_GATE}: {exc}")
|
|
return 1
|
|
|
|
jobs = gate.get("jobs", {}) if isinstance(gate, dict) else {}
|
|
|
|
# SARIF-uploading jobs = release-gate jobs that upload SARIF (inline or via
|
|
# the local reusable workflow they call).
|
|
errors: list[str] = []
|
|
sarif_jobs: dict[str, str] = {}
|
|
for job_id, job in jobs.items():
|
|
if not isinstance(job, dict):
|
|
continue
|
|
if job_uploads_sarif(job, errors):
|
|
uses = job.get("uses")
|
|
sarif_jobs[job_id] = (
|
|
uses[2:]
|
|
if isinstance(uses, str) and uses.startswith("./")
|
|
else "inline steps"
|
|
)
|
|
|
|
if errors:
|
|
print(
|
|
"❌ release-gate.yml references a workflow file that does not exist"
|
|
)
|
|
print("=" * 64)
|
|
for err in errors:
|
|
print(f" - {err}")
|
|
print("=" * 64)
|
|
print(
|
|
"FIX: correct the `uses:` path in .github/workflows/release-gate.yml"
|
|
)
|
|
return 1
|
|
|
|
if not sarif_jobs:
|
|
print("❌ No SARIF-uploading jobs detected in release-gate.yml.")
|
|
print(" The detector is likely broken — check SARIF_UPLOAD_MARKERS.")
|
|
return 1
|
|
|
|
violations: list[tuple[str, str, str]] = [] # (consumer, job, source)
|
|
for consumer in CONSUMER_JOBS:
|
|
needs = needs_of(jobs.get(consumer, {}))
|
|
for job_id, source in sorted(sarif_jobs.items()):
|
|
if job_id not in needs:
|
|
violations.append((consumer, job_id, source))
|
|
|
|
if violations:
|
|
print("❌ SARIF SCANNER MISSING FROM A release-gate.yml needs LIST")
|
|
print("=" * 64)
|
|
print("Every scan that uploads a SARIF to code scanning must be in the")
|
|
print("`needs:` of BOTH check-code-scanning-alerts and")
|
|
print("release-gate-summary. A job missing from check-code-scanning-")
|
|
print("alerts can let its findings race the indexing query and bypass")
|
|
print(
|
|
"the gate (this silently happened to grype-scan for ~1.5y, #4817)."
|
|
)
|
|
print("=" * 64)
|
|
for consumer, job_id, source in violations:
|
|
print(f"\n job '{job_id}' ({source}) uploads SARIF")
|
|
print(f" but is NOT in {consumer}.needs")
|
|
print("\n" + "=" * 64)
|
|
print("FIX: add the job id to that job's `needs:` list in")
|
|
print(" .github/workflows/release-gate.yml")
|
|
print("=" * 64)
|
|
return 1
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|