From a4e630fc1f2cdd03f8422eea120b51601fe5d9b7 Mon Sep 17 00:00:00 2001 From: smb Date: Sat, 5 Sep 2026 00:33:55 +0200 Subject: [PATCH 1/2] fix: close the quote that broke every native release build The compile-cache report I added in #21 opened a string for a command substitution and never closed it. Bash reads a whole script before it runs a line, so the unterminated quote was a parse error on every platform, not only on the Windows branch it sat in. bash says it plainly: unexpected EOF while looking for matching quote. The consequence was worse than a noisy log. The step runs with if: always(), so its failure failed the job after the bundle had been built, and "Upload unsigned bundle" was skipped. The v0.2.0 build spent twenty minutes producing a macOS bundle that was then thrown away, and no draft release could be created because publish had no artifacts. A test now parses every run: block of every JSON-shaped workflow with bash -n. It fails against the broken file with the same message bash gave, which was checked rather than assumed. ci.yml is real YAML and this project has no YAML parser, so it is skipped; release.yml, which holds the long scripts, is covered. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/release.yml | 2 +- tests/release/test_frozen_smoke.py | 42 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e7713d1..b55f370 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -127,7 +127,7 @@ "name": "Report compile cache statistics", "if": "always()", "shell": "bash", - "run": "set +e\nif [ \"$RUNNER_OS\" = \"Windows\" ]; then\n nuitka_cache=\"$HOME/AppData/Local/Nuitka/Nuitka/Cache\"\nelif [ \"$RUNNER_OS\" = \"macOS\" ]; then\n nuitka_cache=\"$HOME/Library/Caches/Nuitka\"\nelse\n nuitka_cache=\"$HOME/.cache/Nuitka\"\nfi\necho \"Nuitka cache: $nuitka_cache\"\ndu -sh \"$nuitka_cache\" 2>/dev/null || true\nif [ \"$RUNNER_OS\" = \"Windows\" ]; then\n ccache_bin=\"$(find \"$nuitka_cache\" -type f -iname 'ccache.exe' -print -quit 2>/dev/null || true)\n if [ -n \"$ccache_bin\" ]; then\n \"$ccache_bin\" -s || true\n else\n echo \"ccache not found\"\n fi\nelif command -v ccache >/dev/null; then\n ccache -s || true\nelse\n echo \"ccache not found\"\nfi\ntrue" + "run": "set +e\nif [ \"$RUNNER_OS\" = \"Windows\" ]; then\n nuitka_cache=\"$HOME/AppData/Local/Nuitka/Nuitka/Cache\"\nelif [ \"$RUNNER_OS\" = \"macOS\" ]; then\n nuitka_cache=\"$HOME/Library/Caches/Nuitka\"\nelse\n nuitka_cache=\"$HOME/.cache/Nuitka\"\nfi\necho \"Nuitka cache: $nuitka_cache\"\ndu -sh \"$nuitka_cache\" 2>/dev/null || true\nif [ \"$RUNNER_OS\" = \"Windows\" ]; then\n ccache_bin=\"$(find \"$nuitka_cache\" -type f -iname 'ccache.exe' -print -quit 2>/dev/null || true)\"\n if [ -n \"$ccache_bin\" ]; then\n \"$ccache_bin\" -s || true\n else\n echo \"ccache not found\"\n fi\nelif command -v ccache >/dev/null; then\n ccache -s || true\nelse\n echo \"ccache not found\"\nfi\ntrue" }, { "name": "Save media build stack cache", diff --git a/tests/release/test_frozen_smoke.py b/tests/release/test_frozen_smoke.py index 3c8477f..134d67f 100644 --- a/tests/release/test_frozen_smoke.py +++ b/tests/release/test_frozen_smoke.py @@ -904,3 +904,45 @@ def test_release_packaging_keeps_lgpl_sources_out_of_the_application_zip( ).stdout.split() assert "MatteLoop.dist/matteloop.exe" in archived assert not [name for name in archived if "sources" in name] + + +def test_every_workflow_shell_script_parses() -> None: + """A `run:` block that cannot be parsed fails its step on every platform. + + bash reads the whole script before it runs a line, so an unterminated + quote in a branch that only Windows takes still kills the step on macOS. + That shipped once: the compile-cache report lost the closing quote of a + command substitution, every native build failed after producing its + bundle, and the artifact upload was skipped. + """ + workflows = sorted((REPOSITORY_ROOT / ".github" / "workflows").glob("*.yml")) + assert workflows, "no workflows found" + checked_any = False + for path in workflows: + try: + workflow = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + # Only release.yml is written as JSON, which YAML accepts. The + # others need a YAML parser this project does not depend on. + continue + checked_any = True + for job_name, job in workflow["jobs"].items(): + for index, step in enumerate(job.get("steps", [])): + script = step.get("run") + if script is None: + continue + shell = step.get("shell", "bash") + if shell not in {"bash", "sh"}: + continue + checked = subprocess.run( + [shell, "-n"], + input=script, + capture_output=True, + text=True, + check=False, + ) + assert checked.returncode == 0, ( + f"{path.name} {job_name} step {index} " + f"({step.get('name', 'unnamed')}): {checked.stderr.strip()}" + ) + assert checked_any, "no JSON-shaped workflow was checked" From a0d8dea7d7b4938632cd688cdfcbc85c38a1091f Mon Sep 17 00:00:00 2001 From: smb Date: Sat, 5 Sep 2026 00:34:18 +0200 Subject: [PATCH 2/2] style: format the workflow parser test Co-Authored-By: Claude Opus 5 (1M context) --- tests/release/test_frozen_smoke.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/tests/release/test_frozen_smoke.py b/tests/release/test_frozen_smoke.py index 134d67f..1ee79bd 100644 --- a/tests/release/test_frozen_smoke.py +++ b/tests/release/test_frozen_smoke.py @@ -82,12 +82,15 @@ def test_native_smoke_resolves_every_v1_session_class_without_weights( catalog = ModelCatalog.load_resource() classes = load_rembg_session_classes() for model_id in V1_MODEL_IDS: - assert _resolve_rembg_session_class( - model_id, - classes, - catalog.rembg_version, - catalog.rembg_version, - ) is not None + assert ( + _resolve_rembg_session_class( + model_id, + classes, + catalog.rembg_version, + catalog.rembg_version, + ) + is not None + ) def test_smoke_result_contract_is_typed_and_immutable() -> None: @@ -554,23 +557,18 @@ def test_native_bundle_includes_project_license_notices() -> None: assert "--include-data-files=legal/GPL-3.0.txt=GPL-3.0.txt" in args assert "--include-data-files=legal/LGPL-3.0.txt=LGPL-3.0.txt" in args assert ( - "--include-data-files=legal/QT-PYSIDE-LGPL-NOTICE.md=" - "QT-PYSIDE-LGPL-NOTICE.md" + "--include-data-files=legal/QT-PYSIDE-LGPL-NOTICE.md=QT-PYSIDE-LGPL-NOTICE.md" ) in args assert "--include-data-files=legal/RELINK.md=RELINK.md" in args def test_distributed_legal_files_preserve_the_measured_macos_floor() -> None: relink = (REPOSITORY_ROOT / "legal" / "RELINK.md").read_text(encoding="utf-8") - notices = (REPOSITORY_ROOT / "THIRD_PARTY_NOTICES.md").read_text( - encoding="utf-8" - ) + notices = (REPOSITORY_ROOT / "THIRD_PARTY_NOTICES.md").read_text(encoding="utf-8") for document in (" ".join(relink.split()), " ".join(notices.split())): assert "complete app requires macOS 15" in document - assert ( - "custom media FFmpeg/libwebp dylibs retain a 13.0 minimum" in document - ) + assert "custom media FFmpeg/libwebp dylibs retain a 13.0 minimum" in document assert "not been launched on an actual macOS 15 host" in document assert "delivered app targets macOS 13.0" not in document assert "actual macOS 13 host" not in document