From 9a6a3b8872a6e1205de9c4b4b13c311883b2f583 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:42:55 +0900 Subject: [PATCH 01/16] test(release): reproduce desktop version identity drift --- .../tests/test_release_version_identity.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 services/analysis-engine/tests/test_release_version_identity.py diff --git a/services/analysis-engine/tests/test_release_version_identity.py b/services/analysis-engine/tests/test_release_version_identity.py new file mode 100644 index 000000000..ce06ed771 --- /dev/null +++ b/services/analysis-engine/tests/test_release_version_identity.py @@ -0,0 +1,66 @@ +"""Release identity contracts for the packaged BandScope desktop application.""" + +from __future__ import annotations + +import json +import tomllib +from pathlib import Path + +_REPOSITORY_ROOT = Path(__file__).resolve().parents[3] + + +def _read_json(relative_path: str) -> dict[str, object]: + """Return one checked-in JSON document as an object.""" + document = json.loads((_REPOSITORY_ROOT / relative_path).read_text(encoding="utf-8")) + assert isinstance(document, dict) + return document + + +def _read_toml(relative_path: str) -> dict[str, object]: + """Return one checked-in TOML document as an object.""" + with (_REPOSITORY_ROOT / relative_path).open("rb") as stream: + document = tomllib.load(stream) + assert isinstance(document, dict) + return document + + +def test_packaged_desktop_uses_authoritative_release_version() -> None: + """Reject release metadata that drifts from the repository VERSION authority.""" + expected = (_REPOSITORY_ROOT / "VERSION").read_text(encoding="utf-8").strip() + assert expected + + root_package = _read_json("package.json") + desktop_package = _read_json("apps/desktop/package.json") + npm_lock = _read_json("package-lock.json") + tauri_config = _read_json("apps/desktop/src-tauri/tauri.conf.json") + cargo_manifest = _read_toml("apps/desktop/src-tauri/Cargo.toml") + cargo_lock = _read_toml("apps/desktop/src-tauri/Cargo.lock") + + npm_packages = npm_lock.get("packages") + assert isinstance(npm_packages, dict) + npm_root = npm_packages.get("") + npm_desktop = npm_packages.get("apps/desktop") + assert isinstance(npm_root, dict) + assert isinstance(npm_desktop, dict) + + cargo_package = cargo_manifest.get("package") + assert isinstance(cargo_package, dict) + locked_desktop = [ + package + for package in cargo_lock.get("package", []) + if isinstance(package, dict) and package.get("name") == "bandscope-desktop" + ] + assert len(locked_desktop) == 1 + + observed = { + "package.json": root_package.get("version"), + "apps/desktop/package.json": desktop_package.get("version"), + "package-lock.json": npm_lock.get("version"), + "package-lock.json#packages['']": npm_root.get("version"), + "package-lock.json#packages['apps/desktop']": npm_desktop.get("version"), + "apps/desktop/src-tauri/tauri.conf.json": tauri_config.get("version"), + "apps/desktop/src-tauri/Cargo.toml": cargo_package.get("version"), + "apps/desktop/src-tauri/Cargo.lock#bandscope-desktop": locked_desktop[0].get("version"), + } + + assert observed == dict.fromkeys(observed, expected), observed From 6749f4e650647e093ebf36e70449b7d504222c61 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:44:56 +0900 Subject: [PATCH 02/16] test(release): narrow RED to release version gate --- .../tests/test_release_version_identity.py | 128 ++++++++++-------- 1 file changed, 72 insertions(+), 56 deletions(-) diff --git a/services/analysis-engine/tests/test_release_version_identity.py b/services/analysis-engine/tests/test_release_version_identity.py index ce06ed771..efd723821 100644 --- a/services/analysis-engine/tests/test_release_version_identity.py +++ b/services/analysis-engine/tests/test_release_version_identity.py @@ -2,65 +2,81 @@ from __future__ import annotations +import importlib.util import json -import tomllib from pathlib import Path +from types import ModuleType + +import pytest _REPOSITORY_ROOT = Path(__file__).resolve().parents[3] +_GUARD_PATH = _REPOSITORY_ROOT / "scripts" / "checks" / "verify_release_identity.py" + + +def _load_guard() -> ModuleType: + """Load the repository-owned release identity guard from its executable path.""" + assert _GUARD_PATH.is_file(), "release preflight must own a version identity guard" + spec = importlib.util.spec_from_file_location("verify_release_identity", _GUARD_PATH) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _write_release_metadata(root: Path, version: str) -> None: + """Write the minimum release metadata consumed by the identity guard.""" + (root / "apps" / "desktop" / "src-tauri").mkdir(parents=True) + (root / "VERSION").write_text(f"{version}\n", encoding="utf-8") + (root / "package.json").write_text( + json.dumps({"name": "bandscope", "version": version}), encoding="utf-8" + ) + (root / "apps" / "desktop" / "src-tauri" / "tauri.conf.json").write_text( + json.dumps( + { + "productName": "BandScope", + "version": version, + "identifier": "com.bandscope.desktop", + } + ), + encoding="utf-8", + ) + + +def test_release_preflight_executes_version_identity_guard() -> None: + """Keep release preflight fail-closed when version projections drift.""" + quickcheck = (_REPOSITORY_ROOT / "scripts" / "harness" / "quickcheck.sh").read_text( + encoding="utf-8" + ) + release_workflow = ( + _REPOSITORY_ROOT / ".github" / "workflows" / "release.yml" + ).read_text(encoding="utf-8") + + assert "python3 scripts/checks/verify_release_identity.py" in quickcheck + assert "./scripts/harness/quickcheck.sh" in release_workflow + + +def test_repository_release_version_matches_authoritative_version_file() -> None: + """Verify the checked-in package and Tauri release versions against VERSION.""" + guard = _load_guard() + assert guard.verify_release_identity(_REPOSITORY_ROOT) == "0.1.3" + + +def test_release_identity_guard_rejects_metadata_drift(tmp_path: Path) -> None: + """Reject a package projection that diverges from the authoritative version.""" + guard = _load_guard() + _write_release_metadata(tmp_path, "1.2.3") + package = json.loads((tmp_path / "package.json").read_text(encoding="utf-8")) + package["version"] = "1.2.4" + (tmp_path / "package.json").write_text(json.dumps(package), encoding="utf-8") + + with pytest.raises(ValueError, match="package.json version does not match VERSION"): + guard.verify_release_identity(tmp_path) + +def test_release_identity_guard_rejects_wrong_tag(tmp_path: Path) -> None: + """Reject a version tag that does not identify the exact VERSION release.""" + guard = _load_guard() + _write_release_metadata(tmp_path, "1.2.3") -def _read_json(relative_path: str) -> dict[str, object]: - """Return one checked-in JSON document as an object.""" - document = json.loads((_REPOSITORY_ROOT / relative_path).read_text(encoding="utf-8")) - assert isinstance(document, dict) - return document - - -def _read_toml(relative_path: str) -> dict[str, object]: - """Return one checked-in TOML document as an object.""" - with (_REPOSITORY_ROOT / relative_path).open("rb") as stream: - document = tomllib.load(stream) - assert isinstance(document, dict) - return document - - -def test_packaged_desktop_uses_authoritative_release_version() -> None: - """Reject release metadata that drifts from the repository VERSION authority.""" - expected = (_REPOSITORY_ROOT / "VERSION").read_text(encoding="utf-8").strip() - assert expected - - root_package = _read_json("package.json") - desktop_package = _read_json("apps/desktop/package.json") - npm_lock = _read_json("package-lock.json") - tauri_config = _read_json("apps/desktop/src-tauri/tauri.conf.json") - cargo_manifest = _read_toml("apps/desktop/src-tauri/Cargo.toml") - cargo_lock = _read_toml("apps/desktop/src-tauri/Cargo.lock") - - npm_packages = npm_lock.get("packages") - assert isinstance(npm_packages, dict) - npm_root = npm_packages.get("") - npm_desktop = npm_packages.get("apps/desktop") - assert isinstance(npm_root, dict) - assert isinstance(npm_desktop, dict) - - cargo_package = cargo_manifest.get("package") - assert isinstance(cargo_package, dict) - locked_desktop = [ - package - for package in cargo_lock.get("package", []) - if isinstance(package, dict) and package.get("name") == "bandscope-desktop" - ] - assert len(locked_desktop) == 1 - - observed = { - "package.json": root_package.get("version"), - "apps/desktop/package.json": desktop_package.get("version"), - "package-lock.json": npm_lock.get("version"), - "package-lock.json#packages['']": npm_root.get("version"), - "package-lock.json#packages['apps/desktop']": npm_desktop.get("version"), - "apps/desktop/src-tauri/tauri.conf.json": tauri_config.get("version"), - "apps/desktop/src-tauri/Cargo.toml": cargo_package.get("version"), - "apps/desktop/src-tauri/Cargo.lock#bandscope-desktop": locked_desktop[0].get("version"), - } - - assert observed == dict.fromkeys(observed, expected), observed + with pytest.raises(ValueError, match="release tag does not match VERSION"): + guard.verify_release_identity(tmp_path, release_tag="v1.2.2") From 261a9a2caa03033d0c7801c23935bad8610076a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:45:16 +0900 Subject: [PATCH 03/16] feat(release): add fail-closed version identity guard --- scripts/checks/verify_release_identity.py | 73 +++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 scripts/checks/verify_release_identity.py diff --git a/scripts/checks/verify_release_identity.py b/scripts/checks/verify_release_identity.py new file mode 100644 index 000000000..8f9e10822 --- /dev/null +++ b/scripts/checks/verify_release_identity.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Fail closed when BandScope release-version projections disagree.""" + +from __future__ import annotations + +import json +import os +import sys +from pathlib import Path +from typing import Any + +_REPOSITORY_ROOT = Path(__file__).resolve().parents[2] + + +def _read_json_object(path: Path) -> dict[str, Any]: + """Read one release metadata document and require a JSON object root.""" + try: + document = json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError) as error: + raise ValueError(f"could not read release metadata: {path.name}") from error + if not isinstance(document, dict): + raise ValueError(f"release metadata must be an object: {path.name}") + return document + + +def _required_string(document: dict[str, Any], key: str, source: str) -> str: + """Return a non-empty string field without coercing malformed metadata.""" + value = document.get(key) + if not isinstance(value, str) or not value.strip() or value != value.strip(): + raise ValueError(f"{source} {key} must be a non-empty trimmed string") + return value + + +def verify_release_identity(root: Path, release_tag: str | None = None) -> str: + """Verify package, Tauri, and optional tag versions against ``VERSION``.""" + try: + version_text = (root / "VERSION").read_text(encoding="utf-8") + except (OSError, UnicodeError) as error: + raise ValueError("could not read authoritative VERSION") from error + expected = version_text.strip() + if not expected or version_text != f"{expected}\n": + raise ValueError("VERSION must contain exactly one non-empty version line") + + package = _read_json_object(root / "package.json") + tauri = _read_json_object(root / "apps" / "desktop" / "src-tauri" / "tauri.conf.json") + + package_version = _required_string(package, "version", "package.json") + tauri_version = _required_string(tauri, "version", "tauri.conf.json") + if package_version != expected: + raise ValueError("package.json version does not match VERSION") + if tauri_version != expected: + raise ValueError("tauri.conf.json version does not match VERSION") + + if release_tag is not None and release_tag != f"v{expected}": + raise ValueError("release tag does not match VERSION") + + return expected + + +def main() -> int: + """Run the release identity gate for repository and tag-triggered workflows.""" + release_tag = os.environ.get("GITHUB_REF_NAME") if os.environ.get("GITHUB_REF_TYPE") == "tag" else None + try: + version = verify_release_identity(_REPOSITORY_ROOT, release_tag=release_tag) + except ValueError as error: + print(f"release identity check failed: {error}", file=sys.stderr) + return 1 + print(f"BandScope release identity verified: v{version}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 8d95854c929f7e505730b0973c9c69fbd9d1bc21 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:45:27 +0900 Subject: [PATCH 04/16] fix(release): enforce version identity in preflight harness --- scripts/harness/quickcheck.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/harness/quickcheck.sh b/scripts/harness/quickcheck.sh index f2b87e4e8..22ba2b31a 100755 --- a/scripts/harness/quickcheck.sh +++ b/scripts/harness/quickcheck.sh @@ -9,6 +9,7 @@ python3 scripts/checks/verify_security_notes.py python3 scripts/checks/security_gates.py python3 scripts/checks/verify_supply_chain.py python3 scripts/checks/verify_github_bootstrap_policy.py +python3 scripts/checks/verify_release_identity.py npm run lint npm run typecheck npm run test From 164f344fa3d680420fd7e8dff50cb4c1fc06b70a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:46:17 +0900 Subject: [PATCH 05/16] test(release): reject ambiguous VERSION authority --- .../tests/test_release_version_identity.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/services/analysis-engine/tests/test_release_version_identity.py b/services/analysis-engine/tests/test_release_version_identity.py index efd723821..0ae40edc4 100644 --- a/services/analysis-engine/tests/test_release_version_identity.py +++ b/services/analysis-engine/tests/test_release_version_identity.py @@ -80,3 +80,27 @@ def test_release_identity_guard_rejects_wrong_tag(tmp_path: Path) -> None: with pytest.raises(ValueError, match="release tag does not match VERSION"): guard.verify_release_identity(tmp_path, release_tag="v1.2.2") + + +def test_release_identity_guard_rejects_multiline_version_authority(tmp_path: Path) -> None: + """Reject an ambiguous VERSION file even if projections repeat the same text.""" + guard = _load_guard() + _write_release_metadata(tmp_path, "1.2.3") + ambiguous = "1.2.3\n2.0.0" + (tmp_path / "VERSION").write_text(f"{ambiguous}\n", encoding="utf-8") + (tmp_path / "package.json").write_text( + json.dumps({"name": "bandscope", "version": ambiguous}), encoding="utf-8" + ) + (tmp_path / "apps" / "desktop" / "src-tauri" / "tauri.conf.json").write_text( + json.dumps( + { + "productName": "BandScope", + "version": ambiguous, + "identifier": "com.bandscope.desktop", + } + ), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="VERSION must contain exactly one non-empty version line"): + guard.verify_release_identity(tmp_path) From f3ebe4dcb92e0f62c1d7378f95a210f03e112188 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:46:36 +0900 Subject: [PATCH 06/16] fix(release): parse VERSION as a single authority line --- scripts/checks/verify_release_identity.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/scripts/checks/verify_release_identity.py b/scripts/checks/verify_release_identity.py index 8f9e10822..1f8e1438f 100644 --- a/scripts/checks/verify_release_identity.py +++ b/scripts/checks/verify_release_identity.py @@ -37,9 +37,16 @@ def verify_release_identity(root: Path, release_tag: str | None = None) -> str: version_text = (root / "VERSION").read_text(encoding="utf-8") except (OSError, UnicodeError) as error: raise ValueError("could not read authoritative VERSION") from error - expected = version_text.strip() - if not expected or version_text != f"{expected}\n": + + lines = version_text.splitlines() + if ( + len(lines) != 1 + or not lines[0] + or lines[0] != lines[0].strip() + or version_text != f"{lines[0]}\n" + ): raise ValueError("VERSION must contain exactly one non-empty version line") + expected = lines[0] package = _read_json_object(root / "package.json") tauri = _read_json_object(root / "apps" / "desktop" / "src-tauri" / "tauri.conf.json") @@ -59,7 +66,11 @@ def verify_release_identity(root: Path, release_tag: str | None = None) -> str: def main() -> int: """Run the release identity gate for repository and tag-triggered workflows.""" - release_tag = os.environ.get("GITHUB_REF_NAME") if os.environ.get("GITHUB_REF_TYPE") == "tag" else None + release_tag = ( + os.environ.get("GITHUB_REF_NAME") + if os.environ.get("GITHUB_REF_TYPE") == "tag" + else None + ) try: version = verify_release_identity(_REPOSITORY_ROOT, release_tag=release_tag) except ValueError as error: From d8efdf7dd641eb5fe7d7e2259b5a59c4873be0bd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:10:40 +0900 Subject: [PATCH 07/16] test(release): require identity gate before publication --- .../tests/test_release_version_identity.py | 54 ++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/services/analysis-engine/tests/test_release_version_identity.py b/services/analysis-engine/tests/test_release_version_identity.py index 0ae40edc4..2616a55e4 100644 --- a/services/analysis-engine/tests/test_release_version_identity.py +++ b/services/analysis-engine/tests/test_release_version_identity.py @@ -8,9 +8,11 @@ from types import ModuleType import pytest +import yaml _REPOSITORY_ROOT = Path(__file__).resolve().parents[3] _GUARD_PATH = _REPOSITORY_ROOT / "scripts" / "checks" / "verify_release_identity.py" +_BUILD_BASELINE_PATH = _REPOSITORY_ROOT / ".github" / "workflows" / "build-baseline.yml" def _load_guard() -> ModuleType: @@ -42,6 +44,16 @@ def _write_release_metadata(root: Path, version: str) -> None: ) +def _workflow_needs(job: dict[str, object]) -> set[str]: + """Normalize a workflow job's ``needs`` dependency to a set of job IDs.""" + needs = job.get("needs", []) + if isinstance(needs, str): + return {needs} + assert isinstance(needs, list) + assert all(isinstance(item, str) for item in needs) + return set(needs) + + def test_release_preflight_executes_version_identity_guard() -> None: """Keep release preflight fail-closed when version projections drift.""" quickcheck = (_REPOSITORY_ROOT / "scripts" / "harness" / "quickcheck.sh").read_text( @@ -55,10 +67,48 @@ def test_release_preflight_executes_version_identity_guard() -> None: assert "./scripts/harness/quickcheck.sh" in release_workflow +def test_tag_build_and_publication_depend_on_release_identity_gate() -> None: + """Block package construction and publication when release identity is invalid.""" + document = yaml.safe_load(_BUILD_BASELINE_PATH.read_text(encoding="utf-8")) + assert isinstance(document, dict) + jobs = document.get("jobs") + assert isinstance(jobs, dict) + + identity_job = jobs.get("release-identity") + assert isinstance(identity_job, dict) + steps = identity_job.get("steps") + assert isinstance(steps, list) + assert any( + isinstance(step, dict) + and step.get("run") == "python3 scripts/checks/verify_release_identity.py" + for step in steps + ) + + for build_job_name in ( + "build-windows-native", + "build-windows-arm64", + "build-macos-native", + "build-macos-arm64", + ): + build_job = jobs.get(build_job_name) + assert isinstance(build_job, dict) + assert "release-identity" in _workflow_needs(build_job) + + publisher = jobs.get("publish-immutable-release") + assert isinstance(publisher, dict) + assert {"release-identity", "gate-windows", "gate-macos"} <= _workflow_needs( + publisher + ) + + def test_repository_release_version_matches_authoritative_version_file() -> None: - """Verify the checked-in package and Tauri release versions against VERSION.""" + """Verify checked-in projections without creating another version authority.""" guard = _load_guard() - assert guard.verify_release_identity(_REPOSITORY_ROOT) == "0.1.3" + version_text = (_REPOSITORY_ROOT / "VERSION").read_text(encoding="utf-8") + assert version_text.endswith("\n") + expected = version_text.removesuffix("\n") + assert "\n" not in expected + assert guard.verify_release_identity(_REPOSITORY_ROOT) == expected def test_release_identity_guard_rejects_metadata_drift(tmp_path: Path) -> None: From 96d6f167d8c8ae53428edaccb16d6324907f36ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:13:14 +0900 Subject: [PATCH 08/16] fix(release): gate artifact publication on version identity --- .github/workflows/build-baseline.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/build-baseline.yml b/.github/workflows/build-baseline.yml index abec57b6b..7a06b0652 100644 --- a/.github/workflows/build-baseline.yml +++ b/.github/workflows/build-baseline.yml @@ -21,8 +21,21 @@ env: GIT_CONFIG_VALUE_0: develop jobs: + release-identity: + name: release-identity + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Verify release identity + run: python3 scripts/checks/verify_release_identity.py + build-windows-native: name: build / windows / amd64 + needs: release-identity runs-on: windows-2025 strategy: fail-fast: false @@ -122,6 +135,7 @@ jobs: build-windows-arm64: name: build / windows / arm64 + needs: release-identity runs-on: windows-11-arm strategy: fail-fast: false @@ -232,6 +246,7 @@ jobs: build-macos-native: name: build / macos / amd64 + needs: release-identity runs-on: macos-15-intel strategy: fail-fast: false @@ -294,6 +309,7 @@ jobs: build-macos-arm64: name: build / macos / arm64 + needs: release-identity runs-on: macos-15 strategy: fail-fast: false @@ -369,6 +385,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest needs: + - release-identity - gate-windows - gate-macos permissions: From 06ef13e62dd85ed439ff25d3732a9619517540a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:22:07 +0900 Subject: [PATCH 09/16] fix(release): keep workflow contract dependency-free --- .../tests/test_release_version_identity.py | 58 +++++++++---------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/services/analysis-engine/tests/test_release_version_identity.py b/services/analysis-engine/tests/test_release_version_identity.py index 2616a55e4..ee4573461 100644 --- a/services/analysis-engine/tests/test_release_version_identity.py +++ b/services/analysis-engine/tests/test_release_version_identity.py @@ -8,7 +8,6 @@ from types import ModuleType import pytest -import yaml _REPOSITORY_ROOT = Path(__file__).resolve().parents[3] _GUARD_PATH = _REPOSITORY_ROOT / "scripts" / "checks" / "verify_release_identity.py" @@ -44,14 +43,22 @@ def _write_release_metadata(root: Path, version: str) -> None: ) -def _workflow_needs(job: dict[str, object]) -> set[str]: - """Normalize a workflow job's ``needs`` dependency to a set of job IDs.""" - needs = job.get("needs", []) - if isinstance(needs, str): - return {needs} - assert isinstance(needs, list) - assert all(isinstance(item, str) for item in needs) - return set(needs) +def _workflow_job_block(workflow: str, job_name: str) -> str: + """Return one top-level GitHub Actions job without requiring a YAML runtime dependency.""" + marker = f" {job_name}:" + lines = workflow.splitlines() + try: + start = lines.index(marker) + except ValueError as error: + raise AssertionError(f"workflow job is missing: {job_name}") from error + + end = len(lines) + for index in range(start + 1, len(lines)): + line = lines[index] + if line.startswith(" ") and not line.startswith(" ") and line.endswith(":"): + end = index + break + return "\n".join(lines[start:end]) def test_release_preflight_executes_version_identity_guard() -> None: @@ -69,20 +76,10 @@ def test_release_preflight_executes_version_identity_guard() -> None: def test_tag_build_and_publication_depend_on_release_identity_gate() -> None: """Block package construction and publication when release identity is invalid.""" - document = yaml.safe_load(_BUILD_BASELINE_PATH.read_text(encoding="utf-8")) - assert isinstance(document, dict) - jobs = document.get("jobs") - assert isinstance(jobs, dict) - - identity_job = jobs.get("release-identity") - assert isinstance(identity_job, dict) - steps = identity_job.get("steps") - assert isinstance(steps, list) - assert any( - isinstance(step, dict) - and step.get("run") == "python3 scripts/checks/verify_release_identity.py" - for step in steps - ) + workflow = _BUILD_BASELINE_PATH.read_text(encoding="utf-8") + + identity_job = _workflow_job_block(workflow, "release-identity") + assert "run: python3 scripts/checks/verify_release_identity.py" in identity_job for build_job_name in ( "build-windows-native", @@ -90,15 +87,12 @@ def test_tag_build_and_publication_depend_on_release_identity_gate() -> None: "build-macos-native", "build-macos-arm64", ): - build_job = jobs.get(build_job_name) - assert isinstance(build_job, dict) - assert "release-identity" in _workflow_needs(build_job) - - publisher = jobs.get("publish-immutable-release") - assert isinstance(publisher, dict) - assert {"release-identity", "gate-windows", "gate-macos"} <= _workflow_needs( - publisher - ) + build_job = _workflow_job_block(workflow, build_job_name) + assert "needs: release-identity" in build_job + + publisher = _workflow_job_block(workflow, "publish-immutable-release") + for required_job in ("release-identity", "gate-windows", "gate-macos"): + assert f" - {required_job}" in publisher def test_repository_release_version_matches_authoritative_version_file() -> None: From 3ecadd733fff9c50701343f614d5b4fa3581b137 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 07:32:12 +0900 Subject: [PATCH 10/16] refactor(release): use semantic identity names --- scripts/checks/verify_release_identity.py | 92 ++++++++++++++--------- 1 file changed, 56 insertions(+), 36 deletions(-) diff --git a/scripts/checks/verify_release_identity.py b/scripts/checks/verify_release_identity.py index 1f8e1438f..ad4bdf61c 100644 --- a/scripts/checks/verify_release_identity.py +++ b/scripts/checks/verify_release_identity.py @@ -12,56 +12,74 @@ _REPOSITORY_ROOT = Path(__file__).resolve().parents[2] -def _read_json_object(path: Path) -> dict[str, Any]: +def _read_json_object(metadata_path: Path) -> dict[str, Any]: """Read one release metadata document and require a JSON object root.""" try: - document = json.loads(path.read_text(encoding="utf-8")) - except (OSError, UnicodeError, json.JSONDecodeError) as error: - raise ValueError(f"could not read release metadata: {path.name}") from error - if not isinstance(document, dict): - raise ValueError(f"release metadata must be an object: {path.name}") - return document - - -def _required_string(document: dict[str, Any], key: str, source: str) -> str: + metadata_document = json.loads(metadata_path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError) as metadata_error: + raise ValueError( + f"could not read release metadata: {metadata_path.name}" + ) from metadata_error + if not isinstance(metadata_document, dict): + raise ValueError(f"release metadata must be an object: {metadata_path.name}") + return metadata_document + + +def _required_string( + metadata_document: dict[str, Any], field_name: str, source_name: str +) -> str: """Return a non-empty string field without coercing malformed metadata.""" - value = document.get(key) - if not isinstance(value, str) or not value.strip() or value != value.strip(): - raise ValueError(f"{source} {key} must be a non-empty trimmed string") - return value + field_value = metadata_document.get(field_name) + if ( + not isinstance(field_value, str) + or not field_value.strip() + or field_value != field_value.strip() + ): + raise ValueError( + f"{source_name} {field_name} must be a non-empty trimmed string" + ) + return field_value -def verify_release_identity(root: Path, release_tag: str | None = None) -> str: +def verify_release_identity( + repository_root: Path, release_tag: str | None = None +) -> str: """Verify package, Tauri, and optional tag versions against ``VERSION``.""" try: - version_text = (root / "VERSION").read_text(encoding="utf-8") - except (OSError, UnicodeError) as error: - raise ValueError("could not read authoritative VERSION") from error + version_text = (repository_root / "VERSION").read_text(encoding="utf-8") + except (OSError, UnicodeError) as identity_error: + raise ValueError("could not read authoritative VERSION") from identity_error - lines = version_text.splitlines() + version_lines = version_text.splitlines() if ( - len(lines) != 1 - or not lines[0] - or lines[0] != lines[0].strip() - or version_text != f"{lines[0]}\n" + len(version_lines) != 1 + or not version_lines[0] + or version_lines[0] != version_lines[0].strip() + or version_text != f"{version_lines[0]}\n" ): raise ValueError("VERSION must contain exactly one non-empty version line") - expected = lines[0] + release_version = version_lines[0] - package = _read_json_object(root / "package.json") - tauri = _read_json_object(root / "apps" / "desktop" / "src-tauri" / "tauri.conf.json") + package_document = _read_json_object(repository_root / "package.json") + tauri_document = _read_json_object( + repository_root / "apps" / "desktop" / "src-tauri" / "tauri.conf.json" + ) - package_version = _required_string(package, "version", "package.json") - tauri_version = _required_string(tauri, "version", "tauri.conf.json") - if package_version != expected: + package_version = _required_string( + package_document, "version", "package.json" + ) + tauri_version = _required_string( + tauri_document, "version", "tauri.conf.json" + ) + if package_version != release_version: raise ValueError("package.json version does not match VERSION") - if tauri_version != expected: + if tauri_version != release_version: raise ValueError("tauri.conf.json version does not match VERSION") - if release_tag is not None and release_tag != f"v{expected}": + if release_tag is not None and release_tag != f"v{release_version}": raise ValueError("release tag does not match VERSION") - return expected + return release_version def main() -> int: @@ -72,11 +90,13 @@ def main() -> int: else None ) try: - version = verify_release_identity(_REPOSITORY_ROOT, release_tag=release_tag) - except ValueError as error: - print(f"release identity check failed: {error}", file=sys.stderr) + release_version = verify_release_identity( + _REPOSITORY_ROOT, release_tag=release_tag + ) + except ValueError as identity_error: + print(f"release identity check failed: {identity_error}", file=sys.stderr) return 1 - print(f"BandScope release identity verified: v{version}") + print(f"BandScope release identity verified: v{release_version}") return 0 From b0d5ecbf18f20842b88879c74fdadd7208476ad7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 08:41:52 +0900 Subject: [PATCH 11/16] test(release): use semantic release-identity names --- .../tests/test_release_version_identity.py | 134 +++++++++++------- 1 file changed, 79 insertions(+), 55 deletions(-) diff --git a/services/analysis-engine/tests/test_release_version_identity.py b/services/analysis-engine/tests/test_release_version_identity.py index ee4573461..67a587bcc 100644 --- a/services/analysis-engine/tests/test_release_version_identity.py +++ b/services/analysis-engine/tests/test_release_version_identity.py @@ -17,25 +17,32 @@ def _load_guard() -> ModuleType: """Load the repository-owned release identity guard from its executable path.""" assert _GUARD_PATH.is_file(), "release preflight must own a version identity guard" - spec = importlib.util.spec_from_file_location("verify_release_identity", _GUARD_PATH) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module + guard_module_spec = importlib.util.spec_from_file_location( + "verify_release_identity", _GUARD_PATH + ) + assert guard_module_spec is not None and guard_module_spec.loader is not None + guard_module = importlib.util.module_from_spec(guard_module_spec) + guard_module_spec.loader.exec_module(guard_module) + return guard_module -def _write_release_metadata(root: Path, version: str) -> None: +def _write_release_metadata(repository_root: Path, release_version: str) -> None: """Write the minimum release metadata consumed by the identity guard.""" - (root / "apps" / "desktop" / "src-tauri").mkdir(parents=True) - (root / "VERSION").write_text(f"{version}\n", encoding="utf-8") - (root / "package.json").write_text( - json.dumps({"name": "bandscope", "version": version}), encoding="utf-8" + (repository_root / "apps" / "desktop" / "src-tauri").mkdir(parents=True) + (repository_root / "VERSION").write_text( + f"{release_version}\n", encoding="utf-8" + ) + (repository_root / "package.json").write_text( + json.dumps({"name": "bandscope", "version": release_version}), + encoding="utf-8", ) - (root / "apps" / "desktop" / "src-tauri" / "tauri.conf.json").write_text( + ( + repository_root / "apps" / "desktop" / "src-tauri" / "tauri.conf.json" + ).write_text( json.dumps( { "productName": "BandScope", - "version": version, + "version": release_version, "identifier": "com.bandscope.desktop", } ), @@ -43,42 +50,46 @@ def _write_release_metadata(root: Path, version: str) -> None: ) -def _workflow_job_block(workflow: str, job_name: str) -> str: +def _workflow_job_block(workflow_text: str, job_name: str) -> str: """Return one top-level GitHub Actions job without requiring a YAML runtime dependency.""" - marker = f" {job_name}:" - lines = workflow.splitlines() + job_marker = f" {job_name}:" + workflow_lines = workflow_text.splitlines() try: - start = lines.index(marker) - except ValueError as error: - raise AssertionError(f"workflow job is missing: {job_name}") from error - - end = len(lines) - for index in range(start + 1, len(lines)): - line = lines[index] - if line.startswith(" ") and not line.startswith(" ") and line.endswith(":"): - end = index + job_start_index = workflow_lines.index(job_marker) + except ValueError as lookup_error: + raise AssertionError(f"workflow job is missing: {job_name}") from lookup_error + + job_end_index = len(workflow_lines) + for line_index in range(job_start_index + 1, len(workflow_lines)): + workflow_line = workflow_lines[line_index] + if ( + workflow_line.startswith(" ") + and not workflow_line.startswith(" ") + and workflow_line.endswith(":") + ): + job_end_index = line_index break - return "\n".join(lines[start:end]) + return "\n".join(workflow_lines[job_start_index:job_end_index]) def test_release_preflight_executes_version_identity_guard() -> None: """Keep release preflight fail-closed when version projections drift.""" - quickcheck = (_REPOSITORY_ROOT / "scripts" / "harness" / "quickcheck.sh").read_text( - encoding="utf-8" - ) - release_workflow = ( + quickcheck_text = ( + _REPOSITORY_ROOT / "scripts" / "harness" / "quickcheck.sh" + ).read_text(encoding="utf-8") + release_workflow_text = ( _REPOSITORY_ROOT / ".github" / "workflows" / "release.yml" ).read_text(encoding="utf-8") - assert "python3 scripts/checks/verify_release_identity.py" in quickcheck - assert "./scripts/harness/quickcheck.sh" in release_workflow + assert "python3 scripts/checks/verify_release_identity.py" in quickcheck_text + assert "./scripts/harness/quickcheck.sh" in release_workflow_text def test_tag_build_and_publication_depend_on_release_identity_gate() -> None: """Block package construction and publication when release identity is invalid.""" - workflow = _BUILD_BASELINE_PATH.read_text(encoding="utf-8") + build_workflow_text = _BUILD_BASELINE_PATH.read_text(encoding="utf-8") - identity_job = _workflow_job_block(workflow, "release-identity") + identity_job = _workflow_job_block(build_workflow_text, "release-identity") assert "run: python3 scripts/checks/verify_release_identity.py" in identity_job for build_job_name in ( @@ -87,64 +98,77 @@ def test_tag_build_and_publication_depend_on_release_identity_gate() -> None: "build-macos-native", "build-macos-arm64", ): - build_job = _workflow_job_block(workflow, build_job_name) + build_job = _workflow_job_block(build_workflow_text, build_job_name) assert "needs: release-identity" in build_job - publisher = _workflow_job_block(workflow, "publish-immutable-release") - for required_job in ("release-identity", "gate-windows", "gate-macos"): - assert f" - {required_job}" in publisher + publication_job = _workflow_job_block( + build_workflow_text, "publish-immutable-release" + ) + for required_job_name in ("release-identity", "gate-windows", "gate-macos"): + assert f" - {required_job_name}" in publication_job def test_repository_release_version_matches_authoritative_version_file() -> None: """Verify checked-in projections without creating another version authority.""" - guard = _load_guard() + release_guard = _load_guard() version_text = (_REPOSITORY_ROOT / "VERSION").read_text(encoding="utf-8") assert version_text.endswith("\n") - expected = version_text.removesuffix("\n") - assert "\n" not in expected - assert guard.verify_release_identity(_REPOSITORY_ROOT) == expected + expected_version = version_text.removesuffix("\n") + assert "\n" not in expected_version + assert ( + release_guard.verify_release_identity(_REPOSITORY_ROOT) == expected_version + ) def test_release_identity_guard_rejects_metadata_drift(tmp_path: Path) -> None: """Reject a package projection that diverges from the authoritative version.""" - guard = _load_guard() + release_guard = _load_guard() _write_release_metadata(tmp_path, "1.2.3") - package = json.loads((tmp_path / "package.json").read_text(encoding="utf-8")) - package["version"] = "1.2.4" - (tmp_path / "package.json").write_text(json.dumps(package), encoding="utf-8") + package_document = json.loads( + (tmp_path / "package.json").read_text(encoding="utf-8") + ) + package_document["version"] = "1.2.4" + (tmp_path / "package.json").write_text( + json.dumps(package_document), encoding="utf-8" + ) with pytest.raises(ValueError, match="package.json version does not match VERSION"): - guard.verify_release_identity(tmp_path) + release_guard.verify_release_identity(tmp_path) def test_release_identity_guard_rejects_wrong_tag(tmp_path: Path) -> None: """Reject a version tag that does not identify the exact VERSION release.""" - guard = _load_guard() + release_guard = _load_guard() _write_release_metadata(tmp_path, "1.2.3") with pytest.raises(ValueError, match="release tag does not match VERSION"): - guard.verify_release_identity(tmp_path, release_tag="v1.2.2") + release_guard.verify_release_identity(tmp_path, release_tag="v1.2.2") def test_release_identity_guard_rejects_multiline_version_authority(tmp_path: Path) -> None: """Reject an ambiguous VERSION file even if projections repeat the same text.""" - guard = _load_guard() + release_guard = _load_guard() _write_release_metadata(tmp_path, "1.2.3") - ambiguous = "1.2.3\n2.0.0" - (tmp_path / "VERSION").write_text(f"{ambiguous}\n", encoding="utf-8") + ambiguous_version = "1.2.3\n2.0.0" + (tmp_path / "VERSION").write_text( + f"{ambiguous_version}\n", encoding="utf-8" + ) (tmp_path / "package.json").write_text( - json.dumps({"name": "bandscope", "version": ambiguous}), encoding="utf-8" + json.dumps({"name": "bandscope", "version": ambiguous_version}), + encoding="utf-8", ) (tmp_path / "apps" / "desktop" / "src-tauri" / "tauri.conf.json").write_text( json.dumps( { "productName": "BandScope", - "version": ambiguous, + "version": ambiguous_version, "identifier": "com.bandscope.desktop", } ), encoding="utf-8", ) - with pytest.raises(ValueError, match="VERSION must contain exactly one non-empty version line"): - guard.verify_release_identity(tmp_path) + with pytest.raises( + ValueError, match="VERSION must contain exactly one non-empty version line" + ): + release_guard.verify_release_identity(tmp_path) From dfbcbca6cd01fd57f03c27a4b48e6f0459d607a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:34:58 +0900 Subject: [PATCH 12/16] docs(release): document identity-gate security boundary --- scripts/checks/verify_release_identity.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/checks/verify_release_identity.py b/scripts/checks/verify_release_identity.py index ad4bdf61c..8b5d1be7e 100644 --- a/scripts/checks/verify_release_identity.py +++ b/scripts/checks/verify_release_identity.py @@ -1,5 +1,15 @@ #!/usr/bin/env python3 -"""Fail closed when BandScope release-version projections disagree.""" +"""Fail closed when BandScope release-version projections disagree. + +Security Notes: +- ``repository_root`` is an already-selected repository boundary; this guard + reads only the fixed ``VERSION``, ``package.json``, and Tauri configuration + paths beneath it and never follows metadata-provided file paths. +- VERSION and JSON fields are validated as exact, non-empty, trimmed strings + before comparison; malformed text or JSON fails closed without echoing values. +- The guard has no network, filesystem-write, subprocess, update, credential, + signing, or publication authority. It only returns a version or a failure. +""" from __future__ import annotations From 764e06c26c55ca6cf8c771389c010e6b0476e95e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 17:12:19 +0900 Subject: [PATCH 13/16] test(release): require platform trust before publication --- .../tests/test_release_platform_trust.py | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 services/analysis-engine/tests/test_release_platform_trust.py diff --git a/services/analysis-engine/tests/test_release_platform_trust.py b/services/analysis-engine/tests/test_release_platform_trust.py new file mode 100644 index 000000000..b1362364e --- /dev/null +++ b/services/analysis-engine/tests/test_release_platform_trust.py @@ -0,0 +1,221 @@ +"""Platform signature and notarization gates for BandScope release artifacts.""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path +from types import ModuleType, SimpleNamespace + +import pytest + +_REPOSITORY_ROOT = Path(__file__).resolve().parents[3] +_GUARD_PATH = _REPOSITORY_ROOT / "scripts" / "checks" / "verify_release_platform_trust.py" +_BUILD_BASELINE_PATH = _REPOSITORY_ROOT / ".github" / "workflows" / "build-baseline.yml" + + +def _load_guard() -> ModuleType: + """Load the executable release-trust guard from its repository path.""" + assert _GUARD_PATH.is_file(), "release builds must own a platform trust verifier" + guard_spec = importlib.util.spec_from_file_location( + "verify_release_platform_trust", _GUARD_PATH + ) + assert guard_spec is not None and guard_spec.loader is not None + guard_module = importlib.util.module_from_spec(guard_spec) + guard_spec.loader.exec_module(guard_module) + return guard_module + + +def _command_result( + returncode: int = 0, stdout: str = "", stderr: str = "" +) -> SimpleNamespace: + """Build the subprocess result shape consumed by the trust verifier.""" + return SimpleNamespace(returncode=returncode, stdout=stdout, stderr=stderr) + + +def _workflow_job_block(workflow_text: str, job_name: str) -> str: + """Return one top-level GitHub Actions job without adding a YAML dependency.""" + job_marker = f" {job_name}:" + workflow_lines = workflow_text.splitlines() + job_start_index = workflow_lines.index(job_marker) + job_end_index = len(workflow_lines) + for line_index in range(job_start_index + 1, len(workflow_lines)): + workflow_line = workflow_lines[line_index] + if ( + workflow_line.startswith(" ") + and not workflow_line.startswith(" ") + and workflow_line.endswith(":") + ): + job_end_index = line_index + break + return "\n".join(workflow_lines[job_start_index:job_end_index]) + + +def test_windows_release_trust_requires_valid_exact_publisher(tmp_path: Path) -> None: + """Accept only valid Authenticode signatures from the configured publisher.""" + guard = _load_guard() + artifact_path = tmp_path / "bandscope.exe" + artifact_path.write_bytes(b"signed-installer-placeholder") + commands: list[list[str]] = [] + + def valid_runner(command: list[str], **_: object) -> SimpleNamespace: + commands.append(command) + return _command_result( + stdout='{"Status":"Valid","Subject":"CN=ContextualWisdomLab"}' + ) + + verified = guard.verify_windows_artifacts( + tmp_path, "CN=ContextualWisdomLab", runner=valid_runner + ) + + assert verified == [artifact_path] + assert commands[0][0] == "pwsh" + assert str(artifact_path) == commands[0][-1] + + def unsigned_runner(command: list[str], **_: object) -> SimpleNamespace: + del command + return _command_result(stdout='{"Status":"NotSigned","Subject":null}') + + with pytest.raises(ValueError, match="valid Authenticode signature"): + guard.verify_windows_artifacts( + tmp_path, "CN=ContextualWisdomLab", runner=unsigned_runner + ) + + def wrong_publisher_runner(command: list[str], **_: object) -> SimpleNamespace: + del command + return _command_result(stdout='{"Status":"Valid","Subject":"CN=Other Publisher"}') + + with pytest.raises(ValueError, match="approved Windows publisher"): + guard.verify_windows_artifacts( + tmp_path, "CN=ContextualWisdomLab", runner=wrong_publisher_runner + ) + + +def test_windows_release_trust_fails_closed_without_identity_or_artifacts( + tmp_path: Path, +) -> None: + """Refuse a tag release when publisher authority or installers are absent.""" + guard = _load_guard() + + with pytest.raises(ValueError, match="Windows publisher subject"): + guard.verify_windows_artifacts(tmp_path, "") + + with pytest.raises(ValueError, match="Windows release installer"): + guard.verify_windows_artifacts(tmp_path, "CN=ContextualWisdomLab") + + +def test_macos_release_trust_requires_team_signature_and_stapled_ticket( + tmp_path: Path, +) -> None: + """Require Developer ID team identity plus offline notarization evidence.""" + guard = _load_guard() + artifact_root = tmp_path / "artifacts" + bundle_root = tmp_path / "bundle" / "macos" + artifact_root.mkdir() + app_path = bundle_root / "BandScope.app" + app_path.mkdir(parents=True) + dmg_path = artifact_root / "bandscope.dmg" + dmg_path.write_bytes(b"notarized-dmg-placeholder") + commands: list[list[str]] = [] + + def valid_runner(command: list[str], **_: object) -> SimpleNamespace: + commands.append(command) + if command[:3] == ["codesign", "--display", "--verbose=4"]: + return _command_result(stderr="TeamIdentifier=ABCDE12345\n") + return _command_result() + + verified_apps, verified_dmgs = guard.verify_macos_artifacts( + artifact_root, bundle_root, "ABCDE12345", runner=valid_runner + ) + + assert verified_apps == [app_path] + assert verified_dmgs == [dmg_path] + assert ["codesign", "--verify", "--deep", "--strict", str(app_path)] in commands + assert ["xcrun", "stapler", "validate", str(dmg_path)] in commands + assert [ + "spctl", + "--assess", + "--type", + "open", + "--context", + "context:primary-signature", + "--verbose=2", + str(dmg_path), + ] in commands + + +def test_macos_release_trust_fails_closed_on_wrong_team_or_notarization( + tmp_path: Path, +) -> None: + """Reject an unexpected signing team and a DMG without valid notarization evidence.""" + guard = _load_guard() + artifact_root = tmp_path / "artifacts" + bundle_root = tmp_path / "bundle" / "macos" + artifact_root.mkdir() + app_path = bundle_root / "BandScope.app" + app_path.mkdir(parents=True) + dmg_path = artifact_root / "bandscope.dmg" + dmg_path.write_bytes(b"dmg-placeholder") + + def wrong_team_runner(command: list[str], **_: object) -> SimpleNamespace: + if command[:3] == ["codesign", "--display", "--verbose=4"]: + return _command_result(stderr="TeamIdentifier=ZZZZZ99999\n") + return _command_result() + + with pytest.raises(ValueError, match="approved Apple Team ID"): + guard.verify_macos_artifacts( + artifact_root, bundle_root, "ABCDE12345", runner=wrong_team_runner + ) + + def unstapled_runner(command: list[str], **_: object) -> SimpleNamespace: + if command[:3] == ["codesign", "--display", "--verbose=4"]: + return _command_result(stderr="TeamIdentifier=ABCDE12345\n") + if command[:3] == ["xcrun", "stapler", "validate"]: + return _command_result(returncode=1, stderr="ticket missing") + return _command_result() + + with pytest.raises(ValueError, match="notarization ticket"): + guard.verify_macos_artifacts( + artifact_root, bundle_root, "ABCDE12345", runner=unstapled_runner + ) + + +def test_macos_release_trust_fails_closed_without_identity_or_outputs( + tmp_path: Path, +) -> None: + """Refuse a macOS release when configured team authority or outputs are absent.""" + guard = _load_guard() + artifact_root = tmp_path / "artifacts" + bundle_root = tmp_path / "bundle" + artifact_root.mkdir() + bundle_root.mkdir() + + with pytest.raises(ValueError, match="Apple Team ID"): + guard.verify_macos_artifacts(artifact_root, bundle_root, "") + + with pytest.raises(ValueError, match="macOS application bundle"): + guard.verify_macos_artifacts(artifact_root, bundle_root, "ABCDE12345") + + +def test_tag_builds_verify_platform_trust_before_upload() -> None: + """Keep immutable publication downstream of platform-native trust verification.""" + workflow_text = _BUILD_BASELINE_PATH.read_text(encoding="utf-8") + + for job_name in ("build-windows-native", "build-windows-arm64"): + job_block = _workflow_job_block(workflow_text, job_name) + verification_index = job_block.index( + "python scripts/checks/verify_release_platform_trust.py windows" + ) + upload_index = job_block.index("uses: actions/upload-artifact@") + assert "if: startsWith(github.ref, 'refs/tags/v')" in job_block + assert "BANDSCOPE_WINDOWS_PUBLISHER_SUBJECT" in job_block + assert verification_index < upload_index + + for job_name in ("build-macos-native", "build-macos-arm64"): + job_block = _workflow_job_block(workflow_text, job_name) + verification_index = job_block.index( + "python3 scripts/checks/verify_release_platform_trust.py macos" + ) + upload_index = job_block.index("uses: actions/upload-artifact@") + assert "if: startsWith(github.ref, 'refs/tags/v')" in job_block + assert "BANDSCOPE_APPLE_TEAM_ID" in job_block + assert verification_index < upload_index From 13da983984ab5fa1069acee31af02f5464cd2747 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 17:13:29 +0900 Subject: [PATCH 14/16] feat(release): verify native signing and notarization evidence --- .../checks/verify_release_platform_trust.py | 242 ++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 scripts/checks/verify_release_platform_trust.py diff --git a/scripts/checks/verify_release_platform_trust.py b/scripts/checks/verify_release_platform_trust.py new file mode 100644 index 000000000..6d6d09cb9 --- /dev/null +++ b/scripts/checks/verify_release_platform_trust.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""Verify platform-native trust on BandScope release artifacts before publication. + +Security Notes: + This verifier has read/execute authority only over repository-built release outputs and + fixed platform trust tools. Artifact paths are passed as subprocess arguments rather than + interpolated into shell text. Publisher identity comes from repository configuration, is + bounded, and is compared exactly. Command output is parsed only for the minimum signature + status/team fields and is never promoted into a filesystem path or command. Any missing + artifact, missing identity, malformed trust output, unsigned artifact, unexpected signer, + failed Gatekeeper assessment, or missing notarization ticket fails closed. +""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +from collections.abc import Callable, Sequence +from pathlib import Path +from typing import Any + +CommandRunner = Callable[..., Any] +_WINDOWS_SUFFIXES = {".exe", ".msi"} +_APPLE_TEAM_ID_PATTERN = re.compile(r"^[A-Z0-9]{10}$") +_WINDOWS_SIGNATURE_SCRIPT = r""" +$signature = Get-AuthenticodeSignature -LiteralPath $args[0] +$subject = $null +if ($null -ne $signature.SignerCertificate) { + $subject = $signature.SignerCertificate.Subject +} +[pscustomobject]@{ + Status = [string]$signature.Status + Subject = $subject +} | ConvertTo-Json -Compress +""".strip() + + +def _configured_identity(label: str, value: str, *, max_length: int) -> str: + """Return a bounded single-line configured signer identity or fail closed.""" + if not value or value != value.strip() or len(value) > max_length: + raise ValueError(f"{label} must be configured exactly for release verification") + if any(character in value for character in "\r\n\x00"): + raise ValueError(f"{label} must be configured exactly for release verification") + return value + + +def _regular_files(root: Path, suffixes: set[str], missing_label: str) -> list[Path]: + """Return direct regular non-link release files with one of the allowed suffixes.""" + if not root.is_dir() or root.is_symlink(): + raise ValueError(f"{missing_label} directory is unavailable") + matches: list[Path] = [] + for candidate in sorted(root.iterdir()): + if candidate.suffix.lower() not in suffixes: + continue + if candidate.is_symlink() or not candidate.is_file(): + raise ValueError(f"{missing_label} must be a regular non-link file") + matches.append(candidate) + if not matches: + raise ValueError(f"no {missing_label} was produced") + return matches + + +def _application_bundles(bundle_root: Path) -> list[Path]: + """Return direct regular macOS application bundles from the Tauri bundle directory.""" + if not bundle_root.is_dir() or bundle_root.is_symlink(): + raise ValueError("macOS application bundle directory is unavailable") + applications: list[Path] = [] + for candidate in sorted(bundle_root.glob("*.app")): + if candidate.is_symlink() or not candidate.is_dir(): + raise ValueError("macOS application bundle must be a regular non-link directory") + applications.append(candidate) + if not applications: + raise ValueError("no macOS application bundle was produced") + return applications + + +def _run_command( + command: Sequence[str], + *, + runner: CommandRunner, + failure_message: str, +) -> Any: + """Run one fixed trust command and translate any nonzero result into a bounded error.""" + try: + result = runner( + list(command), + capture_output=True, + text=True, + check=False, + ) + except OSError as command_error: + raise ValueError(failure_message) from command_error + if result.returncode != 0: + raise ValueError(failure_message) + return result + + +def verify_windows_artifacts( + artifact_root: Path, + expected_publisher_subject: str, + *, + runner: CommandRunner = subprocess.run, +) -> list[Path]: + """Verify Authenticode validity and the exact approved publisher for Windows installers.""" + expected_subject = _configured_identity( + "Windows publisher subject", expected_publisher_subject, max_length=512 + ) + installers = _regular_files( + artifact_root, _WINDOWS_SUFFIXES, "Windows release installer" + ) + for installer in installers: + result = _run_command( + [ + "pwsh", + "-NoProfile", + "-NonInteractive", + "-Command", + _WINDOWS_SIGNATURE_SCRIPT, + str(installer), + ], + runner=runner, + failure_message="Windows release installer does not have a valid Authenticode signature", + ) + try: + signature = json.loads(result.stdout) + except (json.JSONDecodeError, TypeError) as output_error: + raise ValueError( + "Windows release installer does not have a valid Authenticode signature" + ) from output_error + if not isinstance(signature, dict) or signature.get("Status") != "Valid": + raise ValueError( + "Windows release installer does not have a valid Authenticode signature" + ) + if signature.get("Subject") != expected_subject: + raise ValueError("Windows release installer is not signed by the approved Windows publisher") + return installers + + +def _macos_team_identifier(details: str) -> str | None: + """Extract the exact TeamIdentifier line from codesign display output.""" + for line in details.splitlines(): + if line.startswith("TeamIdentifier="): + return line.removeprefix("TeamIdentifier=") + return None + + +def verify_macos_artifacts( + artifact_root: Path, + bundle_root: Path, + expected_team_id: str, + *, + runner: CommandRunner = subprocess.run, +) -> tuple[list[Path], list[Path]]: + """Verify signed app bundles and stapled, Gatekeeper-accepted macOS disk images.""" + team_id = _configured_identity("Apple Team ID", expected_team_id, max_length=10) + if _APPLE_TEAM_ID_PATTERN.fullmatch(team_id) is None: + raise ValueError("Apple Team ID must be configured exactly for release verification") + + applications = _application_bundles(bundle_root) + disk_images = _regular_files(artifact_root, {".dmg"}, "macOS release disk image") + + for application in applications: + _run_command( + ["codesign", "--verify", "--deep", "--strict", str(application)], + runner=runner, + failure_message="macOS application bundle does not have a valid code signature", + ) + details = _run_command( + ["codesign", "--display", "--verbose=4", str(application)], + runner=runner, + failure_message="macOS application bundle signing identity could not be verified", + ) + signer_details = f"{details.stdout}\n{details.stderr}" + if _macos_team_identifier(signer_details) != team_id: + raise ValueError("macOS application bundle is not signed by the approved Apple Team ID") + + for disk_image in disk_images: + _run_command( + ["xcrun", "stapler", "validate", str(disk_image)], + runner=runner, + failure_message="macOS release disk image does not contain a valid notarization ticket", + ) + _run_command( + [ + "spctl", + "--assess", + "--type", + "open", + "--context", + "context:primary-signature", + "--verbose=2", + str(disk_image), + ], + runner=runner, + failure_message="macOS release disk image is not accepted by Gatekeeper", + ) + return applications, disk_images + + +def _parser() -> argparse.ArgumentParser: + """Build the command-line contract used by release jobs.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("platform", choices=("windows", "macos")) + parser.add_argument("artifact_root", type=Path) + parser.add_argument("--bundle-root", type=Path) + parser.add_argument("--expected-identity", required=True) + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + """Verify one platform's release outputs and return a fail-closed process status.""" + arguments = _parser().parse_args(argv) + try: + if arguments.platform == "windows": + verified = verify_windows_artifacts( + arguments.artifact_root, arguments.expected_identity + ) + print(f"Verified {len(verified)} Windows release installer(s).") + return 0 + if arguments.bundle_root is None: + raise ValueError("macOS release verification requires --bundle-root") + applications, disk_images = verify_macos_artifacts( + arguments.artifact_root, + arguments.bundle_root, + arguments.expected_identity, + ) + print( + "Verified " + f"{len(applications)} macOS application bundle(s) and " + f"{len(disk_images)} notarized disk image(s)." + ) + return 0 + except ValueError as verification_error: + print(f"Release platform trust verification failed: {verification_error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) From 8de728b680c2657a6fc5e6bffb54dfe3861a76b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 17:15:48 +0900 Subject: [PATCH 15/16] fix(release): block tag packaging without native trust --- scripts/release/package_desktop_artifact.py | 73 ++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/scripts/release/package_desktop_artifact.py b/scripts/release/package_desktop_artifact.py index 5617ce760..7a7b77601 100644 --- a/scripts/release/package_desktop_artifact.py +++ b/scripts/release/package_desktop_artifact.py @@ -7,8 +7,14 @@ import platform import re import shutil +import subprocess +import sys from collections import Counter +from collections.abc import Callable, Sequence from pathlib import Path +from typing import Any + +CommandRunner = Callable[..., Any] def sha256_file(path: Path) -> str: @@ -104,8 +110,72 @@ def find_installer_packages(repo_root: Path) -> list[Path]: return sorted(installers) +def _is_tag_release() -> bool: + """Return whether this package operation belongs to a version-tag release build.""" + return os.environ.get("GITHUB_REF", "").startswith("refs/tags/v") + + +def _platform_trust_command(repo_root: Path, output_dir: Path) -> Sequence[str]: + """Build the fixed verifier command for the selected tagged release target.""" + verifier_path = repo_root / "scripts" / "checks" / "verify_release_platform_trust.py" + target_platform, _ = resolved_artifact_target() + if target_platform == "windows": + return [ + sys.executable, + str(verifier_path), + "windows", + str(output_dir), + "--expected-identity", + os.environ.get("BANDSCOPE_WINDOWS_PUBLISHER_SUBJECT", ""), + ] + if target_platform == "macos": + target_triple = os.environ.get("BANDSCOPE_TARGET_TRIPLE", "") + if not target_triple: + raise RuntimeError("Tagged macOS release packaging requires BANDSCOPE_TARGET_TRIPLE") + bundle_root = ( + repo_root + / "apps" + / "desktop" + / "src-tauri" + / "target" + / target_triple + / "release" + / "bundle" + / "macos" + ) + return [ + sys.executable, + str(verifier_path), + "macos", + str(output_dir), + "--bundle-root", + str(bundle_root), + "--expected-identity", + os.environ.get("BANDSCOPE_APPLE_TEAM_ID", ""), + ] + raise RuntimeError("Tagged release packaging is unsupported on this platform") + + +def verify_tag_platform_trust( + repo_root: Path, + output_dir: Path, + *, + runner: CommandRunner = subprocess.run, +) -> None: + """Block tagged artifact publication unless platform-native trust evidence passes.""" + if not _is_tag_release(): + return + command = _platform_trust_command(repo_root, output_dir) + try: + result = runner(list(command), check=False) + except OSError as verification_error: + raise RuntimeError("Platform release trust verification could not run") from verification_error + if result.returncode != 0: + raise RuntimeError("Platform release trust verification failed") + + def main() -> int: - """Find the built installer packages, rename them, and calculate checksums.""" + """Find the built installer packages, rename them, calculate checksums, and verify tag trust.""" repo_root = Path(__file__).resolve().parents[2] output_dir = repo_root / "artifacts" output_dir.mkdir(parents=True, exist_ok=True) @@ -155,6 +225,7 @@ def main() -> int: print(f"Packaged {installer_path.name} to artifacts/{archive_name}") + verify_tag_platform_trust(repo_root, output_dir) return 0 From d33bb96ec1794c02492fea3e8cd5e36280709eae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 17:16:59 +0900 Subject: [PATCH 16/16] test(release): exercise tag packaging trust boundary --- .../tests/test_release_platform_trust.py | 142 ++++++++++++++---- 1 file changed, 115 insertions(+), 27 deletions(-) diff --git a/services/analysis-engine/tests/test_release_platform_trust.py b/services/analysis-engine/tests/test_release_platform_trust.py index b1362364e..f3e0ca207 100644 --- a/services/analysis-engine/tests/test_release_platform_trust.py +++ b/services/analysis-engine/tests/test_release_platform_trust.py @@ -10,19 +10,28 @@ _REPOSITORY_ROOT = Path(__file__).resolve().parents[3] _GUARD_PATH = _REPOSITORY_ROOT / "scripts" / "checks" / "verify_release_platform_trust.py" +_PACKAGER_PATH = _REPOSITORY_ROOT / "scripts" / "release" / "package_desktop_artifact.py" _BUILD_BASELINE_PATH = _REPOSITORY_ROOT / ".github" / "workflows" / "build-baseline.yml" +def _load_module(path: Path, module_name: str) -> ModuleType: + """Load one repository-owned executable module without adding a package boundary.""" + assert path.is_file(), f"release boundary module is missing: {path.name}" + module_spec = importlib.util.spec_from_file_location(module_name, path) + assert module_spec is not None and module_spec.loader is not None + module = importlib.util.module_from_spec(module_spec) + module_spec.loader.exec_module(module) + return module + + def _load_guard() -> ModuleType: """Load the executable release-trust guard from its repository path.""" - assert _GUARD_PATH.is_file(), "release builds must own a platform trust verifier" - guard_spec = importlib.util.spec_from_file_location( - "verify_release_platform_trust", _GUARD_PATH - ) - assert guard_spec is not None and guard_spec.loader is not None - guard_module = importlib.util.module_from_spec(guard_spec) - guard_spec.loader.exec_module(guard_module) - return guard_module + return _load_module(_GUARD_PATH, "verify_release_platform_trust") + + +def _load_packager() -> ModuleType: + """Load the release packager that owns the tag-publication trust call.""" + return _load_module(_PACKAGER_PATH, "package_desktop_artifact_trust") def _command_result( @@ -82,7 +91,9 @@ def unsigned_runner(command: list[str], **_: object) -> SimpleNamespace: def wrong_publisher_runner(command: list[str], **_: object) -> SimpleNamespace: del command - return _command_result(stdout='{"Status":"Valid","Subject":"CN=Other Publisher"}') + return _command_result( + stdout='{"Status":"Valid","Subject":"CN=Other Publisher"}' + ) with pytest.raises(ValueError, match="approved Windows publisher"): guard.verify_windows_artifacts( @@ -196,26 +207,103 @@ def test_macos_release_trust_fails_closed_without_identity_or_outputs( guard.verify_macos_artifacts(artifact_root, bundle_root, "ABCDE12345") -def test_tag_builds_verify_platform_trust_before_upload() -> None: - """Keep immutable publication downstream of platform-native trust verification.""" - workflow_text = _BUILD_BASELINE_PATH.read_text(encoding="utf-8") +def test_tag_packager_invokes_windows_trust_guard( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Bind Windows tag packaging to the native verifier before artifact upload.""" + packager = _load_packager() + monkeypatch.setenv("GITHUB_REF", "refs/tags/v1.2.3") + monkeypatch.setenv("BANDSCOPE_ARTIFACT_OS", "windows") + monkeypatch.setenv( + "BANDSCOPE_WINDOWS_PUBLISHER_SUBJECT", "CN=ContextualWisdomLab" + ) + commands: list[list[str]] = [] - for job_name in ("build-windows-native", "build-windows-arm64"): - job_block = _workflow_job_block(workflow_text, job_name) - verification_index = job_block.index( - "python scripts/checks/verify_release_platform_trust.py windows" + def runner(command: list[str], **_: object) -> SimpleNamespace: + commands.append(command) + return _command_result() + + packager.verify_tag_platform_trust(tmp_path, tmp_path / "artifacts", runner=runner) + + assert len(commands) == 1 + assert commands[0][2:5] == [ + "windows", + str(tmp_path / "artifacts"), + "--expected-identity", + ] + assert commands[0][-1] == "CN=ContextualWisdomLab" + + +def test_tag_packager_invokes_macos_trust_guard( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Bind macOS tag packaging to signature, team, and notarization verification.""" + packager = _load_packager() + monkeypatch.setenv("GITHUB_REF", "refs/tags/v1.2.3") + monkeypatch.setenv("BANDSCOPE_ARTIFACT_OS", "macos") + monkeypatch.setenv("BANDSCOPE_TARGET_TRIPLE", "aarch64-apple-darwin") + monkeypatch.setenv("BANDSCOPE_APPLE_TEAM_ID", "ABCDE12345") + commands: list[list[str]] = [] + + def runner(command: list[str], **_: object) -> SimpleNamespace: + commands.append(command) + return _command_result() + + packager.verify_tag_platform_trust(tmp_path, tmp_path / "artifacts", runner=runner) + + expected_bundle_root = ( + tmp_path + / "apps" + / "desktop" + / "src-tauri" + / "target" + / "aarch64-apple-darwin" + / "release" + / "bundle" + / "macos" + ) + assert len(commands) == 1 + assert "macos" in commands[0] + assert str(expected_bundle_root) in commands[0] + assert commands[0][-1] == "ABCDE12345" + + +def test_tag_packager_is_fail_closed_and_non_tag_packaging_stays_build_only( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Reject failed release trust while leaving ordinary validation builds unsigned.""" + packager = _load_packager() + commands: list[list[str]] = [] + + def failing_runner(command: list[str], **_: object) -> SimpleNamespace: + commands.append(command) + return _command_result(returncode=1) + + monkeypatch.setenv("GITHUB_REF", "refs/heads/develop") + packager.verify_tag_platform_trust( + tmp_path, tmp_path / "artifacts", runner=failing_runner + ) + assert commands == [] + + monkeypatch.setenv("GITHUB_REF", "refs/tags/v1.2.3") + monkeypatch.setenv("BANDSCOPE_ARTIFACT_OS", "windows") + with pytest.raises(RuntimeError, match="Platform release trust verification failed"): + packager.verify_tag_platform_trust( + tmp_path, tmp_path / "artifacts", runner=failing_runner ) - upload_index = job_block.index("uses: actions/upload-artifact@") - assert "if: startsWith(github.ref, 'refs/tags/v')" in job_block - assert "BANDSCOPE_WINDOWS_PUBLISHER_SUBJECT" in job_block - assert verification_index < upload_index - for job_name in ("build-macos-native", "build-macos-arm64"): + +def test_tag_builds_package_before_artifact_upload() -> None: + """Keep immutable publication downstream of the packager-owned trust gate.""" + workflow_text = _BUILD_BASELINE_PATH.read_text(encoding="utf-8") + + for job_name in ( + "build-windows-native", + "build-windows-arm64", + "build-macos-native", + "build-macos-arm64", + ): job_block = _workflow_job_block(workflow_text, job_name) - verification_index = job_block.index( - "python3 scripts/checks/verify_release_platform_trust.py macos" - ) + packaging_index = job_block.index("scripts/release/package_desktop_artifact.py") upload_index = job_block.index("uses: actions/upload-artifact@") - assert "if: startsWith(github.ref, 'refs/tags/v')" in job_block - assert "BANDSCOPE_APPLE_TEAM_ID" in job_block - assert verification_index < upload_index + assert packaging_index < upload_index