From cacd1cd2bdfe30aded5473c64da65d960c481812 Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 9 Sep 2026 20:41:07 +0300 Subject: [PATCH] ci: isolate Ubuntu sources and select Git Bash for Windows tests --- .github/workflows/build.yml | 12 ++++-- .github/workflows/check.yml | 12 ++++-- .github/workflows/release.yml | 10 ++++- AGENTS.md | 2 + docs/launcher-architecture.md | 5 ++- scripts/release_gate_test.py | 69 ++++++++++++++++++++++++++++++++--- 6 files changed, 95 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 96d9c94..e20ab2d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,13 +54,19 @@ jobs: - name: Install Linux desktop dependencies if: runner.os == 'Linux' run: | - sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf + # Ubuntu 22.04 keeps distro sources here; unrelated vendor repos are not needed. + test -s /etc/apt/sources.list + apt_options=( + -o Dir::Etc::sourcelist=/etc/apt/sources.list + -o Dir::Etc::sourceparts=- + ) + sudo apt-get "${apt_options[@]}" --error-on=any update + sudo apt-get "${apt_options[@]}" install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf - run: npm ci - run: npm test - - run: cargo test --locked --manifest-path src-tauri/Cargo.toml - run: cargo build --locked --release --manifest-path scripts/release-verifier/Cargo.toml - run: python -m unittest discover -s scripts -p 'release_test.py' -v + - run: cargo test --locked --manifest-path src-tauri/Cargo.toml - name: Generate disposable CI key (never a production secret) run: | python scripts/release.py ci-key --directory "$RUNNER_TEMP/ci-updater" diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 5feb871..d096d78 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -27,11 +27,17 @@ jobs: - uses: dtolnay/rust-toolchain@stable - name: Install Linux desktop dependencies run: | - sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf + # Ubuntu 22.04 keeps distro sources here; unrelated vendor repos are not needed. + test -s /etc/apt/sources.list + apt_options=( + -o Dir::Etc::sourcelist=/etc/apt/sources.list + -o Dir::Etc::sourceparts=- + ) + sudo apt-get "${apt_options[@]}" --error-on=any update + sudo apt-get "${apt_options[@]}" install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf - run: npm ci - run: npm test - run: npm run build - - run: cargo test --locked --manifest-path src-tauri/Cargo.toml - run: cargo build --locked --release --manifest-path scripts/release-verifier/Cargo.toml - run: python -m unittest discover -s scripts -p 'release_test.py' -v + - run: cargo test --locked --manifest-path src-tauri/Cargo.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9eafe11..755a9b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -115,8 +115,14 @@ jobs: - name: Install Linux desktop dependencies if: runner.os == 'Linux' run: | - sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf + # Ubuntu 22.04 keeps distro sources here; unrelated vendor repos are not needed. + test -s /etc/apt/sources.list + apt_options=( + -o Dir::Etc::sourcelist=/etc/apt/sources.list + -o Dir::Etc::sourceparts=- + ) + sudo apt-get "${apt_options[@]}" --error-on=any update + sudo apt-get "${apt_options[@]}" install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf - run: npm ci - run: npm test - run: cargo test --locked --manifest-path src-tauri/Cargo.toml diff --git a/AGENTS.md b/AGENTS.md index 7f6112c..b8f87da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -109,6 +109,8 @@ payload are in `/root/shacraft` on the ShaCraft host; see - `docs/game-trust-boundary.md` — the Mojang/NeoForge/Microsoft/Adoptium trust domains used to install and run the game itself. - `.github/workflows/check.yml` — push/PR UI checks and Linux Rust tests. + Ubuntu 22.04 dependency steps use only the runner main Ubuntu source list; + keep APT signature/hash checks and fail on index errors. Vendor PPAs are not needed. - `.github/workflows/build.yml` — four-platform CI packages with disposable test signing keys, explicitly unusable as production releases. - Release workflows and `scripts/release.py` implement protected draft → publish diff --git a/docs/launcher-architecture.md b/docs/launcher-architecture.md index 087c7ca..997c9eb 100644 --- a/docs/launcher-architecture.md +++ b/docs/launcher-architecture.md @@ -110,7 +110,10 @@ Hostile same-user TOCTOU is outside this protection; it is not an OS sandbox. `npm test` covers asynchronous helpers and state transitions; `npm run build` runs strict TypeScript before Vite. `cargo test --locked` -covers native policy and storage. Push/PR CI repeats checks on Linux. +covers native policy and storage. Push/PR CI repeats checks on Linux. Ubuntu 22.04 +CI installs desktop dependencies from the runner main distro source list only, +without indexing unrelated vendor repositories. APT signature/hash checks remain +mandatory, and any distro index failure stops installation. The package workflow builds and checks Windows x64, Linux x64 and both macOS architectures with disposable test signing keys. These artifacts cannot be published as production updater releases. Separate protected workflows assemble diff --git a/scripts/release_gate_test.py b/scripts/release_gate_test.py index c3291c4..3c16897 100644 --- a/scripts/release_gate_test.py +++ b/scripts/release_gate_test.py @@ -2,10 +2,12 @@ import os import re +import shutil import subprocess import tempfile import unittest from pathlib import Path +from unittest.mock import patch ROOT = Path(os.environ.get("RELEASE_TEST_ROOT", Path(__file__).resolve().parents[1])) @@ -13,6 +15,32 @@ WORKFLOWS = [ROOT / ".github/workflows" / name for name in ("release.yml", "rele GATE = " - name: Resolve tag using trusted workflow Git commands\n" +def gate_bash(windows=os.name == "nt"): + # Native CreateProcess can resolve bare bash to the System32 WSL launcher + # before Git Bash. Bind tests to the Bash installed with the Git we use. + if windows: + git = shutil.which("git") + if git: + directory = Path(git).resolve().parent + candidates = ( + directory / "bash.exe", # Git/bin/git.exe + directory.parent / "bin/bash.exe", # Git/cmd/git.exe + directory.parent.parent / "bin/bash.exe", # Git/mingw64/bin/git.exe + ) + for candidate in candidates: + if candidate.is_file(): + return str(candidate.resolve()) + raise RuntimeError("Git for Windows Bash was not found beside the installed Git") + bash = shutil.which("bash") + if not bash: + raise RuntimeError("Bash is required to execute the release workflow gate") + return str(Path(bash).resolve()) + + +def gate_diagnostic(result): + return f"command={result.args!r}\nstdout={result.stdout!r}\nstderr={result.stderr!r}" + + def workflow_gate(workflow): after = workflow.read_text().split(GATE, 1)[1] lines = after.split(" run: |\n", 1)[1].splitlines() @@ -60,23 +88,52 @@ class ReleaseGateTests(unittest.TestCase): def run_gate(self, workflow, tag): output = self.directory / (workflow.stem + "-output") output.unlink(missing_ok=True) - result = subprocess.run(["bash", "-c", workflow_gate(workflow)], cwd=self.directory, + result = subprocess.run([gate_bash(), "--noprofile", "--norc", "-c", workflow_gate(workflow)], cwd=self.directory, env=dict(self.environment, RELEASE_TAG=tag, GITHUB_OUTPUT=str(output)), capture_output=True, text=True, check=False) return result, output.read_text() if output.exists() else "" + def test_windows_gate_uses_git_bash_even_when_path_contains_wsl_launcher(self): + git_root = self.directory / "Git with spaces" + bash = git_root / "bin/bash.exe" + bash.parent.mkdir(parents=True) + bash.touch() + wsl = self.directory / "System32/bash.exe" + wsl.parent.mkdir() + wsl.touch() + for layout in ("bin", "cmd", "mingw64/bin"): + with self.subTest(layout=layout): + git = git_root / layout / "git.exe" + git.parent.mkdir(parents=True, exist_ok=True) + git.touch() + with patch.object(shutil, "which", side_effect=lambda name: str(git if name == "git" else wsl)) as lookup: + self.assertEqual(gate_bash(windows=True), str(bash.resolve())) + lookup.assert_called_once_with("git") + + def test_windows_gate_never_falls_back_to_unrelated_bash(self): + git = self.directory / "Git/cmd/git.exe" + git.parent.mkdir(parents=True) + git.touch() + wsl = self.directory / "System32/bash.exe" + wsl.parent.mkdir() + wsl.touch() + with patch.object(shutil, "which", side_effect=lambda name: str(git if name == "git" else wsl)) as lookup: + with self.assertRaisesRegex(RuntimeError, "Git for Windows Bash"): + gate_bash(windows=True) + lookup.assert_called_once_with("git") + def test_main_tag_emits_immutable_commit(self): for workflow in WORKFLOWS: with self.subTest(workflow=workflow.name): result, output = self.run_gate(workflow, "v0.2.0") - self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.returncode, 0, gate_diagnostic(result)) self.assertEqual(output, f"commit={self.good}\n") def test_unreviewed_tag_cannot_replace_its_own_ancestry_validator(self): for workflow in WORKFLOWS: with self.subTest(workflow=workflow.name): result, output = self.run_gate(workflow, "v9.9.9") - self.assertNotEqual(result.returncode, 0) + self.assertNotEqual(result.returncode, 0, gate_diagnostic(result)) self.assertEqual(output, "") self.assertFalse((self.directory / "untrusted-code-ran").exists()) @@ -84,15 +141,15 @@ class ReleaseGateTests(unittest.TestCase): for workflow in WORKFLOWS: with self.subTest(workflow=workflow.name): result, output = self.run_gate(workflow, "v8.8.8") - self.assertNotEqual(result.returncode, 0) + self.assertNotEqual(result.returncode, 0, gate_diagnostic(result)) self.assertEqual(output, "") self.git("tag", "-f", "v0.2.0", self.good) result, output = self.run_gate(workflow, "v0.2.0") - self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.returncode, 0, gate_diagnostic(result)) self.git("tag", "-f", "v0.2.0", self.bad) self.assertEqual(output, f"commit={self.good}\n") result, fresh_output = self.run_gate(workflow, "v0.2.0") - self.assertNotEqual(result.returncode, 0) + self.assertNotEqual(result.returncode, 0, gate_diagnostic(result)) self.assertEqual(fresh_output, "") def test_workflows_execute_candidate_code_only_after_gate_and_checkout_sha(self):