mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 07:21:32 +02:00
fix(ci): bind backend candidates to exact main dispatch
This commit is contained in:
@@ -11,6 +11,7 @@ on:
|
||||
type: string
|
||||
|
||||
env:
|
||||
BUILD_LANE: candidate
|
||||
CARGO_TERM_COLOR: never
|
||||
PYTHONDONTWRITEBYTECODE: '1'
|
||||
EXPECTED_BUILD_SHA: ${{ inputs.expected_sha }}
|
||||
@@ -29,6 +30,12 @@ jobs:
|
||||
RUSTUP_HOME: ${{ runner.temp }}/rustup-backend-candidate
|
||||
CARGO_HOME: ${{ runner.temp }}/cargo-backend-candidate
|
||||
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
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -9,6 +9,7 @@ on:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
BUILD_LANE: ci
|
||||
CARGO_TERM_COLOR: never
|
||||
PYTHONDONTWRITEBYTECODE: '1'
|
||||
EXPECTED_BUILD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
+36
-12
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Admit one reviewed canonical source or its exact Builds merge wrapper."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -11,7 +13,11 @@ class Refusal(ValueError):
|
||||
|
||||
|
||||
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):
|
||||
@@ -48,16 +54,36 @@ def admit(source, expected_head=None, require_wrapper=False):
|
||||
return {"source_commit": source, "build_commit": head, "source_tree": tree}
|
||||
|
||||
|
||||
def main():
|
||||
if os.environ.get("GITHUB_REPOSITORY") != "Builds/smirk-backend-core":
|
||||
def build_context(environment):
|
||||
if environment.get("GITHUB_REPOSITORY") != "Builds/smirk-backend-core":
|
||||
raise Refusal("this workflow is admitted only on Builds/smirk-backend-core")
|
||||
event = os.environ.get("GITHUB_EVENT_NAME")
|
||||
expected = os.environ.get("EXPECTED_BUILD_SHA")
|
||||
if not expected:
|
||||
raise Refusal("this build requires the exact reviewed event or dispatch head")
|
||||
if event not in {"push", "pull_request", "workflow_dispatch"}:
|
||||
raise Refusal("this build event is not admitted")
|
||||
wrapper = event != "pull_request"
|
||||
event = environment.get("GITHUB_EVENT_NAME")
|
||||
lane = environment.get("BUILD_LANE")
|
||||
expected = commit(environment.get("EXPECTED_BUILD_SHA"), "expected event or dispatch SHA")
|
||||
if lane == "candidate":
|
||||
if event != "workflow_dispatch":
|
||||
raise Refusal("candidate builds require workflow_dispatch")
|
||||
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)
|
||||
result = admit(canonical, expected, require_wrapper=wrapper)
|
||||
print(f"Admitted canonical source {result['source_commit']} at Builds {result['build_commit']}")
|
||||
@@ -68,5 +94,3 @@ if __name__ == "__main__":
|
||||
main()
|
||||
except Refusal as 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")
|
||||
|
||||
@@ -40,8 +40,10 @@ def main():
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
os.chdir(ROOT)
|
||||
admitted = source.admit(source.source_for_checkout(True),
|
||||
os.environ.get("EXPECTED_BUILD_SHA", ""), require_wrapper=True)
|
||||
expected, wrapper = source.build_context(os.environ)
|
||||
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"
|
||||
required = elf_requirements(binary)
|
||||
rustc = subprocess.check_output(["rustc", "--version"], text=True).strip()
|
||||
|
||||
@@ -101,6 +101,53 @@ class Admission(unittest.TestCase):
|
||||
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):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory(prefix="smirk-elf-fixture-")
|
||||
|
||||
Reference in New Issue
Block a user