diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8d4c9038a7e..83106e47b6d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -103,9 +103,93 @@ jobs: outputs: version: ${{ needs.version.outputs.version }} + sign-cli-windows: + needs: + - build-cli + - version + runs-on: blacksmith-4vcpu-windows-2025 + if: github.repository == 'anomalyco/opencode' + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: Install sign tool + shell: pwsh + run: | + dotnet tool install --tool-path "$env:RUNNER_TEMP\sign" --prerelease sign + "$env:RUNNER_TEMP\sign" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Azure login + uses: azure/login@v2 + with: + client-id: ${{ vars.AZURE_CLIENT_ID || secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID || secrets.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID || secrets.AZURE_SUBSCRIPTION_ID }} + + - uses: actions/download-artifact@v4 + with: + name: opencode-cli + path: packages/opencode/dist + + - name: Sign Windows CLI binaries + shell: pwsh + env: + AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME || secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ vars.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE || secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }} + AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ vars.AZURE_TRUSTED_SIGNING_ENDPOINT || secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} + run: | + ./script/sign-windows.ps1 ` + "${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe" ` + "${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe" + + - name: Verify Windows CLI signatures + shell: pwsh + run: | + $files = @( + "${{ 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 + shell: pwsh + working-directory: packages/opencode/dist + run: | + 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: ${{ github.token }} + run: | + gh release upload "v${{ needs.version.outputs.version }}" ` + "${{ 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-x64 + packages/opencode/dist/opencode-windows-x64-baseline + build-tauri: needs: - build-cli + - sign-cli-windows - version continue-on-error: false strategy: @@ -150,6 +234,26 @@ jobs: - uses: ./.github/actions/setup-bun + - uses: actions/setup-dotnet@v4 + if: runner.os == 'Windows' + with: + dotnet-version: "10.0.x" + + - name: Install sign tool + if: runner.os == 'Windows' + shell: pwsh + run: | + dotnet tool install --tool-path "$env:RUNNER_TEMP\sign" --prerelease sign + "$env:RUNNER_TEMP\sign" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Azure login + if: runner.os == 'Windows' + uses: azure/login@v2 + with: + client-id: ${{ vars.AZURE_CLIENT_ID || secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID || secrets.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID || secrets.AZURE_SUBSCRIPTION_ID }} + - name: Cache apt packages if: contains(matrix.settings.host, 'ubuntu') uses: actions/cache@v4 @@ -184,6 +288,7 @@ jobs: env: OPENCODE_VERSION: ${{ needs.version.outputs.version }} GITHUB_TOKEN: ${{ steps.committer.outputs.token }} + OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-signed-windows') || 'opencode-cli' }} RUST_TARGET: ${{ matrix.settings.target }} GH_TOKEN: ${{ github.token }} GITHUB_RUN_ID: ${{ github.run_id }} @@ -230,6 +335,9 @@ jobs: releaseCommitish: ${{ github.sha }} env: GITHUB_TOKEN: ${{ steps.committer.outputs.token }} + AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME || secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ vars.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE || secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }} + AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ vars.AZURE_TRUSTED_SIGNING_ENDPOINT || secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} TAURI_BUNDLER_NEW_APPIMAGE_FORMAT: true TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} @@ -240,10 +348,28 @@ 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", + "${{ github.workspace }}\packages\desktop\src-tauri\target\${{ matrix.settings.target }}\release\OpenCode.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)" + } + } + publish: needs: - version - build-cli + - sign-cli-windows - build-tauri runs-on: blacksmith-4vcpu-ubuntu-2404 steps: @@ -281,6 +407,11 @@ jobs: name: opencode-cli path: packages/opencode/dist + - uses: actions/download-artifact@v4 + with: + name: opencode-cli-signed-windows + path: packages/opencode/dist + - name: Cache apt packages (AUR) uses: actions/cache@v4 with: diff --git a/.github/workflows/sign-cli.yml b/.github/workflows/sign-cli.yml deleted file mode 100644 index d9d61fd800e..00000000000 --- a/.github/workflows/sign-cli.yml +++ /dev/null @@ -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 diff --git a/packages/desktop/scripts/prepare.ts b/packages/desktop/scripts/prepare.ts index d802f2d89ee..dc2c793f349 100755 --- a/packages/desktop/scripts/prepare.ts +++ b/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 = Bun.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 ${Bun.env.GITHUB_RUN_ID} -n ${artifact}`.cwd(dir) await copyBinaryToSidecarFolder(windowsify(`${dir}/${sidecarConfig.ocBinary}/bin/opencode`)) diff --git a/packages/desktop/src-tauri/tauri.beta.conf.json b/packages/desktop/src-tauri/tauri.beta.conf.json index 4dd7879933c..f2cf1d267e7 100644 --- a/packages/desktop/src-tauri/tauri.beta.conf.json +++ b/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" } diff --git a/packages/desktop/src-tauri/tauri.conf.json b/packages/desktop/src-tauri/tauri.conf.json index d5ca15b8a71..265044625b1 100644 --- a/packages/desktop/src-tauri/tauri.conf.json +++ b/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", diff --git a/packages/desktop/src-tauri/tauri.prod.conf.json b/packages/desktop/src-tauri/tauri.prod.conf.json index 0416c59cbb9..39561e45dc6 100644 --- a/packages/desktop/src-tauri/tauri.prod.conf.json +++ b/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" } diff --git a/script/sign-windows.ps1 b/script/sign-windows.ps1 new file mode 100644 index 00000000000..c39ae3d6591 --- /dev/null +++ b/script/sign-windows.ps1 @@ -0,0 +1,51 @@ +param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]] $Path +) + +$ErrorActionPreference = "Stop" + +if (-not $Path -or $Path.Count -eq 0) { + throw "At least one path is required" +} + +$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 Trusted Signing is not configured" + exit 0 +} + +if (-not (Get-Command sign -ErrorAction SilentlyContinue)) { + Write-Host "Skipping Windows signing because sign was not found on PATH" + exit 0 +} + +$files = $Path | ForEach-Object { Resolve-Path $_ } | Select-Object -ExpandProperty Path -Unique + +if (-not $files -or $files.Count -eq 0) { + throw "No files matched the requested paths" +} + +$groups = $files | Group-Object { Split-Path $_ -Parent } + +foreach ($group in $groups) { + $dir = $group.Name + $names = $group.Group | ForEach-Object { Split-Path $_ -Leaf } + + & sign code trusted-signing ` + -b $dir ` + -tse $vars.endpoint ` + -tscp $vars.profile ` + -tsa $vars.account ` + @names ` + -v Information + + if ($LASTEXITCODE -ne 0) { + throw "Trusted Signing failed for $($group.Name)" + } +}