Address CodeRabbit review + fix contract test for inner signing

- Update package-electron-runtime-contract test: the old test pinned the
  pre-inner-signing layout (publish immediately after installer verify);
  now pins the new fail-open contract (inner verify + evidence upload
  between verify and publish, warn-only gate, continue-on-error chain).
- Budget SignPath waits under the 360-minute hosted-job cap: explicit
  timeout-minutes on build, inner-signing wait 4h -> 1h (missing the
  window falls through to today's unsigned-inner flow).
- Wrap the evidence gate in try/catch so unexpected script errors cannot
  block a release while the gate is warn-only.
- Rehearsal workflow: persist-credentials false, same 1h inner wait.
This commit is contained in:
Jinwoo Hong
2026-07-08 23:38:54 -04:00
parent 068c71bb27
commit 3e9054c737
3 changed files with 80 additions and 38 deletions
+47 -33
View File
@@ -719,6 +719,10 @@ jobs:
~/.cache/electron-builder
runs-on: ${{ matrix.os }}
# Why: hosted runners hard-cap jobs at 6h; the Windows SignPath waits
# (1h inner + 4h installer) are budgeted to fit under this with the
# build itself, so a slow approval can't kill the job mid-flow.
timeout-minutes: 360
permissions:
actions: read
@@ -1072,8 +1076,10 @@ jobs:
Invoke-RestMethod -Method Post -Uri $env:SLACK_WEBHOOK_URL -ContentType 'application/json' -Body $payload
# Why gate on the notify outcome too: if nobody was told to approve,
# don't hold the release for the 4-hour approval window — fall through
# and ship like today instead.
# don't hold the release for the approval window — fall through and
# ship like today instead. The 1h wait (vs the installer's 4h) keeps
# both waits plus the build inside the 360-minute job cap; missing it
# 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'
@@ -1089,7 +1095,7 @@ jobs:
-SigningRequestId $env:SIGNPATH_REQUEST_ID `
-OutputArtifactPath signed-inner.zip `
-Force `
-WaitForCompletionTimeoutInSeconds 14400
-WaitForCompletionTimeoutInSeconds 3600
New-Item -ItemType Directory -Path signed-inner -Force
Expand-Archive -Path signed-inner.zip -DestinationPath signed-inner -Force
@@ -1305,40 +1311,48 @@ jobs:
exit 0
}
$report = New-Object System.Collections.Generic.List[string]
$failures = New-Object System.Collections.Generic.List[string]
# Why try/catch: while the gate is warn-only, even an unexpected
# script error (extraction hiccup, missing file) must not block
# the release — only the flip to required makes failures fatal.
try {
$report = New-Object System.Collections.Generic.List[string]
$failures = New-Object System.Collections.Generic.List[string]
# Why: verify the files a user actually gets on disk, not the build
# tree — 7z parses the NSIS exe directly as its embedded payload.
$7za = 'node_modules/7zip-bin/win/x64/7za.exe'
New-Item -ItemType Directory -Path inner-evidence-extract -Force | Out-Null
& $7za x 'dist/orca-windows-setup.exe' '-oinner-evidence-extract' -y | Out-Null
# Why: verify the files a user actually gets on disk, not the build
# tree — 7z parses the NSIS exe directly as its embedded payload.
$7za = 'node_modules/7zip-bin/win/x64/7za.exe'
New-Item -ItemType Directory -Path inner-evidence-extract -Force | Out-Null
& $7za x 'dist/orca-windows-setup.exe' '-oinner-evidence-extract' -y | Out-Null
$root = Resolve-Path 'inner-evidence-extract'
foreach ($relative in Get-Content 'inner-signing-list.txt') {
$path = Join-Path $root $relative
if (-not (Test-Path $path)) {
$failures.Add("missing from installer payload: $relative")
continue
$root = Resolve-Path 'inner-evidence-extract'
foreach ($relative in Get-Content 'inner-signing-list.txt') {
$path = Join-Path $root $relative
if (-not (Test-Path $path)) {
$failures.Add("missing from installer payload: $relative")
continue
}
$signature = Get-AuthenticodeSignature -FilePath $path
$subject = if ($null -eq $signature.SignerCertificate) { '<none>' } else { $signature.SignerCertificate.Subject }
$line = "{0,-14} {1} <{2}>" -f $signature.Status, $relative, $subject
$report.Add($line)
Write-Host $line
if ($signature.Status -ne 'Valid' -or $subject -notlike '*CN=SignPath Foundation*') {
$failures.Add("not signed by SignPath Foundation: $relative ($($signature.Status), $subject)")
}
}
$signature = Get-AuthenticodeSignature -FilePath $path
$subject = if ($null -eq $signature.SignerCertificate) { '<none>' } else { $signature.SignerCertificate.Subject }
$line = "{0,-14} {1} <{2}>" -f $signature.Status, $relative, $subject
$report.Add($line)
Write-Host $line
if ($signature.Status -ne 'Valid' -or $subject -notlike '*CN=SignPath Foundation*') {
$failures.Add("not signed by SignPath Foundation: $relative ($($signature.Status), $subject)")
}
}
Set-Content -Path 'inner-signing-evidence.txt' -Value ($report -join "`n")
if ($failures.Count -gt 0) {
$failures | ForEach-Object { Write-Host "::warning::$_" }
$message = "Windows inner-binary evidence gate found $($failures.Count) problems."
if ($required) { throw $message }
Write-Host "::warning::$message Fail-open until ORCA_WINDOWS_INNER_SIGNATURE_REQUIRED is 'true'."
} else {
Write-Host "All $((Get-Content 'inner-signing-list.txt').Count) inner binaries in the shipped installer are signed by SignPath Foundation."
Set-Content -Path 'inner-signing-evidence.txt' -Value ($report -join "`n")
if ($failures.Count -gt 0) {
$failures | ForEach-Object { Write-Host "::warning::$_" }
$message = "Windows inner-binary evidence gate found $($failures.Count) problems."
if ($required) { throw $message }
Write-Host "::warning::$message Fail-open until ORCA_WINDOWS_INNER_SIGNATURE_REQUIRED is 'true'."
} else {
Write-Host "All $((Get-Content 'inner-signing-list.txt').Count) inner binaries in the shipped installer are signed by SignPath Foundation."
}
} catch {
if ($required) { throw }
Write-Host "::warning::Windows inner-binary evidence gate errored: $_ (fail-open, issue #7785)."
}
- name: Upload Windows inner signing evidence
@@ -41,6 +41,9 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
with:
# Why: nothing here pushes; keep the token out of .git/config.
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@v6
@@ -180,7 +183,7 @@ jobs:
-SigningRequestId $env:SIGNPATH_REQUEST_ID `
-OutputArtifactPath signed-inner.zip `
-Force `
-WaitForCompletionTimeoutInSeconds 14400
-WaitForCompletionTimeoutInSeconds 3600
New-Item -ItemType Directory -Path signed-inner -Force
Expand-Archive -Path signed-inner.zip -DestinationPath signed-inner -Force
@@ -221,7 +221,7 @@ describe('Electron runtime package contract', () => {
expect(installRun).not.toMatch(/throw\s+\$_/)
})
it('temporarily allows publishing Windows after verifying the signed installer only', () => {
it('verifies Windows inner binary signatures fail-open before publishing', () => {
const releaseWorkflow = readFileSync(
join(projectDir, '.github/workflows/release-cut.yml'),
'utf8'
@@ -230,12 +230,37 @@ describe('Electron runtime package contract', () => {
const steps = parsedWorkflow.jobs.build.steps
const stepNames = steps.map((step) => step.name)
const outerVerifyIndex = stepNames.indexOf('Verify signed Windows installer')
const innerVerifyIndex = stepNames.indexOf('Verify signed Windows inner executable')
const innerVerifyIndex = stepNames.indexOf('Verify Windows inner binary signatures')
const evidenceIndex = stepNames.indexOf('Upload Windows inner signing evidence')
const publishIndex = stepNames.indexOf('Publish signed Windows release artifacts')
expect(outerVerifyIndex).toBeGreaterThan(-1)
expect(innerVerifyIndex).toBe(-1)
expect(publishIndex).toBe(outerVerifyIndex + 1)
expect(innerVerifyIndex).toBe(outerVerifyIndex + 1)
expect(evidenceIndex).toBe(innerVerifyIndex + 1)
expect(publishIndex).toBe(evidenceIndex + 1)
// Why fail-open: unsigned inner binaries must warn, not block, until the
// flow is proven on a real release (issue #7785). Flip this to 'true'
// together with the workflow env to make the gate required.
expect(steps[innerVerifyIndex].env.ORCA_WINDOWS_INNER_SIGNATURE_REQUIRED).toBe('false')
// Why: every step in the inner-signing chain must be unable to fail the
// release — a SignPath outage or timeout falls through to today's
// unsigned-inner flow instead of blocking the cut.
const innerChainStepNames = [
'Stage unsigned inner PE files for signing',
'Upload unsigned inner binaries for SignPath',
'Submit inner binaries signing request',
'Notify Slack that inner-binary signing is waiting for approval',
'Download signed inner binaries from SignPath',
'Restore signed inner binaries into unpacked app',
'Rebuild NSIS installer from signed unpacked app'
]
for (const stepName of innerChainStepNames) {
const step = steps[stepNames.indexOf(stepName)]
expect(step, stepName).toBeDefined()
expect(step['continue-on-error'], stepName).toBe(true)
}
})
it('publishes both Linux release matrix entries', () => {