diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ba9021f..05367b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,11 +67,34 @@ jobs: $wheel = @(Get-ChildItem "release-assets/*.whl") $installers = @(Get-ChildItem "desktop/src-tauri/target/release/bundle/nsis/*.exe") if ($wheel.Count -ne 1 -or $installers.Count -ne 1) { throw "expected one wheel and one NSIS installer" } - Copy-Item $installers[0].FullName release-assets/ + $installerAsset = Join-Path release-assets "ArcheAxis.OS-Windows-x64-setup.exe" + Copy-Item $installers[0].FullName $installerAsset Copy-Item .hermes/desktop-runtime-v1/runtime/release-identity.json release-assets/ - python scripts/release_checksum.py --wheel $wheel[0].FullName --installer (Join-Path release-assets $installers[0].Name) --artifact release-assets/release-identity.json --output release-assets/SHA256SUMS.txt + python scripts/release_checksum.py --wheel $wheel[0].FullName --installer $installerAsset --artifact release-assets/release-identity.json --output release-assets/SHA256SUMS.txt + - name: Verify checksum manifest payload equality + shell: pwsh + run: | + $manifestNames = @(Get-Content release-assets/SHA256SUMS.txt | ForEach-Object { + if ($_ -notmatch '^[0-9a-fA-F]{64} (.+)$') { throw "invalid SHA256SUMS line: $_" } + $Matches[1] + }) + $payloadNames = @(Get-ChildItem release-assets -File | Where-Object Name -ne 'SHA256SUMS.txt' | ForEach-Object Name) + $manifestComparison = Compare-Object ($manifestNames | Sort-Object) ($payloadNames | Sort-Object) + if ($manifestComparison) { throw "SHA256SUMS payload set differs from staged payload set: $($manifestComparison | Out-String)" } + if ($manifestNames.Count -ne 3 -or ($manifestNames | Select-Object -Unique).Count -ne 3) { + throw "expected exactly three unique checksum-bound payloads" + } - name: Publish verified assets env: GH_TOKEN: ${{ github.token }} shell: pwsh - run: gh release create $env:GITHUB_REF_NAME release-assets/* --verify-tag --title "ArcheAxis OS $env:GITHUB_REF_NAME" --generate-notes + run: | + $wheel = @(Get-ChildItem "release-assets/*.whl") + if ($wheel.Count -ne 1) { throw "expected exactly one release wheel" } + $releaseAssets = @( + $wheel[0].FullName, + (Resolve-Path "release-assets/ArcheAxis.OS-Windows-x64-setup.exe").Path, + (Resolve-Path "release-assets/release-identity.json").Path, + (Resolve-Path "release-assets/SHA256SUMS.txt").Path + ) + gh release create $env:GITHUB_REF_NAME $releaseAssets --verify-tag --title "ArcheAxis OS $env:GITHUB_REF_NAME" --generate-notes diff --git a/tests/test_release_manifest.py b/tests/test_release_manifest.py index e668122..2f917bf 100644 --- a/tests/test_release_manifest.py +++ b/tests/test_release_manifest.py @@ -269,6 +269,31 @@ def test_release_checksum_script_refuses_no_artifacts(tmp_path) -> None: assert result.returncode != 0, "should have failed without artifact arguments" +def test_release_workflow_stages_installer_under_provider_stable_name() -> None: + """The checksum filename must equal the name exposed by GitHub Releases.""" + root = Path(__file__).resolve().parents[1] + workflow = (root / ".github" / "workflows" / "release.yml").read_text( + encoding="utf-8" + ) + + assert 'ArcheAxis.OS-Windows-x64-setup.exe' in workflow + assert 'Copy-Item $installers[0].FullName $installerAsset' in workflow + assert '--installer $installerAsset' in workflow + + +def test_release_workflow_publishes_only_checksum_bound_allowlist() -> None: + """No wildcard or unlisted staging payload may reach a public release.""" + root = Path(__file__).resolve().parents[1] + workflow = (root / ".github" / "workflows" / "release.yml").read_text( + encoding="utf-8" + ) + + assert 'gh release create $env:GITHUB_REF_NAME release-assets/*' not in workflow + assert 'Verify checksum manifest payload equality' in workflow + assert '$releaseAssets = @(' in workflow + assert 'gh release create $env:GITHUB_REF_NAME $releaseAssets' in workflow + + def test_release_identity_injection_manifests_exact_commit_and_tree(tmp_path) -> None: """Verify scripts/release_inject_identity.py writes valid identity manifest.""" import json