diff --git a/.github/workflows/build-baseline.yml b/.github/workflows/build-baseline.yml index 5ed5a5f..77c2427 100644 --- a/.github/workflows/build-baseline.yml +++ b/.github/workflows/build-baseline.yml @@ -19,11 +19,17 @@ permissions: contents: read jobs: - build-windows: - name: gate / build / windows - runs-on: windows-latest + build-windows-native: + name: build / windows / amd64 + runs-on: windows-2025 + strategy: + fail-fast: false permissions: contents: read + env: + BANDSCOPE_ARTIFACT_OS: windows + BANDSCOPE_ARTIFACT_ARCH: amd64 + BANDSCOPE_TARGET_TRIPLE: x86_64-pc-windows-msvc steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -39,8 +45,41 @@ jobs: with: version: "0.8.6" - name: Install Rust stable - shell: bash run: rustup toolchain install stable --profile minimal + - name: Add Windows target + run: rustup target add $env:BANDSCOPE_TARGET_TRIPLE --toolchain stable + - name: Verify Windows antivirus baseline + shell: pwsh + run: | + function Write-AntivirusEvidence($message) { + Write-Host $message + if ($env:GITHUB_STEP_SUMMARY) { + Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value $message + } + } + + if (Get-Command Get-MpComputerStatus -ErrorAction SilentlyContinue) { + $status = Get-MpComputerStatus + Write-AntivirusEvidence "Antivirus check: Defender status AntivirusEnabled=$($status.AntivirusEnabled) RealTimeProtectionEnabled=$($status.RealTimeProtectionEnabled)." + if ($status.AntivirusEnabled) { + return + } + Write-AntivirusEvidence "Antivirus check: Defender telemetry is present but antivirus is not reported as enabled on this hosted runner." + } + + $products = Get-CimInstance -Namespace root/SecurityCenter2 -ClassName AntiVirusProduct -ErrorAction SilentlyContinue + if ($products) { + Write-AntivirusEvidence "Antivirus check: SecurityCenter2 reported at least one antivirus product." + return + } + + $defenderService = Get-Service -Name WinDefend -ErrorAction SilentlyContinue + if ($defenderService -and $defenderService.Status -in @('Running', 'StartPending')) { + Write-AntivirusEvidence "Antivirus check: WinDefend service is present and active." + return + } + + Write-AntivirusEvidence "Antivirus check: no explicit antivirus telemetry was available on this hosted runner." - name: Install node dependencies run: npm ci - name: Sync Python dependencies @@ -48,22 +87,29 @@ jobs: - name: Build frontend run: npm run build --workspace @bandscope/desktop - name: Build native shell - run: cargo +stable build --manifest-path apps/desktop/src-tauri/Cargo.toml --release --locked - - name: Package Windows artifact + run: cargo +stable build --manifest-path apps/desktop/src-tauri/Cargo.toml --release --locked --target $env:BANDSCOPE_TARGET_TRIPLE + - name: Package Windows amd64 artifact run: python scripts/release/package_desktop_artifact.py - - name: Upload Windows artifact + - name: Upload Windows amd64 artifact uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: - name: bandscope-windows-${{ github.sha }} + name: bandscope-windows-amd64-${{ github.sha }} path: | artifacts/*.zip artifacts/*.sha256 artifacts/*.manifest.txt - build-macos: - name: gate / build / macos - runs-on: macos-latest + + build-windows-arm64: + name: build / windows / arm64 + runs-on: windows-11-arm + strategy: + fail-fast: false permissions: contents: read + env: + BANDSCOPE_ARTIFACT_OS: windows + BANDSCOPE_ARTIFACT_ARCH: arm64 + BANDSCOPE_TARGET_TRIPLE: aarch64-pc-windows-msvc steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -80,6 +126,40 @@ jobs: version: "0.8.6" - name: Install Rust stable run: rustup toolchain install stable --profile minimal + - name: Add Windows arm target + run: rustup target add $env:BANDSCOPE_TARGET_TRIPLE --toolchain stable + - name: Verify Windows antivirus baseline + shell: pwsh + run: | + function Write-AntivirusEvidence($message) { + Write-Host $message + if ($env:GITHUB_STEP_SUMMARY) { + Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value $message + } + } + + if (Get-Command Get-MpComputerStatus -ErrorAction SilentlyContinue) { + $status = Get-MpComputerStatus + Write-AntivirusEvidence "Antivirus check: Defender status AntivirusEnabled=$($status.AntivirusEnabled) RealTimeProtectionEnabled=$($status.RealTimeProtectionEnabled)." + if ($status.AntivirusEnabled) { + return + } + Write-AntivirusEvidence "Antivirus check: Defender telemetry is present but antivirus is not reported as enabled on this hosted runner." + } + + $products = Get-CimInstance -Namespace root/SecurityCenter2 -ClassName AntiVirusProduct -ErrorAction SilentlyContinue + if ($products) { + Write-AntivirusEvidence "Antivirus check: SecurityCenter2 reported at least one antivirus product." + return + } + + $defenderService = Get-Service -Name WinDefend -ErrorAction SilentlyContinue + if ($defenderService -and $defenderService.Status -in @('Running', 'StartPending')) { + Write-AntivirusEvidence "Antivirus check: WinDefend service is present and active." + return + } + + Write-AntivirusEvidence "Antivirus check: no explicit antivirus telemetry was available on this hosted runner." - name: Install node dependencies run: npm ci - name: Sync Python dependencies @@ -87,32 +167,150 @@ jobs: - name: Build frontend run: npm run build --workspace @bandscope/desktop - name: Build native shell - run: cargo +stable build --manifest-path apps/desktop/src-tauri/Cargo.toml --release --locked - - name: Package macOS artifact + run: cargo +stable build --manifest-path apps/desktop/src-tauri/Cargo.toml --release --locked --target $env:BANDSCOPE_TARGET_TRIPLE + - name: Package Windows arm64 artifact + run: python scripts/release/package_desktop_artifact.py + - name: Upload Windows arm64 artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: bandscope-windows-arm64-${{ github.sha }} + path: | + artifacts/*.zip + artifacts/*.sha256 + artifacts/*.manifest.txt + + gate-windows: + name: gate / build / windows + runs-on: ubuntu-latest + needs: + - build-windows-native + - build-windows-arm64 + steps: + - name: Confirm both Windows architectures built + run: true + + build-macos-native: + name: build / macos / amd64 + runs-on: macos-15-intel + strategy: + fail-fast: false + permissions: + contents: read + env: + BANDSCOPE_ARTIFACT_OS: macos + BANDSCOPE_ARTIFACT_ARCH: amd64 + BANDSCOPE_TARGET_TRIPLE: x86_64-apple-darwin + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 22 + cache: npm + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5 + with: + version: "0.8.6" + - name: Install Rust stable + run: rustup toolchain install stable --profile minimal + - name: Add macOS Intel target + run: rustup target add "$BANDSCOPE_TARGET_TRIPLE" --toolchain stable + - name: Install node dependencies + run: npm ci + - name: Sync Python dependencies + run: uv sync --project services/analysis-engine --group dev --frozen + - name: Build frontend + run: npm run build --workspace @bandscope/desktop + - name: Build native shell + run: cargo +stable build --manifest-path apps/desktop/src-tauri/Cargo.toml --release --locked --target "$BANDSCOPE_TARGET_TRIPLE" + - name: Package macOS amd64 artifact run: python3 scripts/release/package_desktop_artifact.py - - name: Upload macOS artifact + - name: Upload macOS amd64 artifact uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: - name: bandscope-macos-${{ github.sha }} + name: bandscope-macos-amd64-${{ github.sha }} path: | artifacts/*.zip artifacts/*.sha256 artifacts/*.manifest.txt + build-macos-arm64: + name: build / macos / arm64 + runs-on: macos-15 + strategy: + fail-fast: false + permissions: + contents: read + env: + BANDSCOPE_ARTIFACT_OS: macos + BANDSCOPE_ARTIFACT_ARCH: arm64 + BANDSCOPE_TARGET_TRIPLE: aarch64-apple-darwin + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 22 + cache: npm + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5 + with: + version: "0.8.6" + - name: Install Rust stable + run: rustup toolchain install stable --profile minimal + - name: Add macOS arm target + run: rustup target add "$BANDSCOPE_TARGET_TRIPLE" --toolchain stable + - name: Install node dependencies + run: npm ci + - name: Sync Python dependencies + run: uv sync --project services/analysis-engine --group dev --frozen + - name: Build frontend + run: npm run build --workspace @bandscope/desktop + - name: Build native shell + run: cargo +stable build --manifest-path apps/desktop/src-tauri/Cargo.toml --release --locked --target "$BANDSCOPE_TARGET_TRIPLE" + - name: Package macOS arm64 artifact + run: python3 scripts/release/package_desktop_artifact.py + - name: Upload macOS arm64 artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: bandscope-macos-arm64-${{ github.sha }} + path: | + artifacts/*.zip + artifacts/*.sha256 + artifacts/*.manifest.txt + + gate-macos: + name: gate / build / macos + runs-on: ubuntu-latest + needs: + - build-macos-native + - build-macos-arm64 + steps: + - name: Confirm both macOS architectures built + run: true + attach-windows-release-artifact: name: release-artifact / windows if: github.event_name == 'release' runs-on: ubuntu-latest needs: - - build-windows + - build-windows-native + - build-windows-arm64 permissions: contents: write steps: - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: - name: bandscope-windows-${{ github.sha }} + pattern: bandscope-windows-*-${{ github.sha }} path: artifacts - - name: Attach Windows artifact to release + merge-multiple: true + - name: Attach Windows artifacts to release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TAG: ${{ github.event.release.tag_name }} @@ -123,15 +321,17 @@ jobs: if: github.event_name == 'release' runs-on: ubuntu-latest needs: - - build-macos + - build-macos-native + - build-macos-arm64 permissions: contents: write steps: - uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: - name: bandscope-macos-${{ github.sha }} + pattern: bandscope-macos-*-${{ github.sha }} path: artifacts - - name: Attach macOS artifact to release + merge-multiple: true + - name: Attach macOS artifacts to release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TAG: ${{ github.event.release.tag_name }} diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5480c49..d7b9ca9 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,6 +1,6 @@ # ARCHITECTURE.md -Last updated: 2026-03-10 +Last updated: 2026-03-11 ## Brand source @@ -22,6 +22,8 @@ Last updated: 2026-03-10 - Windows and macOS build security policy lives in `docs/security/cross-platform-build-policy.md`. - Target-OS builds are merge gates and release-validation controls, not optional compatibility checks. +- Windows amd64 + arm64 and macOS amd64 + arm64 are all part of the protected-branch and release-validation build baseline. +- Windows build runners should verify antivirus protection before native packaging begins. ## GitHub bootstrap source @@ -86,6 +88,8 @@ Last updated: 2026-03-10 - Supply-chain controls are part of the bootstrap architecture, not a release-afterthought. - Dependency review, audit, supply-chain inventory validation, and SBOM generation are expected protected-branch gates for both `develop` and `main`. - Cross-platform Windows and macOS build coverage is part of the bootstrap security architecture. +- Release artifacts, checksums, and manifests should encode both OS and architecture so packaged binaries remain traceable. +- Exact Windows 10 and macOS 24/25 GitHub-hosted coverage is a platform-capability constraint today; the current hosted baseline uses the closest published explicit runner labels and must move to self-hosted or larger runners if exact-version enforcement becomes mandatory. - GitHub-facing setup is staged: no-git -> local-git -> GitHub-connected -> protected-branches with required checks. - Shared contracts live in `packages/shared-types` so the UI can evolve without importing Python internals. - Shared contracts should ultimately model section, role, cue, confidence, and export artifacts explicitly enough that desktop UI and analysis outputs do not invent their own parallel schemas. @@ -96,7 +100,7 @@ Last updated: 2026-03-10 - `scripts/harness/quickcheck.sh` is the primary local verification entrypoint. - `scripts/checks/check_rust.sh` is an opt-in local Rust/Tauri gate used when the host has the native desktop toolchain ready. -- CI mirrors the default sequence for JS and Python, and adds a dedicated macOS Rust check job. +- CI mirrors the default sequence for JS and Python, and adds dedicated Windows/macOS native build coverage for both amd64 and arm64 runners. - Smoke-grade app verification is currently the React shell render plus Python engine health report. - Security docs and checks are part of the default quickcheck path so design drift is caught early. - Supply-chain docs, workflow pinning, and lockfile verification are part of the default quickcheck path so dependency drift is caught early. diff --git a/docs/security/cross-platform-build-policy.md b/docs/security/cross-platform-build-policy.md index a451127..921eb6a 100644 --- a/docs/security/cross-platform-build-policy.md +++ b/docs/security/cross-platform-build-policy.md @@ -4,6 +4,7 @@ BandScope ships to Windows and macOS. For that reason, Windows and macOS builds are security controls, not optional compatibility checks. +Each platform must be validated for both `amd64` and `arm64` packaging paths. Cross-platform builds help catch: @@ -14,10 +15,14 @@ Cross-platform builds help catch: ## Mandatory baseline -- every protected-branch change must build on Windows and macOS -- every release or tag validation must build on Windows and macOS +- every protected-branch change must build on Windows `amd64` + `arm64` +- every protected-branch change must build on macOS Intel + arm64 +- every release or tag validation must build on the same four OS/architecture combinations - build jobs must execute real dependency install, frontend build, native shell build, analysis engine packaging sanity, and artifact upload - build jobs must remain merge gates on both `develop` and `main` +- workflow runner labels must be explicit and architecture-stable rather than `*-latest` shortcuts when the shortcut hides one architecture +- Windows runner evidence should include an antivirus baseline check before packaging artifacts; hosted-runner telemetry may be incomplete, so the check records available Defender or SecurityCenter evidence rather than assuming real-time flags are always enabled +- exact Windows 10 and macOS 24/25 GitHub-hosted labels are not currently published in the GitHub-hosted runner catalog; if those exact versions become release gates, self-hosted or larger-runner capacity is required ## Required check names @@ -25,10 +30,11 @@ Cross-platform builds help catch: - `gate / build / macos` These are intended required checks for both `develop` and `main`. +Each gate must represent both architectures for its OS. ## Release connection -- release validation must produce Windows and macOS artifacts +- release validation must produce Windows amd64, Windows arm64, macOS amd64, and macOS arm64 artifacts - each artifact must have a checksum - release artifacts should stay linkable to SBOM artifacts and supplemental inventory - code signing and notarization readiness should be documented even if signing credentials are not present in CI yet @@ -37,8 +43,10 @@ These are intended required checks for both `develop` and `main`. Mark work as `BLOCKED` or `FAILED` if any of the following is missing: -- Windows build workflow path -- macOS build workflow path +- Windows amd64 build workflow path +- Windows arm64 build workflow path +- macOS amd64 build workflow path +- macOS arm64 build workflow path - release or tag build coverage - intended required checks recorded in repo docs - actual branch protection enforcement when GitHub admin context is required but unavailable diff --git a/docs/security/github-required-checks.md b/docs/security/github-required-checks.md index f995c94..fd32770 100644 --- a/docs/security/github-required-checks.md +++ b/docs/security/github-required-checks.md @@ -16,6 +16,9 @@ These are the merge-gate status checks that should be required on protected bran - `gate / build / windows` - `gate / build / macos` +`gate / build / windows` must cover both Windows `amd64` and Windows `arm64`. +`gate / build / macos` must cover both macOS Intel (`amd64`) and macOS `arm64`. + ### `main` - `CodeRabbit` @@ -46,6 +49,8 @@ These controls are expressed by repo workflows and are expected to be connected - `supply-chain-inventory`: supplemental validation baseline - `gate / build / windows`: intended required check - `gate / build / macos`: intended required check +- per-architecture desktop artifacts: required for Windows amd64/arm64 and macOS amd64/arm64 +- Windows build jobs: antivirus baseline evidence required before packaging - release-time SBOM artifact retention: required baseline - release-time supplemental inventory retention: required baseline @@ -55,6 +60,7 @@ These controls are expressed by repo workflows and are expected to be connected - CycloneDX JSON SBOM must be attached to the GitHub Release when the workflow runs on a Release event - `supply-chain/supplemental-component-inventory.json` must be uploaded as a GitHub Actions artifact and attached to the GitHub Release on Release events - packaged desktop artifacts and checksums should remain traceable from the same release record when the release workflow emits them +- release artifacts should include explicit OS/arch naming for Windows amd64, Windows arm64, macOS amd64, and macOS arm64 ## Enforcement note diff --git a/scripts/checks/verify_supply_chain.py b/scripts/checks/verify_supply_chain.py index 14aec6f..25f0a5c 100644 --- a/scripts/checks/verify_supply_chain.py +++ b/scripts/checks/verify_supply_chain.py @@ -124,16 +124,31 @@ def verify_workflow_coverage() -> list[str]: "push", "release:", "tags:", - "windows-latest", - "macos-latest", + "windows-2025", + "windows-11-arm", + "macos-15-intel", + "macos-15", "gate / build / windows", "gate / build / macos", "release-artifact / macos", "release-artifact / windows", "ubuntu-latest", + "bandscope-windows-amd64-${{ github.sha }}", + "bandscope-windows-arm64-${{ github.sha }}", + "bandscope-macos-amd64-${{ github.sha }}", + "bandscope-macos-arm64-${{ github.sha }}", + "Get-MpComputerStatus", ]: if build and token not in build: missing.append(f"build workflow missing token: {token}") + if build and "windows-latest" in build: + missing.append( + "build workflow should not rely on windows-latest for architecture coverage" + ) + if build and "macos-latest" in build: + missing.append( + "build workflow should not rely on macos-latest for architecture coverage" + ) return missing diff --git a/scripts/release/package_desktop_artifact.py b/scripts/release/package_desktop_artifact.py index 04dfb88..ceee43e 100644 --- a/scripts/release/package_desktop_artifact.py +++ b/scripts/release/package_desktop_artifact.py @@ -15,20 +15,52 @@ def sha256_file(path: Path) -> str: return digest.hexdigest() -def expected_binary_path(repo_root: Path) -> Path: +def normalized_platform() -> str: + if artifact_platform := os.environ.get("BANDSCOPE_ARTIFACT_OS"): + return artifact_platform + system = platform.system().lower() + if system == "darwin": + return "macos" + + return system + + +def normalized_architecture() -> str: + if artifact_arch := os.environ.get("BANDSCOPE_ARTIFACT_ARCH"): + return artifact_arch + + machine = platform.machine().lower() + if machine in {"x86_64", "amd64"}: + return "amd64" + if machine in {"arm64", "aarch64"}: + return "arm64" + + return machine + + +def artifact_identity() -> dict[str, str]: + git_sha = os.environ.get("GITHUB_SHA", "local")[:12] + target_platform = normalized_platform() + target_arch = normalized_architecture() + suffix = f"bandscope-{target_platform}-{target_arch}-{git_sha}" + return { + "platform": target_platform, + "arch": target_arch, + "archive_name": f"{suffix}.zip", + "manifest_name": f"{suffix}.manifest.txt", + } + + +def expected_binary_path(repo_root: Path) -> Path: + system = normalized_platform() binary_name = ( "bandscope-desktop.exe" if system == "windows" else "bandscope-desktop" ) - return ( - repo_root - / "apps" - / "desktop" - / "src-tauri" - / "target" - / "release" - / binary_name - ) + target_root = repo_root / "apps" / "desktop" / "src-tauri" / "target" + if target_triple := os.environ.get("BANDSCOPE_TARGET_TRIPLE"): + target_root = target_root / target_triple + return target_root / "release" / binary_name def main() -> int: @@ -54,9 +86,8 @@ def main() -> int: missing_list = ", ".join(missing_metadata) raise FileNotFoundError(f"Missing release metadata files: {missing_list}") - git_sha = os.environ.get("GITHUB_SHA", "local")[:12] - system = platform.system().lower() - archive_name = f"bandscope-{system}-{git_sha}.zip" + identity = artifact_identity() + archive_name = identity["archive_name"] archive_path = output_dir / archive_name with zipfile.ZipFile( @@ -77,11 +108,13 @@ def main() -> int: f"{sha256_file(archive_path)} {archive_name}\n", encoding="utf-8" ) - manifest_path = output_dir / f"bandscope-{system}-{git_sha}.manifest.txt" + manifest_path = output_dir / identity["manifest_name"] manifest_path.write_text( "\n".join( [ - f"platform={system}", + f"platform={identity['platform']}", + f"arch={identity['arch']}", + f"target_triple={os.environ.get('BANDSCOPE_TARGET_TRIPLE', 'native')}", f"binary={binary_path.name}", f"archive={archive_name}", f"checksum={checksum_path.name}", diff --git a/services/analysis-engine/tests/conftest.py b/services/analysis-engine/tests/conftest.py new file mode 100644 index 0000000..b942d14 --- /dev/null +++ b/services/analysis-engine/tests/conftest.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path +from types import ModuleType + + +def load_module(relative_path: str, module_name: str) -> ModuleType: + repo_root = Path(__file__).resolve().parents[3] + module_path = repo_root / relative_path + spec = spec_from_file_location(module_name, module_path) + assert spec is not None + assert spec.loader is not None + module = module_from_spec(spec) + spec.loader.exec_module(module) + return module diff --git a/services/analysis-engine/tests/test_release_packaging.py b/services/analysis-engine/tests/test_release_packaging.py new file mode 100644 index 0000000..f24f033 --- /dev/null +++ b/services/analysis-engine/tests/test_release_packaging.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +from pathlib import Path + +from conftest import load_module + + +def test_release_packaging_includes_architecture_in_artifact_identity( + monkeypatch, +) -> None: + packaging = load_module( + "scripts/release/package_desktop_artifact.py", "package_desktop_artifact" + ) + + monkeypatch.setenv("GITHUB_SHA", "abcdef1234567890") + monkeypatch.setenv("BANDSCOPE_ARTIFACT_OS", "windows") + monkeypatch.setenv("BANDSCOPE_ARTIFACT_ARCH", "arm64") + + artifact = packaging.artifact_identity() + + assert artifact == { + "platform": "windows", + "arch": "arm64", + "archive_name": "bandscope-windows-arm64-abcdef123456.zip", + "manifest_name": "bandscope-windows-arm64-abcdef123456.manifest.txt", + } + + +def test_expected_binary_path_uses_target_triple_when_provided(monkeypatch, tmp_path: Path) -> None: + packaging = load_module( + "scripts/release/package_desktop_artifact.py", "package_desktop_artifact_target" + ) + + monkeypatch.setenv("BANDSCOPE_TARGET_TRIPLE", "aarch64-apple-darwin") + + binary_path = packaging.expected_binary_path(tmp_path) + + assert binary_path == ( + tmp_path + / "apps" + / "desktop" + / "src-tauri" + / "target" + / "aarch64-apple-darwin" + / "release" + / "bandscope-desktop" + ) + + +def test_release_packaging_maps_darwin_to_macos(monkeypatch) -> None: + packaging = load_module( + "scripts/release/package_desktop_artifact.py", "package_desktop_artifact_platform" + ) + + monkeypatch.delenv("BANDSCOPE_ARTIFACT_OS", raising=False) + monkeypatch.setattr(packaging.platform, "system", lambda: "Darwin") + + assert packaging.normalized_platform() == "macos" + + +def test_release_packaging_main_writes_arch_specific_manifest(monkeypatch, tmp_path: Path) -> None: + packaging = load_module( + "scripts/release/package_desktop_artifact.py", "package_desktop_artifact_main" + ) + repo_root = tmp_path / "repo" + script_path = repo_root / "scripts" / "release" / "package_desktop_artifact.py" + script_path.parent.mkdir(parents=True) + script_path.write_text("# placeholder", encoding="utf-8") + binary_path = ( + repo_root + / "apps" + / "desktop" + / "src-tauri" + / "target" + / "aarch64-apple-darwin" + / "release" + / "bandscope-desktop" + ) + binary_path.parent.mkdir(parents=True) + binary_path.write_bytes(b"binary") + frontend_file = repo_root / "apps" / "desktop" / "dist" / "index.html" + frontend_file.parent.mkdir(parents=True) + frontend_file.write_text("", encoding="utf-8") + for metadata_path in [ + repo_root / "services" / "analysis-engine" / "uv.lock", + repo_root / "package-lock.json", + repo_root / "apps" / "desktop" / "src-tauri" / "Cargo.lock", + repo_root / "supply-chain" / "supplemental-component-inventory.json", + ]: + metadata_path.parent.mkdir(parents=True, exist_ok=True) + metadata_path.write_text("metadata", encoding="utf-8") + + monkeypatch.setattr(packaging, "__file__", str(script_path)) + monkeypatch.setenv("GITHUB_SHA", "1234567890abcdef") + monkeypatch.setenv("BANDSCOPE_ARTIFACT_OS", "macos") + monkeypatch.setenv("BANDSCOPE_ARTIFACT_ARCH", "arm64") + monkeypatch.setenv("BANDSCOPE_TARGET_TRIPLE", "aarch64-apple-darwin") + + assert packaging.main() == 0 + manifest_path = repo_root / "artifacts" / "bandscope-macos-arm64-1234567890ab.manifest.txt" + + assert manifest_path.exists() + assert "platform=macos" in manifest_path.read_text(encoding="utf-8") + assert "arch=arm64" in manifest_path.read_text(encoding="utf-8") diff --git a/services/analysis-engine/tests/test_supply_chain_policy.py b/services/analysis-engine/tests/test_supply_chain_policy.py new file mode 100644 index 0000000..7a34789 --- /dev/null +++ b/services/analysis-engine/tests/test_supply_chain_policy.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from pathlib import Path + +from conftest import load_module + + +def test_supply_chain_check_requires_multi_arch_runner_labels(monkeypatch, tmp_path: Path) -> None: + supply_chain = load_module("scripts/checks/verify_supply_chain.py", "verify_supply_chain") + + workflow_dir = tmp_path / ".github" / "workflows" + workflow_dir.mkdir(parents=True) + (workflow_dir / "build-baseline.yml").write_text( + """ +name: build-baseline +jobs: + build-windows: + runs-on: windows-latest + build-macos: + runs-on: macos-latest +""".strip(), + encoding="utf-8", + ) + for path in supply_chain.REQUIRED_FILES: + target = tmp_path / path + target.parent.mkdir(parents=True, exist_ok=True) + if not target.exists(): + target.write_text("placeholder", encoding="utf-8") + (tmp_path / ".github" / "dependabot.yml").write_text( + "\n".join( + [ + 'package-ecosystem: "npm"', + 'package-ecosystem: "pip"', + 'package-ecosystem: "cargo"', + 'package-ecosystem: "github-actions"', + ] + ), + encoding="utf-8", + ) + + monkeypatch.chdir(tmp_path) + + violations = supply_chain.verify_workflow_coverage() + + assert "build workflow missing token: windows-11-arm" in violations + assert "build workflow missing token: macos-15-intel" in violations + assert "build workflow missing token: bandscope-windows-arm64-${{ github.sha }}" in violations + assert "build workflow missing token: bandscope-macos-amd64-${{ github.sha }}" in violations + assert "build workflow missing token: Get-MpComputerStatus" in violations + + +def test_supply_chain_check_accepts_repo_multi_arch_workflow(monkeypatch) -> None: + supply_chain = load_module("scripts/checks/verify_supply_chain.py", "verify_supply_chain_repo") + repo_root = Path(__file__).resolve().parents[3] + + monkeypatch.chdir(repo_root) + + violations = supply_chain.verify_workflow_coverage() + + assert not any("build workflow missing token" in violation for violation in violations) + assert ( + "build workflow should not rely on windows-latest for architecture coverage" + not in violations + ) + assert ( + "build workflow should not rely on macos-latest for architecture coverage" not in violations + )