cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

fix(ci): enforce exact demo-gate contract

Dami committed 221e06e
commit
221e06e7e94ffbccec44204130f1159f35401918
parent
e64a7bb336306d6480d3f6010b80c4dad7f12478
author
Dami <dami.etoile@gmail.com>
2 files changed, +200 -7
Mscripts/tests/test_ci_cross_build_policy.py148++++++++++++++++++++++++++++++
Mscripts/verify_ci_cross_build_policy.py59+++++++++++-
M scripts/tests/test_ci_cross_build_policy.py +148 -0
@@ -29,6 +29,13 @@ def remove_named_step(source: str, name: str, next_name: str) -> tuple[str, str]
2929 return source[:start] + source[end:], source[start:end]3030 3131 32+def replace_once_in_demo_gate(source: str, old: str, new: str) -> str:33+ prefix, marker, demo_gate = source.partition(" demo-gate:\n")34+ if not marker:35+ raise AssertionError("fixture source must contain the demo-gate job")36+ return prefix + marker + replace_once(demo_gate, old, new)37+38+3239 class CiCrossBuildPolicyTests(unittest.TestCase):3340 def test_repository_workflow_passes(self) -> None:3441 verify(WORKFLOW)
@@ -107,6 +114,147 @@ class CiCrossBuildPolicyTests(unittest.TestCase):
107114 with self.assertRaisesRegex(PolicyError, "unexpected or missing fields"):108115 verify(mutated)109116 117+ def test_rejects_checkout_ref_override(self) -> None:118+ mutated = replace_once_in_demo_gate(119+ WORKFLOW,120+ " uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1",121+ " uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1\n"122+ " with:\n"123+ " ref: main",124+ )125+ with self.assertRaisesRegex(126+ PolicyError, "checkout step has unexpected or missing fields"127+ ):128+ verify(mutated)129+130+ def test_rejects_checkout_action_change(self) -> None:131+ mutated = replace_once_in_demo_gate(132+ WORKFLOW,133+ "actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1",134+ "actions/checkout@main",135+ )136+ with self.assertRaisesRegex(PolicyError, "checkout action is not digest-pinned"):137+ verify(mutated)138+139+ def test_rejects_step_poisoning_cargo_environment(self) -> None:140+ mutated = replace_once(141+ WORKFLOW,142+ " - name: Install Linux build dependencies\n",143+ " - name: Poison Cargo environment\n"144+ " run: printf '%s\\n' 'export RUSTC_WRAPPER=true' >> ~/.cargo/env\n\n"145+ " - name: Install Linux build dependencies\n",146+ )147+ with self.assertRaisesRegex(148+ PolicyError, "step list or order changed from the CI policy contract"149+ ):150+ verify(mutated)151+152+ def test_rejects_step_replacing_cargo_zigbuild(self) -> None:153+ mutated = replace_once(154+ WORKFLOW,155+ " - name: Run complete demo gate\n",156+ " - name: Replace cargo-zigbuild\n"157+ " run: |\n"158+ " printf '%s\\n' '#!/usr/bin/env bash' 'exit 0' "159+ "> ~/.cargo/bin/cargo-zigbuild\n"160+ " chmod 0755 ~/.cargo/bin/cargo-zigbuild\n\n"161+ " - name: Run complete demo gate\n",162+ )163+ with self.assertRaisesRegex(164+ PolicyError, "step list or order changed from the CI policy contract"165+ ):166+ verify(mutated)167+168+ def test_rejects_extra_steps_at_every_boundary(self) -> None:169+ markers = [170+ " - name: Check out repository\n",171+ " - name: Install Linux build dependencies\n",172+ " - name: Restore pinned cargo-zigbuild\n",173+ " - name: Install pinned cross-build tools\n",174+ " - name: Run complete demo gate\n",175+ ]176+ for position, marker in enumerate(markers):177+ with self.subTest(position=position):178+ mutated = replace_once_in_demo_gate(179+ WORKFLOW,180+ marker,181+ f" - name: Extra step {position}\n"182+ " run: echo unexpected\n\n"183+ f"{marker}",184+ )185+ with self.assertRaisesRegex(186+ PolicyError, "step list or order changed from the CI policy contract"187+ ):188+ verify(mutated)189+190+ mutated = WORKFLOW + (191+ "\n"192+ " - name: Extra trailing step\n"193+ " run: echo unexpected\n"194+ )195+ with self.assertRaisesRegex(196+ PolicyError, "step list or order changed from the CI policy contract"197+ ):198+ verify(mutated)199+200+ def test_rejects_required_step_reordering(self) -> None:201+ cache_marker = " - name: Restore pinned cargo-zigbuild\n"202+ install_marker = " - name: Install pinned cross-build tools\n"203+ gate_marker = " - name: Run complete demo gate\n"204+ cache_start = WORKFLOW.index(cache_marker)205+ install_start = WORKFLOW.index(install_marker, cache_start)206+ gate_start = WORKFLOW.index(gate_marker, install_start)207+ cache_step = WORKFLOW[cache_start:install_start]208+ install_step = WORKFLOW[install_start:gate_start]209+ mutated = (210+ WORKFLOW[:cache_start]211+ + install_step212+ + cache_step213+ + WORKFLOW[gate_start:]214+ )215+ with self.assertRaisesRegex(216+ PolicyError, "step list or order changed from the CI policy contract"217+ ):218+ verify(mutated)219+220+ def test_rejects_partial_linux_dependency_body_mutations(self) -> None:221+ mutations = [222+ (223+ " ffmpeg \\\n",224+ "",225+ ),226+ (227+ " shellcheck\n",228+ " shellcheck \\\n"229+ " unzip\n",230+ ),231+ (232+ " sudo apt-get update\n",233+ " sudo apt-get update --quiet\n",234+ ),235+ ]236+ for old, new in mutations:237+ with self.subTest(replacement=new):238+ mutated = replace_once(WORKFLOW, old, new)239+ with self.assertRaisesRegex(240+ PolicyError, "Linux dependency step body changed"241+ ):242+ verify(mutated)243+244+ def test_rejects_linux_dependency_step_field_changes(self) -> None:245+ mutated = replace_once(246+ WORKFLOW,247+ " - name: Install Linux build dependencies\n run: |",248+ " - name: Install Linux build dependencies\n"249+ " env:\n"250+ " BASH_ENV: ~/.cargo/env\n"251+ " run: |",252+ )253+ with self.assertRaisesRegex(254+ PolicyError, "Linux dependency step has unexpected or missing fields"255+ ):256+ verify(mutated)257+110258 def test_rejects_conditionally_disabled_demo_job(self) -> None:111259 mutated = replace_once(112260 WORKFLOW,
M scripts/verify_ci_cross_build_policy.py +52 -7
@@ -10,11 +10,19 @@ from dataclasses import dataclass, field
1010 from pathlib import Path1111 1212 13+CHECKOUT_ACTION = "actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8"1314 CACHE_ACTION = "actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809"1415 CACHE_PATH = "~/.cargo/bin/cargo-zigbuild"1516 CACHE_KEY = "cargo-zigbuild-${{ runner.os }}-${{ runner.arch }}-0.23.0"1617 LINUX_CROSS_BUILD_GATE = "./scripts/run-linux-cross-build-gate.sh"1718 MAPPING_KEY = r"""(?:[A-Za-z0-9_-]+|"[A-Za-z0-9_-]+"|'[A-Za-z0-9_-]+')"""19+REQUIRED_STEP_NAMES = [20+ "Check out repository",21+ "Install Linux build dependencies",22+ "Restore pinned cargo-zigbuild",23+ "Install pinned cross-build tools",24+ "Run complete demo gate",25+]1826 GATE_SCRIPT_PREFIX = """\1927 #!/usr/bin/env bash2028 set -euo pipefail
@@ -31,6 +39,29 @@ fi
3139 ./scripts/run-linux-cross-build-gate.sh3240 """3341 42+DEPENDENCY_RUN = """\43+sudo apt-get update44+sudo apt-get install --yes --no-install-recommends \\45+ build-essential \\46+ file \\47+ ffmpeg \\48+ gstreamer1.0-plugins-base \\49+ gstreamer1.0-plugins-bad \\50+ gstreamer1.0-plugins-good \\51+ gstreamer1.0-plugins-ugly \\52+ gstreamer1.0-tools \\53+ libayatana-appindicator3-dev \\54+ libgstreamer-plugins-base1.0-dev \\55+ libgstreamer1.0-dev \\56+ librsvg2-dev \\57+ libssl-dev \\58+ libudev-dev \\59+ libwebkit2gtk-4.1-dev \\60+ libxdo-dev \\61+ pkg-config \\62+ python3-venv \\63+ shellcheck"""64+3465 INSTALL_RUN = """\3566 python3 -m venv "$RUNNER_TEMP/zig-venv"3667 printf '%s\\n' \\
@@ -402,6 +433,7 @@ def verify(workflow: str) -> None:
402433 _validate_workflow_layout(workflow)403434 steps = _parse_steps(_job_lines(workflow, "demo-gate"))404435 436+ checkout_index, checkout = _named_step(steps, "Check out repository")405437 dependency_index, dependency = _named_step(406438 steps, "Install Linux build dependencies"407439 )
@@ -409,13 +441,20 @@ def verify(workflow: str) -> None:
409441 install_index, install = _named_step(steps, "Install pinned cross-build tools")410442 gate_index, gate = _named_step(steps, "Run complete demo gate")411443 412- dependency_run = dependency.blocks.get("run", "")413- if " python3-venv \\" not in dependency_run.splitlines():444+ if [step.name for step in steps] != REQUIRED_STEP_NAMES:414445 raise PolicyError(415- "Linux dependency step must install python3-venv for the pinned Zig venv"446+ "demo-gate step list or order changed from the CI policy contract"416447 )417- if "if" in dependency.field_names:418- raise PolicyError("Linux dependency step must not be conditionally disabled")448+449+ if checkout.field_names != {"name", "uses"}:450+ raise PolicyError("checkout step has unexpected or missing fields")451+ if checkout.scalars.get("uses") != CHECKOUT_ACTION:452+ raise PolicyError("checkout action is not digest-pinned to the policy contract")453+454+ if dependency.field_names != {"name", "run"}:455+ raise PolicyError("Linux dependency step has unexpected or missing fields")456+ if dependency.blocks.get("run") != DEPENDENCY_RUN:457+ raise PolicyError("Linux dependency step body changed")419458 420459 if cache.field_names != {"name", "uses", "with"}:421460 raise PolicyError("cargo-zigbuild cache step has unexpected or missing fields")
@@ -439,9 +478,15 @@ def verify(workflow: str) -> None:
439478 if gate.scalars.get("run") != "./scripts/verify-demo-build.sh":440479 raise PolicyError("complete demo gate does not execute the repository gate")441480 442- if not dependency_index < cache_index < install_index < gate_index:481+ if [482+ checkout_index,483+ dependency_index,484+ cache_index,485+ install_index,486+ gate_index,487+ ] != list(range(len(REQUIRED_STEP_NAMES))):443488 raise PolicyError(444- "dependency, cache, cross-tool install, and demo-gate steps are misordered"489+ "demo-gate step list or order changed from the CI policy contract"445490 )446491 447492