diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml index 5588f074157..9bc7d415d7b 100644 --- a/.github/workflows/release-cut.yml +++ b/.github/workflows/release-cut.yml @@ -1193,12 +1193,22 @@ jobs: with: ref: refs/tags/${{ needs.cut.outputs.tag }} + # GitHub reruns also resume jobs skipped behind a failed gate. Never + # recreate Windows signing requests on a rerun; reuse the assets from the + # original attempt and require a fresh dispatch if they are missing. + - name: Skip Windows artifact rebuild on rerun + if: matrix.platform == 'win' && github.run_attempt != 1 + shell: bash + run: | + echo "Windows artifact/signing steps are disabled on reruns (attempt $GITHUB_RUN_ATTEMPT)." + echo "Existing signed release assets must be reused; dispatch a fresh release only when a rebuild is required." >> "$GITHUB_STEP_SUMMARY" + # Why: `uses: ./…` resolves from the checked-out tag, not from the workflow # ref, so cutting from an older/off-main ref whose tree predates a composite # action would fail the step with "Can't find 'action.yml'". Restore the # actions directory from the commit this workflow file itself came from. - name: Restore composite actions from the workflow ref - if: matrix.platform == 'win' + if: matrix.platform == 'win' && github.run_attempt == 1 shell: bash env: WORKFLOW_SHA: ${{ github.workflow_sha }} @@ -1405,7 +1415,7 @@ jobs: # Why: SignPath signs GitHub workflow artifacts, so Windows builds must # upload only after the production-signed installer has been returned. - name: Build Windows release artifacts - if: matrix.platform == 'win' + if: matrix.platform == 'win' && github.run_attempt == 1 uses: nick-fields/retry@v4 with: timeout_minutes: 30 @@ -1416,7 +1426,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Verify Windows node-pty ConPTY runtime - if: matrix.platform == 'win' + if: matrix.platform == 'win' && github.run_attempt == 1 shell: pwsh run: | $runtimeDir = 'dist/win-unpacked/resources/node_modules/node-pty/build/Release' @@ -1433,7 +1443,7 @@ jobs: } - name: Install SignPath PowerShell module - if: matrix.platform == 'win' + if: matrix.platform == 'win' && github.run_attempt == 1 uses: ./.github/actions/install-signpath-module # ── Windows inner-binary signing (issue #7785) ───────────────────── @@ -1450,7 +1460,7 @@ jobs: # valid signature (Microsoft's OpenConsole.exe) must keep their signer. - name: Stage unsigned inner PE files for signing id: stage-inner - if: matrix.platform == 'win' + if: matrix.platform == 'win' && github.run_attempt == 1 continue-on-error: true shell: pwsh run: | @@ -1489,7 +1499,7 @@ jobs: - name: Upload unsigned inner binaries for SignPath id: upload-unsigned-inner - if: matrix.platform == 'win' && steps.stage-inner.outcome == 'success' + if: matrix.platform == 'win' && github.run_attempt == 1 && steps.stage-inner.outcome == 'success' continue-on-error: true uses: actions/upload-artifact@v7 with: @@ -1499,7 +1509,7 @@ jobs: - name: Submit inner binaries signing request id: submit-inner-signing - if: matrix.platform == 'win' && steps.upload-unsigned-inner.outcome == 'success' + if: matrix.platform == 'win' && github.run_attempt == 1 && steps.upload-unsigned-inner.outcome == 'success' continue-on-error: true uses: signpath/github-action-submit-signing-request@v2 with: @@ -1513,7 +1523,7 @@ jobs: - name: Notify Slack that inner-binary signing is waiting for approval id: notify-inner-signing - if: matrix.platform == 'win' && steps.submit-inner-signing.outcome == 'success' + if: matrix.platform == 'win' && github.run_attempt == 1 && steps.submit-inner-signing.outcome == 'success' continue-on-error: true shell: pwsh env: @@ -1581,7 +1591,7 @@ jobs: # falls through to today's unsigned-inner flow rather than blocking. - name: Download signed inner binaries from SignPath id: download-signed-inner - if: matrix.platform == 'win' && steps.submit-inner-signing.outcome == 'success' && steps.notify-inner-signing.outcome == 'success' + if: matrix.platform == 'win' && github.run_attempt == 1 && steps.submit-inner-signing.outcome == 'success' && steps.notify-inner-signing.outcome == 'success' continue-on-error: true shell: pwsh env: @@ -1604,7 +1614,7 @@ jobs: # shipping a mix of signed and unsigned binaries. - name: Restore signed inner binaries into unpacked app id: restore-signed-inner - if: matrix.platform == 'win' && steps.download-signed-inner.outcome == 'success' + if: matrix.platform == 'win' && github.run_attempt == 1 && steps.download-signed-inner.outcome == 'success' continue-on-error: true shell: pwsh run: | @@ -1645,7 +1655,7 @@ jobs: # unsigned again, which the evidence gate will flag. - name: Replace cached elevate.exe with the signed copy id: sign-elevate-cache - if: matrix.platform == 'win' && steps.restore-signed-inner.outcome == 'success' + if: matrix.platform == 'win' && github.run_attempt == 1 && steps.restore-signed-inner.outcome == 'success' continue-on-error: true shell: pwsh run: | @@ -1672,7 +1682,7 @@ jobs: - name: Rebuild NSIS installer from signed unpacked app id: rebuild-nsis-signed - if: matrix.platform == 'win' && steps.restore-signed-inner.outcome == 'success' + if: matrix.platform == 'win' && github.run_attempt == 1 && steps.restore-signed-inner.outcome == 'success' continue-on-error: true shell: pwsh run: | @@ -1689,7 +1699,7 @@ jobs: } - name: Roll back to original installer after failed rebuild - if: matrix.platform == 'win' && steps.rebuild-nsis-signed.outcome == 'failure' + if: matrix.platform == 'win' && github.run_attempt == 1 && steps.rebuild-nsis-signed.outcome == 'failure' shell: pwsh run: | if (Test-Path 'prepack-backup/orca-windows-setup.exe') { @@ -1699,7 +1709,7 @@ jobs: } # ── End Windows inner-binary signing ─────────────────────────────── - name: Upload unsigned Windows installer for SignPath - if: matrix.platform == 'win' + if: matrix.platform == 'win' && github.run_attempt == 1 id: upload-unsigned-windows-installer uses: actions/upload-artifact@v7 with: @@ -1711,7 +1721,7 @@ jobs: # so the release job waits while the signing request is approved in UI. - name: Submit Windows installer signing request id: submit-signing-request - if: matrix.platform == 'win' + if: matrix.platform == 'win' && github.run_attempt == 1 uses: signpath/github-action-submit-signing-request@v2 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} @@ -1723,7 +1733,7 @@ jobs: wait-for-completion: false - name: Notify Slack that Windows signing is waiting for approval - if: matrix.platform == 'win' + if: matrix.platform == 'win' && github.run_attempt == 1 shell: pwsh env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} @@ -1787,7 +1797,7 @@ jobs: Invoke-RestMethod -Method Post -Uri $env:SLACK_WEBHOOK_URL -ContentType 'application/json' -Body $payload - name: Download signed Windows installer from SignPath - if: matrix.platform == 'win' + if: matrix.platform == 'win' && github.run_attempt == 1 shell: pwsh env: SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} @@ -1805,7 +1815,7 @@ jobs: Expand-Archive -Path signed-windows.zip -DestinationPath signed-windows -Force - name: Stage signed Windows release assets - if: matrix.platform == 'win' + if: matrix.platform == 'win' && github.run_attempt == 1 shell: pwsh run: | $signedInstaller = Get-ChildItem -Path signed-windows -Recurse -File -Filter 'orca-windows-setup.exe' | Select-Object -First 1 @@ -1842,7 +1852,7 @@ jobs: Get-Item 'dist/orca-windows-setup.exe', 'dist/orca-windows-setup.exe.blockmap', 'dist/latest.yml' - name: Verify signed Windows installer - if: matrix.platform == 'win' + if: matrix.platform == 'win' && github.run_attempt == 1 shell: pwsh run: | $signature = Get-AuthenticodeSignature -FilePath 'dist/orca-windows-setup.exe' @@ -1860,7 +1870,7 @@ jobs: # proven on a real release, then flip ORCA_WINDOWS_INNER_SIGNATURE_REQUIRED # to 'true' so unsigned inner binaries block the release. - name: Verify Windows inner binary signatures - if: matrix.platform == 'win' + if: matrix.platform == 'win' && github.run_attempt == 1 shell: pwsh env: ORCA_WINDOWS_INNER_SIGNATURE_REQUIRED: 'false' @@ -1992,7 +2002,7 @@ jobs: if ($policyFailure) { throw $policyFailure } - name: Upload Windows inner signing evidence - if: always() && matrix.platform == 'win' + if: always() && matrix.platform == 'win' && github.run_attempt == 1 uses: actions/upload-artifact@v7 with: name: orca-windows-inner-signing-evidence-${{ needs.cut.outputs.tag }} @@ -2003,7 +2013,7 @@ jobs: retention-days: 30 - name: Publish signed Windows release artifacts - if: matrix.platform == 'win' + if: matrix.platform == 'win' && github.run_attempt == 1 uses: nick-fields/retry@v4 with: timeout_minutes: 10 diff --git a/config/scripts/windows-signing-workflow-contract.test.mjs b/config/scripts/windows-signing-workflow-contract.test.mjs index b5a8f41d01f..37edc2196d4 100644 --- a/config/scripts/windows-signing-workflow-contract.test.mjs +++ b/config/scripts/windows-signing-workflow-contract.test.mjs @@ -38,7 +38,7 @@ describe('Windows signing workflow contract', () => { const installStep = steps[installStepIndexes[0]] - expect(installStep.if).toBe("matrix.platform == 'win'") + expect(installStep.if).toBe("matrix.platform == 'win' && github.run_attempt == 1") expect(installStep.uses).toBe('./.github/actions/install-signpath-module') expect(installStep.run).toBeUndefined() @@ -139,6 +139,34 @@ describe('Windows signing workflow contract', () => { expect(installRun).toContain('throw "SHA-256 mismatch for $source') }) + it('never recreates Windows signing requests on a workflow rerun', () => { + const parsedWorkflow = readWorkflow('.github/workflows/release-cut.yml') + const steps = parsedWorkflow.jobs.build.steps + const stepNames = steps.map((step) => step.name) + const skipStep = steps.find((step) => step.name === 'Skip Windows artifact rebuild on rerun') + + expect(skipStep?.if).toBe("matrix.platform == 'win' && github.run_attempt != 1") + expect(skipStep?.run).toContain('Existing signed release assets must be reused') + + const signingStepNames = [ + 'Build Windows release artifacts', + 'Stage unsigned inner PE files for signing', + 'Upload unsigned inner binaries for SignPath', + 'Submit inner binaries signing request', + 'Download signed inner binaries from SignPath', + 'Upload unsigned Windows installer for SignPath', + 'Submit Windows installer signing request', + 'Download signed Windows installer from SignPath', + 'Stage signed Windows release assets', + 'Publish signed Windows release artifacts' + ] + + for (const stepName of signingStepNames) { + const step = steps[stepNames.indexOf(stepName)] + expect(step?.if, stepName).toContain('github.run_attempt == 1') + } + }) + it('shares one SignPath module install path between release and rehearsal', () => { const rehearsalWorkflow = readWorkflow('.github/workflows/windows-signing-rehearsal.yml') const stepNames = rehearsalWorkflow.jobs.rehearse.steps.map((step) => step.name)