Просмотр исходного кода

Updated .github/workflows/windows-installer.yml

maziggy 4 недель назад
Родитель
Сommit
1748d7cefa
1 измененных файлов с 91 добавлено и 4 удалено
  1. 91 4
      .github/workflows/windows-installer.yml

+ 91 - 4
.github/workflows/windows-installer.yml

@@ -6,29 +6,47 @@ name: Windows Installer
 #   - Tag push matching v* (release builds, uploaded as a release asset)
 #   - Manual dispatch (for testing the build pipeline)
 #
-# The installer is unsigned until SignPath OSS approval lands. Once it
-# does, add the SignPath GitHub Action between the ISCC step and the
-# upload step.
+# Release tags are Authenticode-signed through the SignPath Foundation OSS
+# program. Daily prereleases are deliberately left unsigned so they don't burn
+# the OSS signing quota; use the `sign` dispatch input to exercise the signing
+# path by hand.
 
 on:
   push:
     tags:
       - 'v*'
   workflow_dispatch:
+    inputs:
+      sign:
+        description: 'Submit the installer to SignPath for signing'
+        type: boolean
+        default: false
 
 # Least-privilege per CodeQL actions/missing-workflow-permissions.
 # contents: write is required by softprops/action-gh-release to attach
 # the .exe to a tag release; the manual-dispatch path doesn't trigger
 # that step and could run with read-only, but a single workflow-level
 # block keeps the surface auditable in one place.
+# actions: read lets the SignPath connector download the uploaded artifact
+# through the API. Declaring a permissions block at all drops every scope we
+# don't name to `none`, so the signing step fails to fetch the artifact
+# without it.
 permissions:
   contents: write
+  actions: read
 
 jobs:
   build:
     runs-on: windows-latest
     timeout-minutes: 30
 
+    env:
+      # Sign real release tags but not `-daily.` prereleases, and let a manual
+      # run opt in. GitHub's `||` returns the *last* operand when everything is
+      # falsy (an empty string here, not `false`), so every use site compares
+      # against the string 'true' rather than treating this as a boolean.
+      SIGN: ${{ (startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-daily.')) || inputs.sign }}
+
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -57,6 +75,73 @@ jobs:
           & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" bambuddy.iss
         shell: pwsh
 
+      # SignPath signs a *GitHub artifact*, not a workspace path: the connector
+      # pulls the artifact back out through the API, which is why this upload
+      # has to happen before signing and why upload-artifact must be v4 or newer
+      # (older versions expose no `artifact-id` output). Kept as a separate,
+      # clearly-named artifact so an unsigned build is never mistaken for a
+      # signed one when downloading from the run page.
+      - name: Upload unsigned installer
+        id: upload_unsigned
+        uses: actions/upload-artifact@v7
+        with:
+          name: bambuddy-windows-installer-unsigned
+          path: installers/windows/build/output/*.exe
+          if-no-files-found: error
+
+      # The artifact arrives at SignPath as a .zip (that is simply what
+      # upload-artifact produces), so the artifact configuration on the SignPath
+      # side describes a <zip-file> wrapping the <pe-file>. With skip-decompress
+      # left at its default the signed archive is extracted again here, so
+      # `signed/` ends up holding the bare .exe.
+      - name: Sign installer (SignPath)
+        if: env.SIGN == 'true'
+        uses: signpath/github-action-submit-signing-request@v2
+        with:
+          api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
+          # Not a credential -- the organization ID appears in ordinary SignPath
+          # URLs and is useless without the API token above.
+          organization-id: '4d7e5b59-d0fb-4a6b-b385-b861e18c6386'
+          project-slug: 'bambuddy'
+          signing-policy-slug: 'test-signing'
+          github-artifact-id: ${{ steps.upload_unsigned.outputs.artifact-id }}
+          wait-for-completion: true
+          output-artifact-directory: installers/windows/build/signed
+
+      # Replace the unsigned binary in-place so every downstream step (alias,
+      # artifact upload, release attachment) keeps working off one directory and
+      # cannot accidentally publish the unsigned copy.
+      - name: Promote signed installer
+        if: env.SIGN == 'true'
+        shell: pwsh
+        working-directory: installers/windows/build
+        run: |
+          $signed = @(Get-ChildItem -Path signed -Filter *.exe)
+          if ($signed.Count -ne 1) {
+            throw "expected exactly one signed .exe, found $($signed.Count)"
+          }
+          Move-Item -Force $signed[0].FullName (Join-Path output $signed[0].Name)
+          Write-Host "promoted signed installer: $($signed[0].Name)"
+
+      # Fail loudly rather than shipping an unsigned .exe under a signed
+      # release. The test certificate is self-signed, so Windows reports the
+      # signature as untrusted (`UnknownError`) -- that is expected and is not
+      # what this checks. Only the absence of a signature is treated as a
+      # failure; swap in a stricter assertion once the production certificate
+      # is imported.
+      - name: Verify signature
+        if: env.SIGN == 'true'
+        shell: pwsh
+        working-directory: installers/windows/build/output
+        run: |
+          Get-ChildItem -Filter *.exe | ForEach-Object {
+            $sig = Get-AuthenticodeSignature $_.FullName
+            if ($sig.Status -eq 'NotSigned') {
+              throw "$($_.Name) carries no Authenticode signature"
+            }
+            Write-Host "$($_.Name): $($sig.Status) / $($sig.SignerCertificate.Subject)"
+          }
+
       # Stable + beta tag releases (e.g. v0.2.5b1, v0.3.0) get an unversioned
       # copy alongside the versioned filename so external surfaces (website,
       # wiki, newsletters) can link to a stable URL that survives version
@@ -70,6 +155,8 @@ jobs:
       # semantically confusing next to the date-stamped versioned name on a
       # daily prerelease page, and (b) there's no stable "latest daily" URL
       # anyway (`latest` skips prereleases), so the alias adds no value there.
+      #
+      # Runs after signing so the alias is a copy of the *signed* binary.
       - name: Create unversioned alias (non-daily tags only)
         if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-daily.')
         shell: pwsh
@@ -81,7 +168,7 @@ jobs:
           Write-Host "alias: bambuddy-windows-x64-setup.exe -> $($versioned.Name)"
 
       - name: Upload installer artifact
-        uses: actions/upload-artifact@v4
+        uses: actions/upload-artifact@v7
         with:
           name: bambuddy-windows-installer
           path: installers/windows/build/output/*.exe