Two workflow guards had drifted from the code they describe, so `scripts/ci_guard_checks.sh` failed on a clean tree. The Flutter release-tag pin moved out of build.yml into the shared setup-flutter-git composite action, but the checker read that action from a fixed repository path while its test mutated a workflow fixture. The mutation could not reach the checker, so the rejection test asserted against an unmodified run. The checker now resolves the action beside the workflow it is given, and the test materialises a `.github` tree so the pin is genuinely exercised. The script-test roster likewise moved into ci_guard_checks.sh, which discovers `scripts/test_*.py` by glob; the dispatch guard still expected each one to be named explicitly in ci_checks.sh and ci.yml. It now reads that glob and checks both aggregates delegate to the shared roster.
139 lines
5.3 KiB
Python
Executable File
139 lines
5.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Behavior tests for the privileged build-workflow guard."""
|
|
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
CHECKER = ROOT / "scripts/check_build_workflow.py"
|
|
WORKFLOW = ROOT / ".github/workflows/build.yml"
|
|
SETUP_FLUTTER_GIT = ROOT / ".github/actions/setup-flutter-git/action.yml"
|
|
|
|
|
|
class BuildWorkflowGuardTest(unittest.TestCase):
|
|
def _run(self, workflow: str, action: str | None = None) -> subprocess.CompletedProcess[str]:
|
|
with tempfile.TemporaryDirectory(prefix="plezy-build-workflow-test-") as directory:
|
|
# The checker resolves the shared bootstrap beside the workflow, so
|
|
# the fixture has to mirror the real `.github` layout.
|
|
github = Path(directory) / ".github"
|
|
fixture = github / "workflows/build.yml"
|
|
fixture.parent.mkdir(parents=True)
|
|
fixture.write_text(workflow, encoding="utf-8")
|
|
bootstrap = github / "actions/setup-flutter-git/action.yml"
|
|
bootstrap.parent.mkdir(parents=True)
|
|
bootstrap.write_text(action if action is not None else self._action(), encoding="utf-8")
|
|
return subprocess.run(
|
|
[sys.executable, str(CHECKER), str(fixture)],
|
|
cwd=ROOT,
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
def _workflow(self) -> str:
|
|
return WORKFLOW.read_text(encoding="utf-8")
|
|
|
|
def _action(self) -> str:
|
|
return SETUP_FLUTTER_GIT.read_text(encoding="utf-8")
|
|
|
|
def test_locked_root_signer_passes(self) -> None:
|
|
result = self._run(self._workflow())
|
|
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertIn("architecture matrix checks passed", result.stdout)
|
|
|
|
def test_windows_arm_flutter_without_release_tag_is_rejected(self) -> None:
|
|
action = self._action().replace(
|
|
'git -C $root fetch --depth 1 origin "refs/tags/${version}:refs/tags/${version}"',
|
|
"git -C $root fetch --depth 1 origin 559ffa3f75e7402d65a8def9c28389a9b2e6fe42",
|
|
1,
|
|
)
|
|
self.assertNotEqual(action, self._action(), "fixture mutation no longer matches the action")
|
|
|
|
result = self._run(self._workflow(), action)
|
|
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("refs/tags/${version}", result.stderr)
|
|
|
|
def test_mutable_download_in_signing_step_is_rejected(self) -> None:
|
|
workflow = self._workflow().replace(
|
|
" try {\n",
|
|
" Invoke-WebRequest -Uri https://raw.githubusercontent.com/example/main/sign.dart -OutFile sign.dart\n"
|
|
" try {\n",
|
|
1,
|
|
)
|
|
|
|
result = self._run(workflow)
|
|
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("mutable or ad-hoc input: raw.githubusercontent.com", result.stderr)
|
|
self.assertIn("mutable or ad-hoc input: invoke-webrequest", result.stderr)
|
|
|
|
def test_inline_dependency_resolution_in_signing_step_is_rejected(self) -> None:
|
|
workflow = self._workflow().replace(
|
|
" try {\n",
|
|
" Set-Content -Path pubspec.yaml -Value 'dependencies: {}'\n"
|
|
" dart pub get\n"
|
|
" try {\n",
|
|
1,
|
|
)
|
|
|
|
result = self._run(workflow)
|
|
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("mutable or ad-hoc input: pubspec.yaml", result.stderr)
|
|
self.assertIn("mutable or ad-hoc input: dart pub get", result.stderr)
|
|
|
|
def test_downloaded_signer_execution_is_rejected(self) -> None:
|
|
workflow = self._workflow().replace(
|
|
"dart run auto_updater:sign_update plezy-windows-installer.exe $keyPath",
|
|
"dart run sign.dart plezy-windows-installer.exe $keyPath",
|
|
1,
|
|
)
|
|
|
|
result = self._run(workflow)
|
|
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("must execute the locked auto_updater package", result.stderr)
|
|
|
|
def test_unlocked_install_is_rejected(self) -> None:
|
|
prefix, package_and_after = self._workflow().split(" package-windows:\n", 1)
|
|
workflow = prefix + " package-windows:\n" + package_and_after.replace(
|
|
"flutter pub get --enforce-lockfile --no-example",
|
|
"flutter pub get",
|
|
1,
|
|
)
|
|
|
|
result = self._run(workflow)
|
|
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("enforced root dependency lock", result.stderr)
|
|
|
|
def test_missing_finally_cleanup_is_rejected(self) -> None:
|
|
workflow = self._workflow().replace(" } finally {\n", " }\n", 1)
|
|
|
|
result = self._run(workflow)
|
|
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("cleanup must run from a finally block", result.stderr)
|
|
|
|
def test_libmpv_cache_without_native_manifest_is_rejected(self) -> None:
|
|
workflow = self._workflow().replace(
|
|
"hashFiles('linux/packaging/build-libmpv.sh', 'linux/packaging/native-inputs.json')",
|
|
"hashFiles('linux/packaging/build-libmpv.sh')",
|
|
1,
|
|
)
|
|
|
|
result = self._run(workflow)
|
|
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("native input manifest", result.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|