From dfcffb47b8f3951e588a40a076be5d62a483900a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 10:43:58 +0000 Subject: [PATCH 1/8] Fix source-check CI: let bubblewrap set up loopback, isolate Codex test ubuntu-24.04 runners set apparmor_restrict_unprivileged_userns=1, so bwrap --unshare-net dies on RTM_NEWADDR before any sandbox probe runs. Lift that restriction on the disposable runner. The Codex missing-key case now stubs resolve_executable the same way the rest of the test already did, so hosts without the CLI still exercise "not configured". Co-authored-by: Bob Corbin --- .github/workflows/build-iso.yml | 14 ++++++++++++++ .../shadowfetch-missions/tests/test_missions.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-iso.yml b/.github/workflows/build-iso.yml index 75a5f4f..0b726b6 100644 --- a/.github/workflows/build-iso.yml +++ b/.github/workflows/build-iso.yml @@ -1,3 +1,6 @@ +# Named build-iso.yml for history. This workflow is source and package checks +# only; it does not build an ISO. The signed ISO is published from the +# authorized Linux publisher. See RELEASE-4.0.0.md. name: Source and package checks on: @@ -26,6 +29,17 @@ jobs: run: | sudo apt-get update sudo apt-get install -y build-essential debhelper dh-python devscripts fakeroot python3 python3-yaml python3-pyqt6 python3-dbus python3-psutil shellcheck desktop-file-utils bubblewrap ffmpeg podman + # ubuntu-24.04 sets kernel.apparmor_restrict_unprivileged_userns=1. + # bubblewrap can still create a user+net namespace, but configuring + # loopback inside it fails with RTM_NEWADDR EPERM, so every empirical + # sandbox test dies before the probe runs. Runners are disposable; + # lift the restriction for this job instead of skipping the suite. + - name: Allow bubblewrap network namespaces + run: | + if [ "$(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>/dev/null || echo 0)" = "1" ]; then + sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + fi + bwrap --ro-bind / / --dev /dev --unshare-user --unshare-net --die-with-parent -- /bin/true - name: Test behavior run: make test - name: Validate source syntax and desktop entries diff --git a/packages/shadowfetch-missions/tests/test_missions.py b/packages/shadowfetch-missions/tests/test_missions.py index 6aeea9b..fdeb43f 100644 --- a/packages/shadowfetch-missions/tests/test_missions.py +++ b/packages/shadowfetch-missions/tests/test_missions.py @@ -347,7 +347,7 @@ def cli(command, label, **kwargs): def test_codex_incomplete_turn_and_missing_key_refuse_success(self): executor = m.Executor(self.store, self.create()) - with patch.dict(os.environ, {"CODEX_API_KEY": "", "OPENAI_API_KEY": ""}), patch.object(executor, "run_process", side_effect=AssertionError("No call without API key")): + with patch.dict(os.environ, {"CODEX_API_KEY": "", "OPENAI_API_KEY": ""}), patch.object(codex_adapter, "resolve_executable", return_value="/usr/bin/true"), patch.object(sf_providers, "declared_executables", return_value={"/usr/bin/true"}), patch.object(executor, "run_process", side_effect=AssertionError("No call without API key")): with self.assertRaisesRegex(m.MissionError, "not configured"): executor.agent_turn("task") log = executor.directory / "failed.jsonl" From 5f73dbac9d2e5ee065c9eefdbf0d8e4541509329 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 10:54:19 +0000 Subject: [PATCH 2/8] Let Firebreak live tests skip without a systemd user bus Firebreak launches via systemd-run --user. Hosts without a user session (this agent VM, and GitHub runners until linger is enabled) fail the shell containment script on a dbus error before any namespace check. Skip when the user bus is missing, matching the Python live-sandbox gate, and start a lingering user session on the disposable CI runner so the script still runs there. Co-authored-by: Bob Corbin --- .github/workflows/build-iso.yml | 17 ++++++++++++++++- .../tests/test_firebreak.sh | 7 +++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-iso.yml b/.github/workflows/build-iso.yml index 0b726b6..e6d3a27 100644 --- a/.github/workflows/build-iso.yml +++ b/.github/workflows/build-iso.yml @@ -28,7 +28,7 @@ jobs: - name: Install source and package test dependencies run: | sudo apt-get update - sudo apt-get install -y build-essential debhelper dh-python devscripts fakeroot python3 python3-yaml python3-pyqt6 python3-dbus python3-psutil shellcheck desktop-file-utils bubblewrap ffmpeg podman + sudo apt-get install -y build-essential debhelper dh-python devscripts fakeroot python3 python3-yaml python3-pyqt6 python3-dbus python3-psutil shellcheck desktop-file-utils bubblewrap ffmpeg podman dbus-user-session # ubuntu-24.04 sets kernel.apparmor_restrict_unprivileged_userns=1. # bubblewrap can still create a user+net namespace, but configuring # loopback inside it fails with RTM_NEWADDR EPERM, so every empirical @@ -40,6 +40,21 @@ jobs: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 fi bwrap --ro-bind / / --dev /dev --unshare-user --unshare-net --die-with-parent -- /bin/true + # Firebreak wraps every live sandbox in systemd-run --user. Hosted + # runners have systemd as PID 1 but no lingering user session, so the + # user bus is missing unless we start one. + - name: Start systemd user session for Firebreak + run: | + sudo loginctl enable-linger "$USER" + sudo systemctl start "user@$(id -u).service" + echo "XDG_RUNTIME_DIR=/run/user/$(id -u)" >> "$GITHUB_ENV" + echo "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus" >> "$GITHUB_ENV" + for _ in $(seq 1 40); do + [ -S "/run/user/$(id -u)/bus" ] && break + sleep 0.25 + done + test -S "/run/user/$(id -u)/bus" + systemd-run --user --scope --quiet --collect -- /bin/true - name: Test behavior run: make test - name: Validate source syntax and desktop entries diff --git a/packages/shadowfetch-fireline/tests/test_firebreak.sh b/packages/shadowfetch-fireline/tests/test_firebreak.sh index f518635..f8d182f 100755 --- a/packages/shadowfetch-fireline/tests/test_firebreak.sh +++ b/packages/shadowfetch-fireline/tests/test_firebreak.sh @@ -3,6 +3,13 @@ set -euo pipefail FB=${SHADOWFETCH_FIREBREAK_TEST_BIN:-shadowfetch-firebreak} CP=${SHADOWFETCH_CHECKPOINT_BIN:-shadowfetch-checkpoint} +# Firebreak launches via systemd-run --user. A host without a user bus cannot +# exercise that path; the Python live-sandbox tests skip on the same check. +# Missing namespaces remain a failure once the bus is present. +if [[ ! -S "/run/user/$(id -u)/bus" ]]; then + echo "SKIP live Firebreak containment: no systemd user bus at /run/user/$(id -u)/bus" + exit 0 +fi fixture=$(mktemp -d) trap 'rm -rf "$fixture"' EXIT export SHADOWFETCH_AGENT_WORKSPACES="$fixture/Workspaces" From 4a8669a758a506991011bd47606be4a631dab434 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 10:56:46 +0000 Subject: [PATCH 3/8] Unblock the rest of make test after the missions suite Once missions pass, the same workflow still dies later: - drift_gate treated git SHAs on the ISO readme as a second signing key because they sit next to an OpenPGP fingerprint line - tools/tests require qemu-img and qemu-system-x86_64 on the runner - the Firebreak shell script must probe systemd-run --user, not merely the presence of a leftover user bus socket Co-authored-by: Bob Corbin --- .github/workflows/build-iso.yml | 2 +- .../tests/test_firebreak.sh | 11 ++++++----- tools/drift_gate.py | 5 +++++ tools/tests/test_drift_gate.py | 17 +++++++++++++++++ 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-iso.yml b/.github/workflows/build-iso.yml index e6d3a27..d3a7af3 100644 --- a/.github/workflows/build-iso.yml +++ b/.github/workflows/build-iso.yml @@ -28,7 +28,7 @@ jobs: - name: Install source and package test dependencies run: | sudo apt-get update - sudo apt-get install -y build-essential debhelper dh-python devscripts fakeroot python3 python3-yaml python3-pyqt6 python3-dbus python3-psutil shellcheck desktop-file-utils bubblewrap ffmpeg podman dbus-user-session + sudo apt-get install -y build-essential debhelper dh-python devscripts fakeroot python3 python3-yaml python3-pyqt6 python3-dbus python3-psutil shellcheck desktop-file-utils bubblewrap ffmpeg podman dbus-user-session qemu-utils qemu-system-x86 # ubuntu-24.04 sets kernel.apparmor_restrict_unprivileged_userns=1. # bubblewrap can still create a user+net namespace, but configuring # loopback inside it fails with RTM_NEWADDR EPERM, so every empirical diff --git a/packages/shadowfetch-fireline/tests/test_firebreak.sh b/packages/shadowfetch-fireline/tests/test_firebreak.sh index f8d182f..b7444c7 100755 --- a/packages/shadowfetch-fireline/tests/test_firebreak.sh +++ b/packages/shadowfetch-fireline/tests/test_firebreak.sh @@ -3,11 +3,12 @@ set -euo pipefail FB=${SHADOWFETCH_FIREBREAK_TEST_BIN:-shadowfetch-firebreak} CP=${SHADOWFETCH_CHECKPOINT_BIN:-shadowfetch-checkpoint} -# Firebreak launches via systemd-run --user. A host without a user bus cannot -# exercise that path; the Python live-sandbox tests skip on the same check. -# Missing namespaces remain a failure once the bus is present. -if [[ ! -S "/run/user/$(id -u)/bus" ]]; then - echo "SKIP live Firebreak containment: no systemd user bus at /run/user/$(id -u)/bus" +# Firebreak launches via systemd-run --user. A socket at /run/user/$UID/bus +# is not enough -- a leftover dbus-daemon without a user manager still fails +# the launch. Probe the real wrapper. Missing namespaces remain a failure +# once systemd-run --user works (the Python live-sandbox tests skip too). +if ! systemd-run --user --scope --quiet --collect -- /bin/true >/dev/null 2>&1; then + echo "SKIP live Firebreak containment: systemd-run --user is not available" exit 0 fi fixture=$(mktemp -d) diff --git a/tools/drift_gate.py b/tools/drift_gate.py index 5c9d8a5..d67a413 100644 --- a/tools/drift_gate.py +++ b/tools/drift_gate.py @@ -364,6 +364,11 @@ def check_fingerprint(truth: dict) -> list[Finding]: if field is not None: if not _KEY_CONTEXT.search(field.group(1)): continue # e.g. "source_commit", "release_build", "URL" + elif re.search(r"(?i)\bcommit\b", line) and not _KEY_CONTEXT.search(line): + # Prose like "Source commit (ISO): " next to an OpenPGP + # fingerprint line: the 3-line window would otherwise inherit + # the word "fingerprint" and treat a git SHA as a signing key. + continue else: window = "\n".join(lines[max(0, number - 3):number + 1]) if not _KEY_CONTEXT.search(window): diff --git a/tools/tests/test_drift_gate.py b/tools/tests/test_drift_gate.py index d5d9ac5..2b97fc6 100644 --- a/tools/tests/test_drift_gate.py +++ b/tools/tests/test_drift_gate.py @@ -260,6 +260,23 @@ def test_a_commit_sha_is_not_mistaken_for_a_key(self): r'"source_commit":\s*"[0-9a-f]{40}"') self.assertEqual([], drifts(drift_gate.check_fingerprint(TRUTH))) + def test_a_prose_commit_sha_beside_a_fingerprint_is_not_a_key(self): + """sf41-ia-readme.txt names the signing key, then two git SHAs. + + The SHAs sit inside the three-line fingerprint window, so a sweep that + only looks at nearby labels would treat them as a second key.""" + with sandbox(*FINGERPRINT_RELS) as fake: + planted = fake / "sf41-ia-readme.txt" + planted.write_text( + "OpenPGP fingerprint: {fp}\n" + "Source commit (ISO): {sha1}\n" + "Release-tooling commit: {sha2}\n".format( + fp=TRUTH["signing"]["fingerprint"], + sha1="78ee38ceac0ff989d596e5a5e0b97aac17c3b936", + sha2="aa8fd1b22e8e3c8a098a28e43fd6b156fbb74b56"), + encoding="utf-8") + self.assertEqual([], drifts(drift_gate.check_fingerprint(TRUTH))) + def test_a_third_party_key_must_be_named(self): with sandbox(*FINGERPRINT_RELS) as fake: planted = fake / "packages/anything/vendor/provenance.json" From 71d67991ee8b9e953d949568f5cd19236145aa3e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 11:27:37 +0000 Subject: [PATCH 4/8] Skip lifecycle attacks when systemd-run --user cannot launch Those four attacks already skip when bwrap/systemd-run/ffmpeg are absent. On a host where the binaries exist but systemd-run --user cannot start a scope (no user manager), they used to fail as harness errors. Probe the wrapper the same way the Firebreak shell test does. Co-authored-by: Bob Corbin --- tools/attacks/attack_lifecycle.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tools/attacks/attack_lifecycle.py b/tools/attacks/attack_lifecycle.py index ab9d7cd..af427b2 100755 --- a/tools/attacks/attack_lifecycle.py +++ b/tools/attacks/attack_lifecycle.py @@ -277,7 +277,23 @@ def chain_summary(store): def missing_sandbox_tools(): - return [name for name in SANDBOX_TOOLS if shutil.which(name) is None] + missing = [name for name in SANDBOX_TOOLS if shutil.which(name) is None] + if missing: + return missing + # systemd-run is on PATH on this host, but Firebreak launches via + # systemd-run --user. A leftover dbus socket without a user manager + # still fails that wrapper; treat it as missing so these attacks skip + # instead of reporting a harness error as a product FAIL. + try: + done = subprocess.run( + ["systemd-run", "--user", "--scope", "--quiet", "--collect", + "--", "/bin/true"], + capture_output=True, timeout=10) + except (OSError, subprocess.SubprocessError): + return ["systemd-run --user"] + if done.returncode != 0: + return ["systemd-run --user"] + return [] def lines(*parts): From 81f36f122167af1d4bb188d3d65b5ada606a24ee Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 11:30:09 +0000 Subject: [PATCH 5/8] Skip live egress probes without slirp/user systemd and fix domain ENGINE path The allowlist attack treated a Firebreak launch failure (no RESULT) as a containment failure. Skip that measurement when systemd-run --user cannot start the probe. Point attack_domain at the repo missions tree instead of a hardcoded 4.0.0 home path. Install slirp4netns on CI so hosted runners can run the live egress case. Co-authored-by: Bob Corbin --- .github/workflows/build-iso.yml | 2 +- tools/attacks/attack_concurrency.py | 31 ++++++++++++++++++++++++----- tools/attacks/attack_domain.py | 2 +- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-iso.yml b/.github/workflows/build-iso.yml index d3a7af3..c90a473 100644 --- a/.github/workflows/build-iso.yml +++ b/.github/workflows/build-iso.yml @@ -28,7 +28,7 @@ jobs: - name: Install source and package test dependencies run: | sudo apt-get update - sudo apt-get install -y build-essential debhelper dh-python devscripts fakeroot python3 python3-yaml python3-pyqt6 python3-dbus python3-psutil shellcheck desktop-file-utils bubblewrap ffmpeg podman dbus-user-session qemu-utils qemu-system-x86 + sudo apt-get install -y build-essential debhelper dh-python devscripts fakeroot python3 python3-yaml python3-pyqt6 python3-dbus python3-psutil shellcheck desktop-file-utils bubblewrap ffmpeg podman dbus-user-session qemu-utils qemu-system-x86 slirp4netns # ubuntu-24.04 sets kernel.apparmor_restrict_unprivileged_userns=1. # bubblewrap can still create a user+net namespace, but configuring # loopback inside it fails with RTM_NEWADDR EPERM, so every empirical diff --git a/tools/attacks/attack_concurrency.py b/tools/attacks/attack_concurrency.py index ccaa730..29f8c1e 100755 --- a/tools/attacks/attack_concurrency.py +++ b/tools/attacks/attack_concurrency.py @@ -1309,6 +1309,18 @@ def accept(): egress = (record or {}).get("enforcement", {}).get("egress_allowlist", {}) network = (record or {}).get("enforcement", {}).get("network", {}) + if not reached: + report(name, + "an egress allowlist naming one host does not let the sandbox reach " + "the host's loopback, and any destination it CAN still reach is " + "described honestly rather than as enforced", + f"$ shadowfetch-firebreak run --net allow --egress-host one.one.one.one " + f"-- python3 (rc={proc.returncode}, no RESULT line)\n" + f"stderr={(proc.stderr or '')[-400:]}", + None, + "SKIPPED: Firebreak did not execute the probe. This measurement " + "needs systemd-run --user and slirp4netns.") + return loopback_contained = (reached.get("loopback") != "REACHED" and not received) # The allowlist named one.one.one.one and nothing else. A destination it # never named must not be reachable, and the one it did name must be: a @@ -1418,7 +1430,14 @@ def main(): def report(name, expected, observed, passed, note=""): rows.append({"attack": name, "expected": expected, "observed": observed, - "passed": bool(passed), "note": note}) + "passed": passed, "note": note}) + + def label(passed): + if passed is True: + return "PASS" + if passed is None: + return "SKIP" + return "FAIL" run(report) width = max(len(row["attack"]) for row in rows) @@ -1427,19 +1446,21 @@ def report(name, expected, observed, passed, note=""): print("=" * 78) for row in rows: print() - print(f"{'PASS' if row['passed'] else 'FAIL'} {row['attack']}") + print(f"{label(row['passed'])} {row['attack']}") print(f" EXPECTED {row['expected']}") for index, line in enumerate(row["observed"].splitlines() or [""]): print(f" {'OBSERVED ' if index == 0 else ' '}{line}") if row["note"]: print(f" NOTE {row['note']}") - failed = [row["attack"] for row in rows if not row["passed"]] + failed = [row["attack"] for row in rows if row["passed"] is False] print() print("-" * 78) for row in rows: - print(f" {'PASS' if row['passed'] else 'FAIL'} {row['attack']:<{width}}") + print(f" {label(row['passed'])} {row['attack']:<{width}}") print("-" * 78) - print(f"{len(rows) - len(failed)} passed, {len(failed)} FAILED") + skipped = sum(1 for row in rows if row["passed"] is None) + print(f"{len(rows) - len(failed) - skipped} passed, {len(failed)} FAILED" + + (f", {skipped} SKIPPED" if skipped else "")) if failed: print("FAILED: " + ", ".join(failed)) return 1 if failed else 0 diff --git a/tools/attacks/attack_domain.py b/tools/attacks/attack_domain.py index f5fa4bf..1336d25 100755 --- a/tools/attacks/attack_domain.py +++ b/tools/attacks/attack_domain.py @@ -8,7 +8,7 @@ """ import json, os, sqlite3, sys, tempfile, time from pathlib import Path -ENGINE = Path.home() / "projects/shadowfetch-4.0.0/packages/shadowfetch-missions/data/usr/lib/shadowfetch/missions" +ENGINE = Path(__file__).resolve().parents[2] / "packages/shadowfetch-missions/data/usr/lib/shadowfetch/missions" sys.path.insert(0, str(ENGINE)) R0 = tempfile.mkdtemp(prefix="stgA-") os.environ["SHADOWFETCH_MISSIONS_STATE"] = R0 From 2f2d639fc05dcaa79c69d79c9fc8a9255e7752c7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 11:39:40 +0000 Subject: [PATCH 6/8] Do not treat GHA's /home/runner/work as a skip directory product_files() skipped any absolute path whose parts included work, build, tests, etc. Hosted runners check out under /home/runner/work, so the fireproof pin lists compared against an empty tree after missions went green. Match skip names against the path relative to packages/ instead. Co-authored-by: Bob Corbin --- .../tests/test_single_update_authority.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/shadowfetch-fireproof/tests/test_single_update_authority.py b/packages/shadowfetch-fireproof/tests/test_single_update_authority.py index 7100e77..0732ed9 100644 --- a/packages/shadowfetch-fireproof/tests/test_single_update_authority.py +++ b/packages/shadowfetch-fireproof/tests/test_single_update_authority.py @@ -86,11 +86,17 @@ def code_only(text): def product_files(): - """Every shipped payload file under packages/, build copies excluded.""" + """Every shipped payload file under packages/, build copies excluded. + + Skip names are matched against the path relative to packages/, not the + absolute path. GitHub Actions checks out under /home/runner/work/, + and treating that ancestor `work` as a skip directory emptied the scan + so the pin lists compared against nothing. + """ for path in PACKAGES.rglob("*"): if not path.is_file(): continue - if any(part in SKIP_PARTS for part in path.parts): + if any(part in SKIP_PARTS for part in path.relative_to(PACKAGES).parts): continue yield path @@ -221,6 +227,16 @@ def product_code(): continue +class TestProductScanIsRelative(unittest.TestCase): + def test_an_ancestor_named_work_does_not_empty_the_scan(self): + """GitHub Actions checks out under /home/runner/work//.""" + files = list(product_files()) + self.assertTrue(files, "product_files() scanned an empty tree") + self.assertTrue( + any(path.name == "85fireproof" for path in files), + "the one APT::Periodic file was skipped with the rest") + + class TestOnlyOneUpdater(unittest.TestCase): def test_the_shim_contains_no_update_mechanism_of_its_own(self): body = code_only(SHIM.read_text()) From 8e35ceeff31822494767701f2049eb9f89727cf4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 11:46:59 +0000 Subject: [PATCH 7/8] Fetch v4.0.0 so phoenix regression proofs can git-show the pre-fix scripts A shallow Actions checkout has no tags. After fireproof went green, five phoenix tests ERRORed on `git show v4.0.0:packages/shadowfetch-phoenix/...`. Fetch that tag so the published-vs-fixed comparison can run. Co-authored-by: Bob Corbin --- .github/workflows/build-iso.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build-iso.yml b/.github/workflows/build-iso.yml index c90a473..e44c620 100644 --- a/.github/workflows/build-iso.yml +++ b/.github/workflows/build-iso.yml @@ -25,6 +25,12 @@ jobs: PYTHONDONTWRITEBYTECODE: '1' steps: - uses: actions/checkout@v4 + # Phoenix restore/report regression proofs compare against the scripts + # as published in v4.0.0 (`git show v4.0.0:...`). A shallow checkout + # has no tags, so those tests ERROR instead of measuring the pre-fix + # behaviour. + - name: Fetch v4.0.0 for phoenix regression proofs + run: git fetch --depth=1 origin tag v4.0.0 - name: Install source and package test dependencies run: | sudo apt-get update From c9d22c5b496b93a7a8ae57c60ee41972bd6fb178 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 11:57:00 +0000 Subject: [PATCH 8/8] Skip deliberately invalid test JSON in the source-syntax gate make test is green on hosted runners. The next step json.loads every tracked .json file, including missions fixture broken-not-json.json, which exists to prove a provider manifest that never parses is refused. Do not require test fixtures to be valid JSON. Co-authored-by: Bob Corbin --- .github/workflows/build-iso.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-iso.yml b/.github/workflows/build-iso.yml index e44c620..a6cc1e9 100644 --- a/.github/workflows/build-iso.yml +++ b/.github/workflows/build-iso.yml @@ -79,7 +79,8 @@ jobs: first = text.splitlines()[0] if text.splitlines() else '' if p.suffix == '.py' or first.startswith('#!') and 'python' in first: ast.parse(text, filename=str(p)) - if p.suffix == '.json': + # Test fixtures may be deliberately invalid (e.g. broken-not-json.json). + if p.suffix == '.json' and 'tests' not in p.parts: json.loads(text) if p.suffix == '.desktop' and ('applications' in p.parts or 'autostart' in p.parts): subprocess.run(['desktop-file-validate', str(p)], check=True)