diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 23d0da60..09e23fbd 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -12,6 +12,11 @@ concurrency: group: desktop-release-${{ github.ref }} cancel-in-progress: false +# A manual run is a release rehearsal: both platforms build and go through the +# same signing gates a tag does, with nothing published and the installers +# attached to the run instead. Only the release-repo steps stay tag-gated, so a +# credential that has expired or a configuration Electron Builder no longer +# accepts surfaces on demand rather than on the tag that needed it. jobs: # Create one draft before either platform uploads. It stays private until both # jobs and the final downloaded-asset validation succeed. @@ -179,11 +184,11 @@ jobs: echo 'No macOS signing certificate configured; building unsigned.' fi - # A tag build that quietly produces an unsigned app is worse than a failed + # A build that quietly produces an unsigned app is worse than a failed # one: macOS rejects unsigned updates, so it ships a release users cannot - # install or update from. Manual runs stay free to build unsigned. - - name: Require signing for tagged releases - if: startsWith(github.ref, 'refs/tags/desktop-v') + # install or update from. Manual runs are held to the same bar, which is + # what makes them a rehearsal rather than a smoke test. + - name: Require signing working-directory: apps/desktop run: node --import tsx scripts/assert-release-signing.ts @@ -206,7 +211,6 @@ jobs: node --import tsx apps/desktop/scripts/verify-update-manifest.ts mac apps/desktop/dist "$DESKTOP_VERSION" - name: Verify macOS signatures and notarization - if: startsWith(github.ref, 'refs/tags/desktop-v') shell: bash run: | set -euo pipefail @@ -313,9 +317,9 @@ jobs: working-directory: apps/desktop run: node --import tsx scripts/stage-runtime.ts - # Only non-empty WIN_CSC_* signing secrets are exported. Without them, - # Windows artifacts are unsigned and installers trigger a SmartScreen - # warning on first run. + # Only non-empty WIN_CSC_* signing secrets are exported. Without them the + # artifacts are unsigned and installers trigger a SmartScreen warning, so + # the credential check below fails the run rather than shipping one. - name: Resolve Windows signing credentials shell: bash env: @@ -347,8 +351,7 @@ jobs: if [ -n "$value" ]; then printf '%s<<%s\n%s\n%s\n' "$name" "$delimiter" "$value" "$delimiter" >> "$GITHUB_ENV"; fi done - - name: Require signing for tagged Windows releases - if: startsWith(github.ref, 'refs/tags/desktop-v') + - name: Require signing working-directory: apps/desktop run: node --import tsx scripts/assert-windows-release-signing.ts @@ -366,7 +369,6 @@ jobs: node --import tsx apps/desktop/scripts/verify-update-manifest.ts win apps/desktop/dist "$DESKTOP_VERSION" - name: Verify Windows release signatures - if: startsWith(github.ref, 'refs/tags/desktop-v') working-directory: apps/desktop run: node --import tsx scripts/verify-windows-signatures.ts diff --git a/apps/desktop/tests/desktop-release-workflow.spec.ts b/apps/desktop/tests/desktop-release-workflow.spec.ts index 9f335ea7..043df82f 100644 --- a/apps/desktop/tests/desktop-release-workflow.spec.ts +++ b/apps/desktop/tests/desktop-release-workflow.spec.ts @@ -41,6 +41,28 @@ describe('desktop release workflow', () => { expect(workflow).toContain("if: startsWith(github.ref, 'refs/tags/desktop-v')\n shell: bash\n env:\n GH_TOKEN:") }) + it('runs every signing gate on a manual rehearsal, not only on a tag', () => { + // A manual run is the only chance to prove the signing path before the tag + // that depends on it. Gating these behind the tag would make a rehearsal + // pass while the credentials it was meant to exercise were already broken. + const rehearsed = [ + 'scripts/assert-release-signing.ts', + 'scripts/assert-windows-release-signing.ts', + 'scripts/verify-windows-signatures.ts', + 'xcrun stapler validate', + ] + for (const step of rehearsed) { + const stepIndex = workflow.indexOf(step) + expect(stepIndex).toBeGreaterThan(-1) + const precedingStep = workflow.lastIndexOf(' - name:', stepIndex) + // Any condition at all, not just a tag test, would skip one trigger: + // `github.event_name == 'workflow_dispatch'` reintroduces the same hole + // from the other side. These steps must be unconditional. + const declaration = workflow.slice(precedingStep, stepIndex) + expect(declaration).not.toMatch(/^\s+if:/mu) + } + }) + it('downloads and revalidates uploaded assets before publishing', () => { expect(workflow).toContain('gh release download "$RELEASE_TAG"') expect(workflow).toContain('scripts/verify-update-manifest.ts mac release-assets')