@@ -715,7 +715,7 @@ jobs:
715715 ~/Library/Caches/electron-builder
716716 - os : windows-latest
717717 platform : win
718- release_command : ' node config/scripts/ensure-native-runtime.mjs --runtime=electron; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; pnpm exec electron-builder --config config/electron-builder.config.cjs --win --publish always '
718+ release_command : ' node config/scripts/ensure-native-runtime.mjs --runtime=electron; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; pnpm exec electron-builder --config config/electron-builder.config.cjs --win --publish never '
719719 eb_cache_path : |
720720 ~\AppData\Local\electron\Cache
721721 ~\AppData\Local\electron-builder\Cache
@@ -729,6 +729,7 @@ jobs:
729729 runs-on : ${{ matrix.os }}
730730
731731 permissions :
732+ actions : read
732733 contents : write
733734
734735 steps :
@@ -890,8 +891,8 @@ jobs:
890891 APPLE_APP_SPECIFIC_PASSWORD : ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
891892 APPLE_TEAM_ID : ${{ secrets.APPLE_TEAM_ID }}
892893
893- - name : Publish release artifacts
894- if : matrix.platform != 'mac '
894+ - name : Publish release artifacts (Linux)
895+ if : matrix.platform == 'linux '
895896 uses : nick-fields/retry@v4
896897 with :
897898 timeout_minutes : 30
@@ -901,6 +902,164 @@ jobs:
901902 env :
902903 GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
903904
905+ # Why: SignPath signs GitHub workflow artifacts, so Windows builds must
906+ # upload only after the production-signed installer has been returned.
907+ - name : Build Windows release artifacts
908+ if : matrix.platform == 'win'
909+ uses : nick-fields/retry@v4
910+ with :
911+ timeout_minutes : 30
912+ max_attempts : 3
913+ retry_wait_seconds : 30
914+ command : ${{ matrix.release_command }}
915+ env :
916+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
917+
918+ - name : Upload unsigned Windows installer for SignPath
919+ if : matrix.platform == 'win'
920+ id : upload-unsigned-windows-installer
921+ uses : actions/upload-artifact@v7
922+ with :
923+ name : orca-windows-unsigned-${{ needs.cut.outputs.tag }}
924+ path : dist/orca-windows-setup.exe
925+ if-no-files-found : error
926+
927+ # Why: SignPath Foundation production certificates require manual review,
928+ # so the release job waits while the signing request is approved in UI.
929+ - name : Submit Windows installer signing request
930+ id : submit-signing-request
931+ if : matrix.platform == 'win'
932+ uses : signpath/github-action-submit-signing-request@v2
933+ with :
934+ api-token : ${{ secrets.SIGNPATH_API_TOKEN }}
935+ organization-id : c37aa192-a27a-4377-9c90-5d6c95912dc0
936+ project-slug : orca
937+ signing-policy-slug : release-signing
938+ artifact-configuration-slug : github-actions-windows-installer
939+ github-artifact-id : ${{ steps.upload-unsigned-windows-installer.outputs.artifact-id }}
940+ wait-for-completion : false
941+
942+ - name : Notify Slack that Windows signing is waiting for approval
943+ if : matrix.platform == 'win'
944+ shell : pwsh
945+ env :
946+ SLACK_WEBHOOK_URL : ${{ secrets.SLACK_WEBHOOK_URL }}
947+ SIGNPATH_ORGANIZATION_ID : c37aa192-a27a-4377-9c90-5d6c95912dc0
948+ SIGNPATH_REQUEST_ID : ${{ steps.submit-signing-request.outputs.signing-request-id }}
949+ SIGNPATH_REQUEST_URL : ${{ steps.submit-signing-request.outputs.signing-request-web-url }}
950+ TAG : ${{ needs.cut.outputs.tag }}
951+ GITHUB_RUN_URL : https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
952+ run : |
953+ if ([string]::IsNullOrWhiteSpace($env:SLACK_WEBHOOK_URL)) {
954+ throw 'SLACK_WEBHOOK_URL secret is required so release approvers know when SignPath is waiting.'
955+ }
956+
957+ $requestUrl = $env:SIGNPATH_REQUEST_URL
958+ if ([string]::IsNullOrWhiteSpace($requestUrl)) {
959+ $requestUrl = "https://app.signpath.io/Web/$env:SIGNPATH_ORGANIZATION_ID/SigningRequests/$env:SIGNPATH_REQUEST_ID"
960+ }
961+
962+ $message = "Orca Windows release $env:TAG is ready for SignPath approval.`n<$requestUrl|Open SignPath signing request>`n<$env:GITHUB_RUN_URL|Open GitHub Actions run>"
963+ $payload = @{
964+ text = $message
965+ blocks = @(
966+ @{
967+ type = 'section'
968+ text = @{
969+ type = 'mrkdwn'
970+ text = $message
971+ }
972+ }
973+ )
974+ } | ConvertTo-Json -Depth 5
975+
976+ Invoke-RestMethod -Method Post -Uri $env:SLACK_WEBHOOK_URL -ContentType 'application/json' -Body $payload
977+
978+ - name : Install SignPath PowerShell module
979+ if : matrix.platform == 'win'
980+ shell : pwsh
981+ run : |
982+ Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
983+ Install-Module -Name SignPath -MinimumVersion 4.0.0 -MaximumVersion 4.999.999 -Scope CurrentUser -Force
984+
985+ - name : Download signed Windows installer from SignPath
986+ if : matrix.platform == 'win'
987+ shell : pwsh
988+ env :
989+ SIGNPATH_API_TOKEN : ${{ secrets.SIGNPATH_API_TOKEN }}
990+ SIGNPATH_REQUEST_ID : ${{ steps.submit-signing-request.outputs.signing-request-id }}
991+ run : |
992+ Get-SignedArtifact `
993+ -OrganizationId c37aa192-a27a-4377-9c90-5d6c95912dc0 `
994+ -ApiToken $env:SIGNPATH_API_TOKEN `
995+ -SigningRequestId $env:SIGNPATH_REQUEST_ID `
996+ -OutputArtifactPath signed-windows.zip `
997+ -Force `
998+ -WaitForCompletionTimeoutInSeconds 14400
999+
1000+ New-Item -ItemType Directory -Path signed-windows -Force
1001+ Expand-Archive -Path signed-windows.zip -DestinationPath signed-windows -Force
1002+
1003+ - name : Stage signed Windows release assets
1004+ if : matrix.platform == 'win'
1005+ shell : pwsh
1006+ run : |
1007+ $signedInstaller = Get-ChildItem -Path signed-windows -Recurse -File -Filter 'orca-windows-setup.exe' | Select-Object -First 1
1008+ if ($null -eq $signedInstaller) {
1009+ throw 'Signed Windows installer was not returned by SignPath.'
1010+ }
1011+
1012+ Copy-Item -Path $signedInstaller.FullName -Destination 'dist/orca-windows-setup.exe' -Force
1013+ & 'node_modules/app-builder-bin/win/x64/app-builder.exe' blockmap --input 'dist/orca-windows-setup.exe' --output 'dist/orca-windows-setup.exe.blockmap'
1014+
1015+ $installer = Get-Item 'dist/orca-windows-setup.exe'
1016+ $blockmap = Get-Item 'dist/orca-windows-setup.exe.blockmap'
1017+ $stream = [System.IO.File]::OpenRead($installer.FullName)
1018+ try {
1019+ $sha512 = [System.Security.Cryptography.SHA512]::Create()
1020+ $hash = [Convert]::ToBase64String($sha512.ComputeHash($stream))
1021+ } finally {
1022+ if ($null -ne $sha512) {
1023+ $sha512.Dispose()
1024+ }
1025+ $stream.Dispose()
1026+ }
1027+
1028+ $latestYml = Get-Content -Path 'dist/latest.yml' -Raw
1029+ $latestYml = [regex]::Replace($latestYml, '(?m)^(\s*)sha512: .+$', {
1030+ param($match)
1031+ "$($match.Groups[1].Value)sha512: $hash"
1032+ })
1033+ $latestYml = $latestYml -replace '(?m)^ size: \d+$', " size: $($installer.Length)"
1034+ $latestYml = $latestYml -replace '(?m)^ blockMapSize: \d+$', " blockMapSize: $($blockmap.Length)"
1035+ Set-Content -Path 'dist/latest.yml' -Value $latestYml -NoNewline
1036+
1037+ Get-Item 'dist/orca-windows-setup.exe', 'dist/orca-windows-setup.exe.blockmap', 'dist/latest.yml'
1038+
1039+ - name : Verify signed Windows installer
1040+ if : matrix.platform == 'win'
1041+ shell : pwsh
1042+ run : |
1043+ $signature = Get-AuthenticodeSignature -FilePath 'dist/orca-windows-setup.exe'
1044+ if ($signature.Status -ne 'Valid') {
1045+ throw ($signature | Format-List * | Out-String)
1046+ }
1047+ if ($signature.SignerCertificate.Subject -notlike '*CN=SignPath Foundation*') {
1048+ throw "Unexpected Windows signer: $($signature.SignerCertificate.Subject)"
1049+ }
1050+ $signature.SignerCertificate | Format-List Subject,Issuer,NotBefore,NotAfter,Thumbprint
1051+
1052+ - name : Publish signed Windows release artifacts
1053+ if : matrix.platform == 'win'
1054+ uses : nick-fields/retry@v4
1055+ with :
1056+ timeout_minutes : 10
1057+ max_attempts : 3
1058+ retry_wait_seconds : 30
1059+ command : gh release upload "${{ needs.cut.outputs.tag }}" "dist/orca-windows-setup.exe" "dist/orca-windows-setup.exe.blockmap" "dist/latest.yml" --clobber --repo "${{ github.repository }}"
1060+ env :
1061+ GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
1062+
9041063 - name : Verify release remains draft after artifact upload
9051064 # Why: the build matrix must never be the actor that exposes a partial
9061065 # release. If an uploader or GitHub transition flips draft early, fail
@@ -925,10 +1084,10 @@ jobs:
9251084 exit 1
9261085 fi
9271086
928- # Why post-publish (not pre-publish) : electron-builder packs and
929- # uploads in a single `--publish always` invocation, so there is no
930- # cheap insertion point between pack and upload without splitting
931- # that step. Running verify last still blocks the bad release: the
1087+ # Why post-publish for macOS/Linux : electron-builder packs and uploads
1088+ # in a single `--publish always` invocation, so there is no cheap
1089+ # insertion point between pack and upload without splitting those steps.
1090+ # Running verify last still blocks the bad release: the
9321091 # binary is uploaded to the draft, but a failed matrix job blocks
9331092 # the `publish-release` job (which depends on `build`) from flipping
9341093 # the release from draft → published, so users never see it. A human
0 commit comments