"""Keep the automatic main-SHA release proof bound to canonical CI checks."""
from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
WORKFLOW_PATH = Path(".github/workflows/release-eligibility.yml")
ACTION_PATH = Path(".github/actions/release-eligibility/action.yml")
UV_SETUP_ACTION = "astral-sh/setup-uv@ecd24dd710f2fb0dca1693a67af11fc4a5c5ec84"
BUN_SETUP_ACTION = "oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6"
def require_fragment(errors: list[str], text: str, fragment: str, message: str) -> None:
if fragment not in text:
errors.append(message)
def mapping_block(text: str, key: str, indent: int) -> str | None:
"""Return a YAML mapping block with the repository's fixed indentation."""
lines = text.splitlines()
marker = f"{' ' * indent}{key}:"
try:
start = lines.index(marker)
except ValueError:
return None
body: list[str] = []
for line in lines[start + 1 :]:
if line and len(line) - len(line.lstrip()) <= indent:
break
body.append(line)
return "\n".join(body)
def named_step_block(text: str, name: str, indent: int) -> str | None:
"""Return one named step block, stopping at the next peer step."""
lines = text.splitlines()
marker = f"{' ' * indent}- name: {name}"
try:
start = lines.index(marker)
except ValueError:
return None
body = [lines[start]]
peer = f"{' ' * indent}- "
for line in lines[start + 1 :]:
if line.startswith(peer):
break
body.append(line)
return "\n".join(body)
def validate_required_step(errors: list[str], text: str, name: str, indent: int, label: str) -> str:
"""Require a critical step to run normally and fail closed."""
step = named_step_block(text, name, indent)
if step is None:
errors.append(f"release eligibility is missing its {label} step")
return ""
field_indent = " " * (indent + 2)
if re.search(rf"(?m)^{re.escape(field_indent)}[\"']?(?:if|continue-on-error)[\"']?:", step):
errors.append(f"release eligibility {label} step must not be conditionally skipped or tolerated")
script_indent = f"{field_indent} "
if not re.search(rf"(?m)^{re.escape(script_indent)}set -euo pipefail$", step):
errors.append(f"release eligibility {label} step must enable strict shell failure handling")
if re.search(r"\|\|\s*(?:true\b|:|exit\s+0\b)|\bset\s+\+(?:e|o\s+(?:errexit|pipefail))\b", step):
errors.append(f"release eligibility {label} step must not contain a shell fail-open path")
return step
def validate_required_uses_step(errors: list[str], text: str, name: str, indent: int, label: str, action: str) -> str:
"""Require an action step to remain unconditional and pinned."""
step = named_step_block(text, name, indent)
if step is None:
errors.append(f"release eligibility is missing its {label} step")
return ""
field_indent = " " * (indent + 2)
if re.search(rf"(?m)^{re.escape(field_indent)}[\"']?(?:if|continue-on-error)[\"']?:", step):
errors.append(f"release eligibility {label} step must not be conditionally skipped or tolerated")
require_fragment(
errors,
step,
f"uses: {action}",
f"release eligibility {label} step must use the pinned setup action",
)
return step
def validate_workflow(text: str) -> list[str]:
errors: list[str] = []
push = re.search(r"(?ms)^ push:\n(?P<body>(?: .*\n?)*)", text)
if push is None or " branches: [main]" not in push.group("body"):
errors.append("release eligibility must trigger only on pushes to main")
elif re.search(r"(?m)^ (?:paths|paths-ignore|tags|tags-ignore|branches-ignore):", push.group("body")):
errors.append("release eligibility must not path-filter or otherwise narrow main pushes")
on_block = mapping_block(text, "on", 0)
trigger_keys = (
[]
if on_block is None
else [
match.group("key").strip("\"'")
for match in re.finditer(r"(?m)^ (?P<key>[\"']?[A-Za-z_]+[\"']?):", on_block)
]
)
if trigger_keys != ["push", "workflow_dispatch"]:
errors.append("release eligibility must declare the automatic push trigger and workflow_dispatch")
require_fragment(
errors, text, "name: Release Eligibility", "release eligibility workflow is missing its unique check name"
)
require_fragment(
errors,
text,
" release-eligibility:\n name: Release Eligibility",
"release eligibility workflow is missing its uniquely named result job",
)
job = mapping_block(text, "release-eligibility", 2)
if job is None:
errors.append("release eligibility workflow is missing its result job")
elif re.search(r"(?m)^ [\"']?(?:if|continue-on-error)[\"']?:", job):
errors.append("release eligibility result job must not be conditionally skipped or tolerated")
require_fragment(
errors,
text,
"uses: actions/checkout@v7\n with:\n ref: ${{ github.sha }}\n fetch-depth: 0",
"release eligibility must check out the exact GitHub SHA with complete history",
)
require_fragment(
errors,
text,
"uses: ./.github/actions/release-eligibility",
"release eligibility workflow must use the canonical release-eligibility action",
)
require_fragment(
errors,
text,
'echo "before=${{ github.event.before }}" >> "$GITHUB_OUTPUT"',
"release eligibility must resolve the push event before SHA on automatic runs",
)
require_fragment(
errors,
text,
'echo "after=${{ github.event.after }}" >> "$GITHUB_OUTPUT"',
"release eligibility must resolve the push event after SHA on automatic runs",
)
require_fragment(
errors,
text,
"echo \"before=$(git rev-parse --verify HEAD^)\" >> \"$GITHUB_OUTPUT\"",
"release eligibility dispatch must resolve before as HEAD^",
)
require_fragment(
errors,
text,
"echo \"after=$(git rev-parse --verify HEAD)\" >> \"$GITHUB_OUTPUT\"",
"release eligibility dispatch must resolve after as HEAD",
)
for name, expression in {
"ref": "${{ github.ref }}",
"sha": "${{ github.sha }}",
"before": "${{ steps.range.outputs.before }}",
"after": "${{ steps.range.outputs.after }}",
}.items():
require_fragment(
errors,
text,
f" {name}: {expression}",
f"release eligibility must pass {name} as {expression}",
)
invocation = named_step_block(text, "Verify release eligibility", 6)
if invocation is None:
errors.append("release eligibility workflow is missing its action invocation step")
elif re.search(r"(?m)^ [\"']?(?:if|continue-on-error)[\"']?:", invocation):
errors.append("release eligibility action invocation must not be conditionally skipped or tolerated")
permissions = mapping_block(text, "permissions", 0)
if permissions is None or [line.strip() for line in permissions.splitlines() if line.strip()] != ["contents: read"]:
errors.append("release eligibility must use only repository contents: read permissions")
if re.search(r"(?m)^ permissions:", job or ""):
errors.append("release eligibility result job must not override least-privilege workflow permissions")
return errors
def validate_action(text: str) -> list[str]:
errors: list[str] = []
for name in ("ref", "sha", "before", "after"):
require_fragment(
errors,
text,
f" {name}:\n",
f"release eligibility action is missing required {name} input",
)
for env_name, expression in {
"RELEASE_REF": "${{ inputs.ref }}",
"RELEASE_SHA": "${{ inputs.sha }}",
"RELEASE_BEFORE": "${{ inputs.before }}",
"RELEASE_AFTER": "${{ inputs.after }}",
}.items():
require_fragment(
errors,
text,
f" {env_name}: {expression}",
f"release eligibility action must bind {env_name} from {expression}",
)
identity_step = validate_required_step(
errors,
text,
"Verify exact main release identity",
4,
"identity validation",
)
preflight_step = validate_required_step(
errors,
text,
"Run canonical deterministic CI preflight",
4,
"canonical preflight",
)
uv_step = validate_required_uses_step(
errors,
text,
"Set up uv for canonical checks",
4,
"uv setup",
UV_SETUP_ACTION,
)
bun_step = validate_required_uses_step(
errors,
text,
"Set up Bun for canonical checks",
4,
"Bun setup",
BUN_SETUP_ACTION,
)
uv_marker = " - name: Set up uv for canonical checks"
bun_marker = " - name: Set up Bun for canonical checks"
preflight_marker = " - name: Run canonical deterministic CI preflight"
if uv_marker in text and preflight_marker in text and text.index(uv_marker) > text.index(preflight_marker):
errors.append("release eligibility must set up uv before the canonical preflight")
if bun_marker in text and preflight_marker in text and text.index(bun_marker) > text.index(preflight_marker):
errors.append("release eligibility must set up Bun before the canonical preflight")
for fragment, message in (
("RELEASE_CHECKOUT_SHA=\"$(git rev-parse --verify HEAD)\"", "release eligibility must resolve checkout SHA"),
(
".github/scripts/verify_release_eligibility.py",
"release eligibility must validate immutable release identity",
),
("--ref \"$RELEASE_REF\"", "release identity validator must receive the triggering ref"),
("--sha \"$RELEASE_SHA\"", "release identity validator must receive the immutable release SHA"),
("--before \"$RELEASE_BEFORE\"", "release identity validator must receive the deterministic-check base SHA"),
("--after \"$RELEASE_AFTER\"", "release identity validator must receive the push event SHA"),
("--checkout-sha \"$RELEASE_CHECKOUT_SHA\"", "release identity validator must receive the checkout SHA"),
(
"git cat-file -e \"${RELEASE_BEFORE}^{commit}\"",
"release eligibility must verify the base identity is a commit",
),
(
"git cat-file -e \"${RELEASE_SHA}^{commit}\"",
"release eligibility must verify the release identity is a commit",
),
(
"git merge-base --is-ancestor \"$RELEASE_BEFORE\" \"$RELEASE_SHA\"",
"release eligibility must require the base SHA to be an ancestor",
),
(".github/scripts/run_checks.py", "release eligibility must call the canonical deterministic check runner"),
("--lane ci", "release eligibility must use the CI check lane"),
("--base \"$RELEASE_BEFORE\"", "release eligibility must use the event base SHA"),
("--head \"$RELEASE_SHA\"", "release eligibility must use the immutable release SHA as check head"),
("--skip-pr-body-checks", "release eligibility must use the canonical post-merge preflight mode"),
):
require_fragment(errors, text, fragment, message)
for step, fragment, message in (
(
identity_step,
".github/scripts/verify_release_eligibility.py",
"identity validation step must run the immutable identity verifier",
),
(identity_step, "git merge-base --is-ancestor", "identity validation step must enforce main ancestry"),
(uv_step, "enable-cache: true", "release eligibility uv setup must enable the deterministic dependency cache"),
(bun_step, f"uses: {BUN_SETUP_ACTION}", "release eligibility Bun setup must use the pinned setup action"),
(
uv_step,
"cache-dependency-glob: backend/openapi-requirements.txt",
"release eligibility uv setup must key the OpenAPI dependency cache",
),
(
preflight_step,
".github/scripts/run_checks.py",
"canonical preflight step must run the deterministic check runner",
),
):
require_fragment(errors, step, fragment, message)
return errors
def validate(root: Path = ROOT) -> list[str]:
errors: list[str] = []
workflow = root / WORKFLOW_PATH
action = root / ACTION_PATH
if not workflow.is_file():
return [f"release eligibility workflow is missing: {WORKFLOW_PATH}"]
if not action.is_file():
return [f"release eligibility action is missing: {ACTION_PATH}"]
errors.extend(validate_workflow(workflow.read_text(encoding="utf-8")))
errors.extend(validate_action(action.read_text(encoding="utf-8")))
return errors
def main() -> int:
errors = validate()
if errors:
for error in errors:
print(f"ERROR: {error}", file=sys.stderr)
return 1
print("release eligibility contract passed")
return 0
if __name__ == "__main__":
raise SystemExit(main())