diff --git a/.github/workflows/launcher.yml b/.github/workflows/launcher.yml index 24179a54..65895a35 100644 --- a/.github/workflows/launcher.yml +++ b/.github/workflows/launcher.yml @@ -5,11 +5,11 @@ on: hardware_tag: description: Published hardware release containing the wheel and component bundle required: true - default: v0.1.9 + default: v0.1.10 cli_tag: description: Published compatible CLI release required: true - default: v0.1.8 + default: v0.1.10 minimal_tag: description: Published Minimal Linux release (supplies the Alpine base and one compatible root) required: true @@ -162,6 +162,19 @@ jobs: "${installations[@]}" --release "${HARDWARE_TAG#v}" \ --public-base-url "https://github.com/OrchardLinux/hardware/releases/download/$HARDWARE_TAG" \ --output out/launcher + # Require the paired host firmware recipe, not only the boot protocol. + python - <<'PY' + import os, re + from pathlib import Path + minimum = os.environ['CLI_TAG'].removeprefix('v') + for name in ('boot.toml', 'j71b-launcher.boot.toml'): + path = Path('out/launcher') / name + text, count = re.subn(r'^min_orchard = "[^"]+"$', + 'min_orchard = "' + minimum + '"', + path.read_text(), flags=re.MULTILINE) + assert count == 1, 'Expected one launcher CLI requirement' + path.write_text(text) + PY mkdir -p out/publication cp out/launcher/recovery.bin out/launcher/pongo.bin out/launcher/j71b-launcher.boot.toml \ out/launcher/launch.json out/publication/ diff --git a/docs/ci.md b/docs/ci.md index 73e257f1..f9c73474 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -56,7 +56,7 @@ Run `.github/workflows/launcher.yml` manually after the image releases exist. Se - A Minimal Linux tag (supplies its signed Alpine base and compatible root). - Optionally an Omarchy A tag, built with the same kernel/modules/runtime. -The producer validates component inventories, quiet-patch receipts, root streams and kernel/runtime compatibility. It refuses incompatible roots rather than advertising unsupported installations. +The producer validates component inventories, quiet-patch receipts, root streams and kernel/runtime compatibility. It refuses incompatible roots rather than advertising unsupported installations. For the J71b update, rebuild both Minimal Linux and Omarchy A with hardware/CLI v0.1.10 and include both new roots. Before checksumming and attesting, the workflow sets both launcher manifests' `min_orchard` to the selected CLI tag so older host firmware recipes are rejected before preparation. By default the workflow only uploads `shared-launcher-assets`. Explicitly enable `attach_assets` to attach new assets to the selected hardware release. This never overwrites existing assets, changes the latest-release selection, or publishes a draft. The wheel/component checksums stay immutable; launcher assets have a separate `launcher.SHA256SUMS`. diff --git a/tests/test_quiet_boot.py b/tests/test_quiet_boot.py index 703d7db0..c57dfbe3 100644 --- a/tests/test_quiet_boot.py +++ b/tests/test_quiet_boot.py @@ -1,6 +1,10 @@ """Shared discovery must not accidentally inherit distro artwork.""" import hashlib import json +import os +import subprocess +import sys +import textwrap from pathlib import Path import tempfile import unittest @@ -10,6 +14,19 @@ class QuietBoot(unittest.TestCase): + def test_published_launcher_requires_paired_cli(self): + workflow = (Path(__file__).resolve().parents[1] / '.github/workflows/launcher.yml').read_text() + script = textwrap.dedent(workflow.split(" python - <<'PY'\n", 1)[1].split(' PY\n', 1)[0]) + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) / 'out/launcher' + output.mkdir(parents=True) + for name in ('boot.toml', 'j71b-launcher.boot.toml'): + (output / name).write_text('min_orchard = "0.1.8"\nprotocol = 1\n') + subprocess.run([sys.executable, '-c', script], cwd=directory, + env={**os.environ, 'CLI_TAG': 'v0.1.10'}, check=True) + for path in output.iterdir(): + self.assertEqual(path.read_text(), 'min_orchard = "0.1.10"\nprotocol = 1\n') + def test_receipt_binds_both_components_and_current_patches(self): here = Path(orchard_hardware.__file__).parent / 'boot/quiet' files = {role: {'bytes': 1, 'sha256': role * 8} for role in ('loader', 'pongo')}