Explorar el Código

feat(ci): use Azure Artifact Signing for Windows releases (#15201)

Luke Parker hace 4 meses
padre
commit
82fc493520

+ 183 - 1
.github/workflows/publish.yml

@@ -98,15 +98,129 @@ jobs:
       - uses: actions/upload-artifact@v4
         with:
           name: opencode-cli
-          path: packages/opencode/dist
+          path: |
+            packages/opencode/dist/opencode-darwin*
+            packages/opencode/dist/opencode-linux*
+
+      - uses: actions/upload-artifact@v4
+        with:
+          name: opencode-cli-windows
+          path: packages/opencode/dist/opencode-windows*
     outputs:
       version: ${{ needs.version.outputs.version }}
 
+  sign-cli-windows:
+    needs:
+      - build-cli
+      - version
+    runs-on: blacksmith-4vcpu-windows-2025
+    if: github.repository == 'anomalyco/opencode'
+    env:
+      AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
+      AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
+      AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+      AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
+      AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
+      AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
+    steps:
+      - uses: actions/checkout@v3
+
+      - uses: actions/download-artifact@v4
+        with:
+          name: opencode-cli-windows
+          path: packages/opencode/dist
+
+      - name: Setup git committer
+        id: committer
+        uses: ./.github/actions/setup-git-committer
+        with:
+          opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
+          opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}
+
+      - name: Azure login
+        uses: azure/login@v2
+        with:
+          client-id: ${{ env.AZURE_CLIENT_ID }}
+          tenant-id: ${{ env.AZURE_TENANT_ID }}
+          subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
+
+      - uses: azure/artifact-signing-action@v1
+        with:
+          endpoint: ${{ env.AZURE_TRUSTED_SIGNING_ENDPOINT }}
+          signing-account-name: ${{ env.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
+          certificate-profile-name: ${{ env.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
+          files: |
+            ${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe
+            ${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe
+            ${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe
+          exclude-environment-credential: true
+          exclude-workload-identity-credential: true
+          exclude-managed-identity-credential: true
+          exclude-shared-token-cache-credential: true
+          exclude-visual-studio-credential: true
+          exclude-visual-studio-code-credential: true
+          exclude-azure-cli-credential: false
+          exclude-azure-powershell-credential: true
+          exclude-azure-developer-cli-credential: true
+          exclude-interactive-browser-credential: true
+
+      - name: Verify Windows CLI signatures
+        shell: pwsh
+        run: |
+          $files = @(
+            "${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe",
+            "${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe",
+            "${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe"
+          )
+
+          foreach ($file in $files) {
+            $sig = Get-AuthenticodeSignature $file
+            if ($sig.Status -ne "Valid") {
+              throw "Invalid signature for ${file}: $($sig.Status)"
+            }
+          }
+
+      - name: Repack Windows CLI archives
+        working-directory: packages/opencode/dist
+        shell: pwsh
+        run: |
+          Compress-Archive -Path "opencode-windows-arm64\bin\*" -DestinationPath "opencode-windows-arm64.zip" -Force
+          Compress-Archive -Path "opencode-windows-x64\bin\*" -DestinationPath "opencode-windows-x64.zip" -Force
+          Compress-Archive -Path "opencode-windows-x64-baseline\bin\*" -DestinationPath "opencode-windows-x64-baseline.zip" -Force
+
+      - name: Upload signed Windows CLI release assets
+        if: needs.version.outputs.release != ''
+        shell: pwsh
+        env:
+          GH_TOKEN: ${{ steps.committer.outputs.token }}
+        run: |
+          gh release upload "v${{ needs.version.outputs.version }}" `
+            "${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64.zip" `
+            "${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64.zip" `
+            "${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline.zip" `
+            --clobber `
+            --repo "${{ needs.version.outputs.repo }}"
+
+      - uses: actions/upload-artifact@v4
+        with:
+          name: opencode-cli-signed-windows
+          path: |
+            packages/opencode/dist/opencode-windows-arm64
+            packages/opencode/dist/opencode-windows-x64
+            packages/opencode/dist/opencode-windows-x64-baseline
+
   build-tauri:
     needs:
       - build-cli
       - version
     continue-on-error: false
+    env:
+      AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
+      AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
+      AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+      AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
+      AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
+      AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
     strategy:
       fail-fast: false
       matrix:
@@ -152,6 +266,14 @@ jobs:
 
       - uses: ./.github/actions/setup-bun
 
+      - name: Azure login
+        if: runner.os == 'Windows'
+        uses: azure/login@v2
+        with:
+          client-id: ${{ env.AZURE_CLIENT_ID }}
+          tenant-id: ${{ env.AZURE_TENANT_ID }}
+          subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
+
       - uses: actions/setup-node@v4
         with:
           node-version: "24"
@@ -190,6 +312,7 @@ jobs:
         env:
           OPENCODE_VERSION: ${{ needs.version.outputs.version }}
           GITHUB_TOKEN: ${{ steps.committer.outputs.token }}
+          OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }}
           RUST_TARGET: ${{ matrix.settings.target }}
           GH_TOKEN: ${{ github.token }}
           GITHUB_RUN_ID: ${{ github.run_id }}
@@ -246,11 +369,34 @@ jobs:
           APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
           APPLE_API_KEY_PATH: ${{ runner.temp }}/apple-api-key.p8
 
+      - name: Verify signed Windows desktop artifacts
+        if: runner.os == 'Windows'
+        shell: pwsh
+        run: |
+          $files = @(
+            "${{ github.workspace }}\packages\desktop\src-tauri\sidecars\opencode-cli-${{ matrix.settings.target }}.exe"
+          )
+          $files += Get-ChildItem "${{ github.workspace }}\packages\desktop\src-tauri\target\${{ matrix.settings.target }}\release\bundle\nsis\*.exe" | Select-Object -ExpandProperty FullName
+
+          foreach ($file in $files) {
+            $sig = Get-AuthenticodeSignature $file
+            if ($sig.Status -ne "Valid") {
+              throw "Invalid signature for ${file}: $($sig.Status)"
+            }
+          }
+
   build-electron:
     needs:
       - build-cli
       - version
     continue-on-error: false
+    env:
+      AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
+      AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
+      AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+      AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
+      AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
+      AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
     strategy:
       fail-fast: false
       matrix:
@@ -292,6 +438,14 @@ jobs:
 
       - uses: ./.github/actions/setup-bun
 
+      - name: Azure login
+        if: runner.os == 'Windows'
+        uses: azure/login@v2
+        with:
+          client-id: ${{ env.AZURE_CLIENT_ID }}
+          tenant-id: ${{ env.AZURE_TENANT_ID }}
+          subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }}
+
       - uses: actions/setup-node@v4
         with:
           node-version: "24"
@@ -326,6 +480,7 @@ jobs:
         env:
           OPENCODE_VERSION: ${{ needs.version.outputs.version }}
           OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
+          OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }}
           RUST_TARGET: ${{ matrix.settings.target }}
           GH_TOKEN: ${{ github.token }}
           GITHUB_RUN_ID: ${{ github.run_id }}
@@ -358,6 +513,22 @@ jobs:
         env:
           OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }}
 
+      - name: Verify signed Windows Electron artifacts
+        if: runner.os == 'Windows'
+        shell: pwsh
+        run: |
+          $files = @()
+          $files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*.exe" | Select-Object -ExpandProperty FullName
+          $files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*unpacked\*.exe" | Select-Object -ExpandProperty FullName
+          $files += Get-ChildItem "${{ github.workspace }}\packages\desktop-electron\dist\*unpacked\resources\opencode-cli.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
+
+          foreach ($file in $files | Select-Object -Unique) {
+            $sig = Get-AuthenticodeSignature $file
+            if ($sig.Status -ne "Valid") {
+              throw "Invalid signature for ${file}: $($sig.Status)"
+            }
+          }
+
       - uses: actions/upload-artifact@v4
         with:
           name: opencode-electron-${{ matrix.settings.target }}
@@ -373,6 +544,7 @@ jobs:
     needs:
       - version
       - build-cli
+      - sign-cli-windows
       - build-tauri
       - build-electron
     runs-on: blacksmith-4vcpu-ubuntu-2404
@@ -411,6 +583,16 @@ jobs:
           name: opencode-cli
           path: packages/opencode/dist
 
+      - uses: actions/download-artifact@v4
+        with:
+          name: opencode-cli-windows
+          path: packages/opencode/dist
+
+      - uses: actions/download-artifact@v4
+        with:
+          name: opencode-cli-signed-windows
+          path: packages/opencode/dist
+
       - uses: actions/download-artifact@v4
         if: needs.version.outputs.release
         with:

+ 0 - 54
.github/workflows/sign-cli.yml

@@ -1,54 +0,0 @@
-name: sign-cli
-
-on:
-  push:
-    branches:
-      - brendan/desktop-signpath
-  workflow_dispatch:
-
-permissions:
-  contents: read
-  actions: read
-
-jobs:
-  sign-cli:
-    runs-on: blacksmith-4vcpu-ubuntu-2404
-    if: github.repository == 'anomalyco/opencode'
-    steps:
-      - uses: actions/checkout@v3
-        with:
-          fetch-tags: true
-
-      - uses: ./.github/actions/setup-bun
-
-      - name: Build
-        run: |
-          ./packages/opencode/script/build.ts
-
-      - name: Upload unsigned Windows CLI
-        id: upload_unsigned_windows_cli
-        uses: actions/upload-artifact@v4
-        with:
-          name: unsigned-opencode-windows-cli
-          path: packages/opencode/dist/opencode-windows-x64/bin/opencode.exe
-          if-no-files-found: error
-
-      - name: Submit SignPath signing request
-        id: submit_signpath_signing_request
-        uses: signpath/github-action-submit-signing-request@v1
-        with:
-          api-token: ${{ secrets.SIGNPATH_API_KEY }}
-          organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
-          project-slug: ${{ secrets.SIGNPATH_PROJECT_SLUG }}
-          signing-policy-slug: ${{ secrets.SIGNPATH_SIGNING_POLICY_SLUG }}
-          artifact-configuration-slug: ${{ secrets.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }}
-          github-artifact-id: ${{ steps.upload_unsigned_windows_cli.outputs.artifact-id }}
-          wait-for-completion: true
-          output-artifact-directory: signed-opencode-cli
-
-      - name: Upload signed Windows CLI
-        uses: actions/upload-artifact@v4
-        with:
-          name: signed-opencode-windows-cli
-          path: signed-opencode-cli/*.exe
-          if-no-files-found: error

+ 0 - 5
.signpath/policies/opencode/test-signing.yml

@@ -1,5 +0,0 @@
-github-policies:
-  runners:
-    allowed_groups:
-      - "GitHub Actions"
-      - "blacksmith runners 01kbd5v56sg8tz7rea39b7ygpt"

+ 23 - 0
packages/desktop-electron/electron-builder.config.ts

@@ -1,5 +1,25 @@
+import { execFile } from "node:child_process"
+import path from "node:path"
+import { fileURLToPath } from "node:url"
+import { promisify } from "node:util"
+
 import type { Configuration } from "electron-builder"
 
+const execFileAsync = promisify(execFile)
+const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..")
+const signScript = path.join(rootDir, "script", "sign-windows.ps1")
+
+async function signWindows(configuration: { path: string }) {
+  if (process.platform !== "win32") return
+  if (process.env.GITHUB_ACTIONS !== "true") return
+
+  await execFileAsync(
+    "pwsh",
+    ["-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", signScript, configuration.path],
+    { cwd: rootDir },
+  )
+}
+
 const channel = (() => {
   const raw = process.env.OPENCODE_CHANNEL
   if (raw === "dev" || raw === "beta" || raw === "prod") return raw
@@ -44,6 +64,9 @@ const getBase = (): Configuration => ({
   },
   win: {
     icon: `resources/icons/icon.ico`,
+    signtoolOptions: {
+      sign: signWindows,
+    },
     target: ["nsis"],
   },
   nsis: {

+ 2 - 1
packages/desktop-electron/scripts/prepare.ts

@@ -13,11 +13,12 @@ await Bun.write("./package.json", JSON.stringify(pkg, null, 2) + "\n")
 console.log(`Updated package.json version to ${Script.version}`)
 
 const sidecarConfig = getCurrentSidecar()
+const artifact = process.env.OPENCODE_CLI_ARTIFACT ?? "opencode-cli"
 
 const dir = "resources/opencode-binaries"
 
 await $`mkdir -p ${dir}`
-await $`gh run download ${Bun.env.GITHUB_RUN_ID} -n opencode-cli`.cwd(dir)
+await $`gh run download ${process.env.GITHUB_RUN_ID} -n ${artifact}`.cwd(dir)
 
 await copyBinaryToSidecarFolder(windowsify(`${dir}/${sidecarConfig.ocBinary}/bin/opencode`))
 

+ 3 - 0
packages/desktop-electron/scripts/utils.ts

@@ -63,6 +63,9 @@ export async function copyBinaryToSidecarFolder(source: string) {
   await $`mkdir -p ${dir}`
   const dest = windowsify(`${dir}/opencode-cli`)
   await $`cp ${source} ${dest}`
+  if (process.platform === "win32" && process.env.GITHUB_ACTIONS === "true") {
+    await $`pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File ../../script/sign-windows.ps1 ${dest}`
+  }
   if (process.platform === "darwin") await $`codesign --force --sign - ${dest}`
 
   console.log(`Copied ${source} to ${dest}`)

+ 2 - 1
packages/desktop/scripts/prepare.ts

@@ -10,10 +10,11 @@ await Bun.write("./package.json", JSON.stringify(pkg, null, 2) + "\n")
 console.log(`Updated package.json version to ${Script.version}`)
 
 const sidecarConfig = getCurrentSidecar()
+const artifact = process.env.OPENCODE_CLI_ARTIFACT ?? "opencode-cli"
 
 const dir = "src-tauri/target/opencode-binaries"
 
 await $`mkdir -p ${dir}`
-await $`gh run download ${Bun.env.GITHUB_RUN_ID} -n opencode-cli`.cwd(dir)
+await $`gh run download ${process.env.GITHUB_RUN_ID} -n ${artifact}`.cwd(dir)
 
 await copyBinaryToSidecarFolder(windowsify(`${dir}/${sidecarConfig.ocBinary}/bin/opencode`))

+ 3 - 0
packages/desktop/scripts/utils.ts

@@ -48,6 +48,9 @@ export async function copyBinaryToSidecarFolder(source: string, target = RUST_TA
   await $`mkdir -p src-tauri/sidecars`
   const dest = windowsify(`src-tauri/sidecars/opencode-cli-${target}`)
   await $`cp ${source} ${dest}`
+  if (process.platform === "win32" && process.env.GITHUB_ACTIONS === "true") {
+    await $`pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File ../../script/sign-windows.ps1 ${dest}`
+  }
 
   console.log(`Copied ${source} to ${dest}`)
 }

+ 4 - 0
packages/desktop/src-tauri/tauri.beta.conf.json

@@ -12,6 +12,10 @@
       "icons/beta/icon.ico"
     ],
     "windows": {
+      "signCommand": {
+        "cmd": "powershell",
+        "args": ["-ExecutionPolicy", "Bypass", "-File", "../../../script/sign-windows.ps1", "%1"]
+      },
       "nsis": {
         "installerIcon": "icons/beta/icon.ico"
       }

+ 4 - 0
packages/desktop/src-tauri/tauri.conf.json

@@ -45,6 +45,10 @@
       "entitlements": "./entitlements.plist"
     },
     "windows": {
+      "signCommand": {
+        "cmd": "powershell",
+        "args": ["-ExecutionPolicy", "Bypass", "-File", "../../../script/sign-windows.ps1", "%1"]
+      },
       "nsis": {
         "installerIcon": "icons/dev/icon.ico",
         "headerImage": "assets/nsis-header.bmp",

+ 4 - 0
packages/desktop/src-tauri/tauri.prod.conf.json

@@ -12,6 +12,10 @@
       "icons/prod/icon.ico"
     ],
     "windows": {
+      "signCommand": {
+        "cmd": "powershell",
+        "args": ["-ExecutionPolicy", "Bypass", "-File", "../../../script/sign-windows.ps1", "%1"]
+      },
       "nsis": {
         "installerIcon": "icons/prod/icon.ico"
       }

+ 70 - 0
script/sign-windows.ps1

@@ -0,0 +1,70 @@
+param(
+  [Parameter(ValueFromRemainingArguments = $true)]
+  [string[]] $Path
+)
+
+$ErrorActionPreference = "Stop"
+
+if (-not $Path -or $Path.Count -eq 0) {
+  throw "At least one path is required"
+}
+
+if ($env:GITHUB_ACTIONS -ne "true") {
+  Write-Host "Skipping Windows signing because this is not running on GitHub Actions"
+  exit 0
+}
+
+$vars = @{
+  endpoint = $env:AZURE_TRUSTED_SIGNING_ENDPOINT
+  account = $env:AZURE_TRUSTED_SIGNING_ACCOUNT_NAME
+  profile = $env:AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE
+}
+
+if ($vars.Values | Where-Object { -not $_ }) {
+  Write-Host "Skipping Windows signing because Azure Artifact Signing is not configured"
+  exit 0
+}
+
+$moduleVersion = "0.5.8"
+$module = Get-Module -ListAvailable -Name TrustedSigning | Where-Object { $_.Version -eq [version] $moduleVersion }
+
+if (-not $module) {
+  try {
+    Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser | Out-Null
+  }
+  catch {
+    Write-Host "NuGet package provider install skipped: $($_.Exception.Message)"
+  }
+
+  Install-Module -Name TrustedSigning -RequiredVersion $moduleVersion -Force -Repository PSGallery -Scope CurrentUser
+}
+
+Import-Module TrustedSigning -RequiredVersion $moduleVersion -Force
+
+$files = @($Path | ForEach-Object { Resolve-Path $_ -ErrorAction SilentlyContinue } | Select-Object -ExpandProperty Path -Unique)
+
+if (-not $files -or $files.Count -eq 0) {
+  throw "No files matched the requested paths"
+}
+
+$params = @{
+  Endpoint                         = $vars.endpoint
+  CodeSigningAccountName           = $vars.account
+  CertificateProfileName           = $vars.profile
+  Files                            = ($files -join ",")
+  FileDigest                       = "SHA256"
+  TimestampDigest                  = "SHA256"
+  TimestampRfc3161                 = "http://timestamp.acs.microsoft.com"
+  ExcludeEnvironmentCredential     = $true
+  ExcludeWorkloadIdentityCredential = $true
+  ExcludeManagedIdentityCredential = $true
+  ExcludeSharedTokenCacheCredential = $true
+  ExcludeVisualStudioCredential    = $true
+  ExcludeVisualStudioCodeCredential = $true
+  ExcludeAzureCliCredential        = $false
+  ExcludeAzurePowerShellCredential = $true
+  ExcludeAzureDeveloperCliCredential = $true
+  ExcludeInteractiveBrowserCredential = $true
+}
+
+Invoke-TrustedSigning @params