Merge pull request #2 from emil28092005/codex/reliability-and-onboarding
Cross-platform build (disposable signatures) / CI test bundle / linux-x86_64 (push) Waiting to run
Cross-platform build (disposable signatures) / CI test bundle / darwin-aarch64 (push) Waiting to run
Cross-platform build (disposable signatures) / CI test bundle / darwin-x86_64 (push) Waiting to run
Cross-platform build (disposable signatures) / CI test bundle / windows-x86_64 (push) Waiting to run
Launcher checks / check (push) Waiting to run
Cross-platform build (disposable signatures) / CI test bundle / linux-x86_64 (push) Waiting to run
Cross-platform build (disposable signatures) / CI test bundle / darwin-aarch64 (push) Waiting to run
Cross-platform build (disposable signatures) / CI test bundle / darwin-x86_64 (push) Waiting to run
Cross-platform build (disposable signatures) / CI test bundle / windows-x86_64 (push) Waiting to run
Launcher checks / check (push) Waiting to run
Fix cross-platform CI for signed launcher updates
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
# Signed updater fixtures must retain their exact committed bytes on every OS.
|
||||||
|
src-tauri/tests/fixtures/updater/* -text
|
||||||
@@ -54,13 +54,19 @@ jobs:
|
|||||||
- name: Install Linux desktop dependencies
|
- name: Install Linux desktop dependencies
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
# Ubuntu 22.04 keeps distro sources here; unrelated vendor repos are not needed.
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
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 ci
|
||||||
- run: npm test
|
- 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: cargo build --locked --release --manifest-path scripts/release-verifier/Cargo.toml
|
||||||
- run: python -m unittest discover -s scripts -p 'release_test.py' -v
|
- 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)
|
- name: Generate disposable CI key (never a production secret)
|
||||||
run: |
|
run: |
|
||||||
python scripts/release.py ci-key --directory "$RUNNER_TEMP/ci-updater"
|
python scripts/release.py ci-key --directory "$RUNNER_TEMP/ci-updater"
|
||||||
|
|||||||
@@ -27,11 +27,17 @@ jobs:
|
|||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
- name: Install Linux desktop dependencies
|
- name: Install Linux desktop dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
# Ubuntu 22.04 keeps distro sources here; unrelated vendor repos are not needed.
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
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 ci
|
||||||
- run: npm test
|
- run: npm test
|
||||||
- run: npm run build
|
- 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: cargo build --locked --release --manifest-path scripts/release-verifier/Cargo.toml
|
||||||
- run: python -m unittest discover -s scripts -p 'release_test.py' -v
|
- run: python -m unittest discover -s scripts -p 'release_test.py' -v
|
||||||
|
- run: cargo test --locked --manifest-path src-tauri/Cargo.toml
|
||||||
|
|||||||
@@ -115,8 +115,14 @@ jobs:
|
|||||||
- name: Install Linux desktop dependencies
|
- name: Install Linux desktop dependencies
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
# Ubuntu 22.04 keeps distro sources here; unrelated vendor repos are not needed.
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
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 ci
|
||||||
- run: npm test
|
- run: npm test
|
||||||
- run: cargo test --locked --manifest-path src-tauri/Cargo.toml
|
- run: cargo test --locked --manifest-path src-tauri/Cargo.toml
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ payload are in `/root/shacraft` on the ShaCraft host; see
|
|||||||
- `docs/game-trust-boundary.md` — the Mojang/NeoForge/Microsoft/Adoptium
|
- `docs/game-trust-boundary.md` — the Mojang/NeoForge/Microsoft/Adoptium
|
||||||
trust domains used to install and run the game itself.
|
trust domains used to install and run the game itself.
|
||||||
- `.github/workflows/check.yml` — push/PR UI checks and Linux Rust tests.
|
- `.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
|
- `.github/workflows/build.yml` — four-platform CI packages with disposable test
|
||||||
signing keys, explicitly unusable as production releases.
|
signing keys, explicitly unusable as production releases.
|
||||||
- Release workflows and `scripts/release.py` implement protected draft → publish
|
- Release workflows and `scripts/release.py` implement protected draft → publish
|
||||||
|
|||||||
@@ -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 test` covers asynchronous helpers and state transitions;
|
||||||
`npm run build` runs strict TypeScript before Vite. `cargo test --locked`
|
`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
|
The package workflow builds and checks Windows x64, Linux x64 and both macOS
|
||||||
architectures with disposable test signing keys. These artifacts cannot be
|
architectures with disposable test signing keys. These artifacts cannot be
|
||||||
published as production updater releases. Separate protected workflows assemble
|
published as production updater releases. Separate protected workflows assemble
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(os.environ.get("RELEASE_TEST_ROOT", Path(__file__).resolve().parents[1]))
|
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"
|
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):
|
def workflow_gate(workflow):
|
||||||
after = workflow.read_text().split(GATE, 1)[1]
|
after = workflow.read_text().split(GATE, 1)[1]
|
||||||
lines = after.split(" run: |\n", 1)[1].splitlines()
|
lines = after.split(" run: |\n", 1)[1].splitlines()
|
||||||
@@ -60,23 +88,52 @@ class ReleaseGateTests(unittest.TestCase):
|
|||||||
def run_gate(self, workflow, tag):
|
def run_gate(self, workflow, tag):
|
||||||
output = self.directory / (workflow.stem + "-output")
|
output = self.directory / (workflow.stem + "-output")
|
||||||
output.unlink(missing_ok=True)
|
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)),
|
env=dict(self.environment, RELEASE_TAG=tag, GITHUB_OUTPUT=str(output)),
|
||||||
capture_output=True, text=True, check=False)
|
capture_output=True, text=True, check=False)
|
||||||
return result, output.read_text() if output.exists() else ""
|
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):
|
def test_main_tag_emits_immutable_commit(self):
|
||||||
for workflow in WORKFLOWS:
|
for workflow in WORKFLOWS:
|
||||||
with self.subTest(workflow=workflow.name):
|
with self.subTest(workflow=workflow.name):
|
||||||
result, output = self.run_gate(workflow, "v0.2.0")
|
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")
|
self.assertEqual(output, f"commit={self.good}\n")
|
||||||
|
|
||||||
def test_unreviewed_tag_cannot_replace_its_own_ancestry_validator(self):
|
def test_unreviewed_tag_cannot_replace_its_own_ancestry_validator(self):
|
||||||
for workflow in WORKFLOWS:
|
for workflow in WORKFLOWS:
|
||||||
with self.subTest(workflow=workflow.name):
|
with self.subTest(workflow=workflow.name):
|
||||||
result, output = self.run_gate(workflow, "v9.9.9")
|
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.assertEqual(output, "")
|
||||||
self.assertFalse((self.directory / "untrusted-code-ran").exists())
|
self.assertFalse((self.directory / "untrusted-code-ran").exists())
|
||||||
|
|
||||||
@@ -84,15 +141,15 @@ class ReleaseGateTests(unittest.TestCase):
|
|||||||
for workflow in WORKFLOWS:
|
for workflow in WORKFLOWS:
|
||||||
with self.subTest(workflow=workflow.name):
|
with self.subTest(workflow=workflow.name):
|
||||||
result, output = self.run_gate(workflow, "v8.8.8")
|
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.assertEqual(output, "")
|
||||||
self.git("tag", "-f", "v0.2.0", self.good)
|
self.git("tag", "-f", "v0.2.0", self.good)
|
||||||
result, output = self.run_gate(workflow, "v0.2.0")
|
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.git("tag", "-f", "v0.2.0", self.bad)
|
||||||
self.assertEqual(output, f"commit={self.good}\n")
|
self.assertEqual(output, f"commit={self.good}\n")
|
||||||
result, fresh_output = self.run_gate(workflow, "v0.2.0")
|
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, "")
|
self.assertEqual(fresh_output, "")
|
||||||
|
|
||||||
def test_workflows_execute_candidate_code_only_after_gate_and_checkout_sha(self):
|
def test_workflows_execute_candidate_code_only_after_gate_and_checkout_sha(self):
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
#[ignore = "downloads the real pack/game and executes the official installer; needs network and Java 21"]
|
#[ignore = "downloads the real pack/game and executes the official installer; needs network and Java 21"]
|
||||||
fn live_cold_install_and_corruption_repair() {
|
fn live_cold_install_and_corruption_repair() {
|
||||||
let directory = std::env::temp_dir().join(format!(
|
let directory = std::env::temp_dir().canonicalize().unwrap().join(format!(
|
||||||
"shacraft-cold-install-{}",
|
"shacraft-cold-install-{}",
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
|||||||
@@ -459,8 +459,10 @@ mod tests {
|
|||||||
use crate::{java, mojang};
|
use crate::{java, mojang};
|
||||||
|
|
||||||
let client = Client::builder().build().unwrap();
|
let client = Client::builder().build().unwrap();
|
||||||
let root =
|
let root = std::env::temp_dir()
|
||||||
std::env::temp_dir().join(format!("shacraft-neoforge-pipeline-{}", std::process::id()));
|
.canonicalize()
|
||||||
|
.unwrap()
|
||||||
|
.join(format!("shacraft-neoforge-pipeline-{}", std::process::id()));
|
||||||
let game_dir = root.join("game");
|
let game_dir = root.join("game");
|
||||||
let cache_dir = root.join("cache");
|
let cache_dir = root.join("cache");
|
||||||
fs::create_dir_all(&cache_dir).unwrap();
|
fs::create_dir_all(&cache_dir).unwrap();
|
||||||
|
|||||||
@@ -494,7 +494,11 @@ mod tests {
|
|||||||
|
|
||||||
impl Fixture {
|
impl Fixture {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
let root = Stage::new(&std::env::temp_dir()).unwrap();
|
// macOS temporary directories may use the system /var -> /private/var
|
||||||
|
// alias. Canonicalize only this trusted fixture anchor; production
|
||||||
|
// safe_path must continue rejecting root/descendant substitutions.
|
||||||
|
let temporary_root = std::env::temp_dir().canonicalize().unwrap();
|
||||||
|
let root = Stage::new(&temporary_root).unwrap();
|
||||||
let game = root.0.join("game");
|
let game = root.0.join("game");
|
||||||
let cache = root.0.join("cache");
|
let cache = root.0.join("cache");
|
||||||
let input = jar_bytes(b"verified vanilla");
|
let input = jar_bytes(b"verified vanilla");
|
||||||
|
|||||||
@@ -506,6 +506,12 @@ mod tests {
|
|||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn trusted_temporary_root() -> std::path::PathBuf {
|
||||||
|
// Resolve only the OS-provided fixture anchor. The profile root, its
|
||||||
|
// parent and payload descendants retain their production link checks.
|
||||||
|
std::env::temp_dir().canonicalize().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn manifest(hash: String, size: u64) -> Manifest {
|
fn manifest(hash: String, size: u64) -> Manifest {
|
||||||
Manifest {
|
Manifest {
|
||||||
schema_version: 1,
|
schema_version: 1,
|
||||||
@@ -531,7 +537,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reports_missing_and_matching_files() {
|
fn reports_missing_and_matching_files() {
|
||||||
let root = std::env::temp_dir().join(format!(
|
let root = trusted_temporary_root().join(format!(
|
||||||
"shacraft-launcher-test-{}-{}",
|
"shacraft-launcher-test-{}-{}",
|
||||||
process::id(),
|
process::id(),
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
@@ -559,7 +565,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn edited_seed_files_remain_up_to_date() {
|
fn edited_seed_files_remain_up_to_date() {
|
||||||
let root = std::env::temp_dir().join(format!(
|
let root = trusted_temporary_root().join(format!(
|
||||||
"shacraft-seed-test-{}-{}",
|
"shacraft-seed-test-{}-{}",
|
||||||
process::id(),
|
process::id(),
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
@@ -577,7 +583,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn preserves_changed_seed_files_as_current() {
|
fn preserves_changed_seed_files_as_current() {
|
||||||
let root = std::env::temp_dir().join(format!(
|
let root = trusted_temporary_root().join(format!(
|
||||||
"shacraft-launcher-seed-test-{}-{}",
|
"shacraft-launcher-seed-test-{}-{}",
|
||||||
process::id(),
|
process::id(),
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
@@ -600,7 +606,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn refuses_linked_profile_directories() {
|
fn refuses_linked_profile_directories() {
|
||||||
use std::os::unix::fs::symlink;
|
use std::os::unix::fs::symlink;
|
||||||
let root = std::env::temp_dir().join(format!(
|
let root = trusted_temporary_root().join(format!(
|
||||||
"shacraft-link-test-{}-{}",
|
"shacraft-link-test-{}-{}",
|
||||||
process::id(),
|
process::id(),
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
|
|||||||
Reference in New Issue
Block a user