diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6b3a3e33d..d54c31d24 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,6 +64,42 @@ jobs: retention-days: 30 if-no-files-found: error + build-windows: + name: Build Windows + runs-on: windows-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required for git describe to find tags + + - uses: ./.github/actions/setup-mux + + - name: Install GNU Make (for build) + run: choco install -y make + + - name: Verify tools + shell: bash + run: | + make --version + bun --version + magick --version | head -1 + + - name: Build application + run: bun run build + + # No code signing - releases use release.yml (triggered by tag publish). + - name: Package for Windows + run: make dist-win + + - name: Upload Windows exe + uses: actions/upload-artifact@v4 + with: + name: windows-exe + path: release/*.exe + retention-days: 30 + if-no-files-found: error + build-vscode-extension: name: Build VS Code Extension runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c05401b04..b1ae79acc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,7 @@ on: permissions: contents: write # Required for electron-builder to upload release assets + id-token: write # Required for GCP workload identity authentication (Windows code signing) env: RELEASE_TAG: ${{ inputs.tag || github.event.release.tag_name || github.ref_name }} @@ -168,7 +169,50 @@ jobs: - name: Build application run: bun run build + # Setup Java for jsign (EV code signing with GCP KMS) + - name: Setup Java + uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + with: + distribution: "zulu" + java-version: "11.0" + + - name: Authenticate to Google Cloud + id: gcloud_auth + if: ${{ vars.GCP_WORKLOAD_ID_PROVIDER != '' }} + uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8 + with: + workload_identity_provider: ${{ vars.GCP_WORKLOAD_ID_PROVIDER }} + service_account: ${{ vars.GCP_SERVICE_ACCOUNT }} + token_format: "access_token" + + - name: Setup code signing + shell: pwsh + run: | + if (-not $env:EV_SIGNING_CERT) { + Write-Host "⚠️ No Windows code signing certificate provided - building unsigned" + exit 0 + } + + # Save EV certificate to temp file + $certPath = Join-Path $env:TEMP "ev_cert.pem" + Set-Content -Path $certPath -Value $env:EV_SIGNING_CERT + Add-Content -Path $env:GITHUB_ENV -Value "EV_CERTIFICATE_PATH=$certPath" + + # Download jsign + $jsignPath = Join-Path $env:TEMP "jsign-6.0.jar" + Invoke-WebRequest -Uri "https://github.com/ebourg/jsign/releases/download/6.0/jsign-6.0.jar" -OutFile $jsignPath + Add-Content -Path $env:GITHUB_ENV -Value "JSIGN_PATH=$jsignPath" + + Write-Host "✅ Windows EV code signing configured" + env: + EV_SIGNING_CERT: ${{ secrets.EV_SIGNING_CERT }} + - name: Package and publish for Windows (.exe) run: bun x electron-builder --win --publish always env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # EV signing environment variables (used by custom sign script if configured) + EV_KEYSTORE: ${{ vars.EV_KEYSTORE }} + EV_KEY: ${{ vars.EV_KEY }} + EV_TSA_URL: ${{ vars.EV_TSA_URL }} + GCLOUD_ACCESS_TOKEN: ${{ steps.gcloud_auth.outputs.access_token }} diff --git a/docs/system-prompt.md b/docs/system-prompt.md index fb5bb1305..3b7de1ba0 100644 --- a/docs/system-prompt.md +++ b/docs/system-prompt.md @@ -62,5 +62,4 @@ You are in a git worktree at ${workspacePath} } ``` - {/* END SYSTEM_PROMPT_DOCS */} diff --git a/package.json b/package.json index 375273b2d..e8f04a5b5 100644 --- a/package.json +++ b/package.json @@ -250,7 +250,11 @@ "win": { "target": "nsis", "icon": "build/icon.png", - "artifactName": "${productName}-${version}-${arch}.${ext}" + "artifactName": "${productName}-${version}-${arch}.${ext}", + "sign": "scripts/sign-windows.js", + "signingHashAlgorithms": [ + "sha256" + ] }, "npmRebuild": false, "buildDependenciesFromSource": false diff --git a/scripts/sign-windows.js b/scripts/sign-windows.js new file mode 100644 index 000000000..6e1b5ebc5 --- /dev/null +++ b/scripts/sign-windows.js @@ -0,0 +1,78 @@ +/** + * Windows EV code signing script for electron-builder + * Uses jsign with GCP Cloud KMS for EV certificate signing + * + * Required environment variables: + * JSIGN_PATH - Path to jsign JAR file + * EV_KEYSTORE - GCP Cloud KMS keystore URL + * EV_KEY - Key alias in the keystore + * EV_CERTIFICATE_PATH - Path to the EV certificate PEM file + * EV_TSA_URL - Timestamp server URL + * GCLOUD_ACCESS_TOKEN - GCP access token for authentication + */ + +const { execSync } = require("child_process"); +const path = require("path"); + +/** + * @param {import("electron-builder").CustomWindowsSignTaskConfiguration} configuration + * @returns {Promise} + */ +exports.default = async function sign(configuration) { + const filePath = configuration.path; + + // Check if signing is configured + if (!process.env.JSIGN_PATH || !process.env.EV_KEYSTORE) { + console.log( + `⚠️ Windows code signing not configured - skipping signing for ${filePath}` + ); + return; + } + + // Validate required environment variables + const requiredVars = [ + "JSIGN_PATH", + "EV_KEYSTORE", + "EV_KEY", + "EV_CERTIFICATE_PATH", + "EV_TSA_URL", + "GCLOUD_ACCESS_TOKEN", + ]; + + for (const varName of requiredVars) { + if (!process.env[varName]) { + throw new Error(`Missing required environment variable: ${varName}`); + } + } + + console.log(`Signing ${filePath} with EV certificate...`); + + const jsignArgs = [ + "-jar", + process.env.JSIGN_PATH, + "--storetype", + "GOOGLECLOUD", + "--storepass", + process.env.GCLOUD_ACCESS_TOKEN, + "--keystore", + process.env.EV_KEYSTORE, + "--alias", + process.env.EV_KEY, + "--certfile", + process.env.EV_CERTIFICATE_PATH, + "--tsmode", + "RFC3161", + "--tsaurl", + process.env.EV_TSA_URL, + filePath, + ]; + + try { + execSync(`java ${jsignArgs.map((a) => `"${a}"`).join(" ")}`, { + stdio: "inherit", + }); + console.log(`✅ Successfully signed ${filePath}`); + } catch (error) { + throw new Error(`Failed to sign ${filePath}: ${error.message}`); + } +};