fix(ci): bind backend candidates to exact main dispatch

This commit is contained in:
2026-09-27 10:28:48 -04:00
parent 6ec84f3e4d
commit 99b3a490ea
5 changed files with 95 additions and 14 deletions
+7
View File
@@ -11,6 +11,7 @@ on:
type: string type: string
env: env:
BUILD_LANE: candidate
CARGO_TERM_COLOR: never CARGO_TERM_COLOR: never
PYTHONDONTWRITEBYTECODE: '1' PYTHONDONTWRITEBYTECODE: '1'
EXPECTED_BUILD_SHA: ${{ inputs.expected_sha }} EXPECTED_BUILD_SHA: ${{ inputs.expected_sha }}
@@ -29,6 +30,12 @@ jobs:
RUSTUP_HOME: ${{ runner.temp }}/rustup-backend-candidate RUSTUP_HOME: ${{ runner.temp }}/rustup-backend-candidate
CARGO_HOME: ${{ runner.temp }}/cargo-backend-candidate CARGO_HOME: ${{ runner.temp }}/cargo-backend-candidate
steps: steps:
- name: Require exact main dispatch before checkout
run: |
test "$GITHUB_EVENT_NAME" = workflow_dispatch || { echo 'Candidate event must be workflow_dispatch.' >&2; exit 1; }
test "$GITHUB_REF" = refs/heads/main || { echo 'Candidate dispatch must select main.' >&2; exit 1; }
[[ "$EXPECTED_BUILD_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo 'Candidate expected_sha must be a full lowercase commit.' >&2; exit 1; }
test "$GITHUB_SHA" = "$EXPECTED_BUILD_SHA" || { echo 'Dispatch main moved from the reviewed expected_sha.' >&2; exit 1; }
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
+1
View File
@@ -9,6 +9,7 @@ on:
branches: [main] branches: [main]
env: env:
BUILD_LANE: ci
CARGO_TERM_COLOR: never CARGO_TERM_COLOR: never
PYTHONDONTWRITEBYTECODE: '1' PYTHONDONTWRITEBYTECODE: '1'
EXPECTED_BUILD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} EXPECTED_BUILD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
+36 -12
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Admit one reviewed canonical source or its exact Builds merge wrapper.""" """Admit one reviewed canonical source or its exact Builds merge wrapper."""
import json
import os import os
from pathlib import Path
import re import re
import subprocess import subprocess
import sys import sys
@@ -11,7 +13,11 @@ class Refusal(ValueError):
def git(*args): def git(*args):
return subprocess.check_output(["git", *args], text=True, stderr=subprocess.PIPE).strip() try:
return subprocess.check_output(["git", *args], text=True, stderr=subprocess.PIPE).strip()
except subprocess.CalledProcessError as error:
cause = " ".join((error.stderr or "Git returned no diagnostic").split())[:600]
raise Refusal(f"Git evidence failed (exit {error.returncode}): {cause}") from None
def commit(value, label): def commit(value, label):
@@ -48,16 +54,36 @@ def admit(source, expected_head=None, require_wrapper=False):
return {"source_commit": source, "build_commit": head, "source_tree": tree} return {"source_commit": source, "build_commit": head, "source_tree": tree}
def main(): def build_context(environment):
if os.environ.get("GITHUB_REPOSITORY") != "Builds/smirk-backend-core": if environment.get("GITHUB_REPOSITORY") != "Builds/smirk-backend-core":
raise Refusal("this workflow is admitted only on Builds/smirk-backend-core") raise Refusal("this workflow is admitted only on Builds/smirk-backend-core")
event = os.environ.get("GITHUB_EVENT_NAME") event = environment.get("GITHUB_EVENT_NAME")
expected = os.environ.get("EXPECTED_BUILD_SHA") lane = environment.get("BUILD_LANE")
if not expected: expected = commit(environment.get("EXPECTED_BUILD_SHA"), "expected event or dispatch SHA")
raise Refusal("this build requires the exact reviewed event or dispatch head") if lane == "candidate":
if event not in {"push", "pull_request", "workflow_dispatch"}: if event != "workflow_dispatch":
raise Refusal("this build event is not admitted") raise Refusal("candidate builds require workflow_dispatch")
wrapper = event != "pull_request" elif lane == "ci":
if event not in {"push", "pull_request"}:
raise Refusal("fundless CI requires push or pull_request")
else:
raise Refusal("build lane is not admitted")
if event == "pull_request":
try:
payload = json.loads(Path(environment.get("GITHUB_EVENT_PATH", "")).read_text())
head = payload["pull_request"]["head"]["sha"]
base = payload["pull_request"]["base"]["ref"]
except (OSError, ValueError, KeyError, TypeError) as error:
raise Refusal(f"pull-request event evidence is unavailable: {type(error).__name__}") from None
if head != expected or base != "main":
raise Refusal("CI checkout does not match the exact main-targeted pull-request event head")
elif environment.get("GITHUB_REF") != "refs/heads/main" or environment.get("GITHUB_SHA") != expected:
raise Refusal("main event head must equal the reviewed full expected_sha")
return expected, event != "pull_request"
def main():
expected, wrapper = build_context(os.environ)
canonical = source_for_checkout(wrapper) canonical = source_for_checkout(wrapper)
result = admit(canonical, expected, require_wrapper=wrapper) result = admit(canonical, expected, require_wrapper=wrapper)
print(f"Admitted canonical source {result['source_commit']} at Builds {result['build_commit']}") print(f"Admitted canonical source {result['source_commit']} at Builds {result['build_commit']}")
@@ -68,5 +94,3 @@ if __name__ == "__main__":
main() main()
except Refusal as error: except Refusal as error:
sys.exit(f"Build source refused: {error}") sys.exit(f"Build source refused: {error}")
except subprocess.CalledProcessError:
sys.exit("Build source refused: Git could not resolve the reviewed source or checkout")
+4 -2
View File
@@ -40,8 +40,10 @@ def main():
parser.add_argument("--output", type=Path, required=True) parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args() args = parser.parse_args()
os.chdir(ROOT) os.chdir(ROOT)
admitted = source.admit(source.source_for_checkout(True), expected, wrapper = source.build_context(os.environ)
os.environ.get("EXPECTED_BUILD_SHA", ""), require_wrapper=True) if not wrapper or os.environ.get("BUILD_LANE") != "candidate":
raise source.Refusal("artifact staging requires the admitted candidate dispatch lane")
admitted = source.admit(source.source_for_checkout(True), expected, require_wrapper=True)
binary = ROOT / "target/x86_64-unknown-linux-gnu/release/smirk-backend-core" binary = ROOT / "target/x86_64-unknown-linux-gnu/release/smirk-backend-core"
required = elf_requirements(binary) required = elf_requirements(binary)
rustc = subprocess.check_output(["rustc", "--version"], text=True).strip() rustc = subprocess.check_output(["rustc", "--version"], text=True).strip()
+47
View File
@@ -101,6 +101,53 @@ class Admission(unittest.TestCase):
source.main() source.main()
class EventBoundary(unittest.TestCase):
def setUp(self):
self.head = "a" * 40
self.environment = {"GITHUB_REPOSITORY": "Builds/smirk-backend-core",
"BUILD_LANE": "candidate", "GITHUB_EVENT_NAME": "workflow_dispatch",
"GITHUB_REF": "refs/heads/main", "GITHUB_SHA": self.head,
"EXPECTED_BUILD_SHA": self.head}
def test_exact_dispatch_event_admitted(self):
self.assertEqual(source.build_context(self.environment), (self.head, True))
def test_bad_ref_event_sha_or_malformed_input_refused(self):
for key, value in (("GITHUB_REF", "refs/heads/topic"), ("GITHUB_EVENT_NAME", "push"),
("GITHUB_SHA", "b" * 40), ("EXPECTED_BUILD_SHA", "main"),
("EXPECTED_BUILD_SHA", "a" * 39), ("BUILD_LANE", "unknown")):
with self.subTest(key=key, value=value), self.assertRaises(source.Refusal):
source.build_context({**self.environment, key: value})
def test_push_requires_main_and_exact_event_sha(self):
env = {**self.environment, "BUILD_LANE": "ci", "GITHUB_EVENT_NAME": "push"}
self.assertEqual(source.build_context(env), (self.head, True))
with self.assertRaises(source.Refusal):
source.build_context({**env, "GITHUB_REF": "refs/heads/topic"})
def test_pr_binds_actual_event_head_and_target(self):
import json
with tempfile.TemporaryDirectory() as directory:
event = Path(directory) / "event.json"
event.write_text(json.dumps({"pull_request": {"head": {"sha": self.head}, "base": {"ref": "main"}}}))
env = {**self.environment, "BUILD_LANE": "ci", "GITHUB_EVENT_NAME": "pull_request",
"GITHUB_EVENT_PATH": str(event)}
self.assertEqual(source.build_context(env), (self.head, False))
with self.assertRaises(source.Refusal):
source.build_context({**env, "EXPECTED_BUILD_SHA": "b" * 40})
event.write_text(json.dumps({"pull_request": {"head": {"sha": self.head}, "base": {"ref": "topic"}}}))
with self.assertRaises(source.Refusal):
source.build_context(env)
def test_git_refusal_reports_bounded_underlying_cause(self):
failure = subprocess.CalledProcessError(128, ["git"], stderr="fatal: object unavailable " + "x" * 2000)
with patch.object(subprocess, "check_output", side_effect=failure):
with self.assertRaises(source.Refusal) as caught:
source.git("rev-parse", "HEAD")
self.assertIn("object unavailable", str(caught.exception))
self.assertLess(len(str(caught.exception)), 700)
class BinaryCompatibility(unittest.TestCase): class BinaryCompatibility(unittest.TestCase):
def setUp(self): def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="smirk-elf-fixture-") self.temp = tempfile.TemporaryDirectory(prefix="smirk-elf-fixture-")