Skip to content

Commit f2a54fe

Browse files
committed
🤖 ci: add Windows build to PR/merge queue and EV code signing to releases
- Add Windows build job to build.yml (runs on PRs and merge queue) - Add Windows EV code signing with GCP KMS and jsign (mirrors coder-desktop-windows pattern) - Custom signing script at scripts/sign-windows.js for electron-builder - Uses repository variables for non-sensitive config (EV_KEYSTORE, EV_KEY, EV_TSA_URL) - Uses secrets for sensitive data (EV_SIGNING_CERT, GCP_WORKLOAD_ID_PROVIDER, GCP_SERVICE_ACCOUNT) - Gracefully skips signing if secrets not configured _Generated with `mux`_
1 parent 1fc5493 commit f2a54fe

File tree

4 files changed

+161
-1
lines changed

4 files changed

+161
-1
lines changed

.github/workflows/build.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,42 @@ jobs:
6464
retention-days: 30
6565
if-no-files-found: error
6666

67+
build-windows:
68+
name: Build Windows
69+
runs-on: windows-latest
70+
steps:
71+
- name: Checkout code
72+
uses: actions/checkout@v4
73+
with:
74+
fetch-depth: 0 # Required for git describe to find tags
75+
76+
- uses: ./.github/actions/setup-mux
77+
78+
- name: Install GNU Make (for build)
79+
run: choco install -y make
80+
81+
- name: Verify tools
82+
shell: bash
83+
run: |
84+
make --version
85+
bun --version
86+
magick --version | head -1
87+
88+
- name: Build application
89+
run: bun run build
90+
91+
# No code signing - releases use release.yml (triggered by tag publish).
92+
- name: Package for Windows
93+
run: make dist-win
94+
95+
- name: Upload Windows exe
96+
uses: actions/upload-artifact@v4
97+
with:
98+
name: windows-exe
99+
path: release/*.exe
100+
retention-days: 30
101+
if-no-files-found: error
102+
67103
build-vscode-extension:
68104
name: Build VS Code Extension
69105
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}

.github/workflows/release.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ on:
1212

1313
permissions:
1414
contents: write # Required for electron-builder to upload release assets
15+
id-token: write # Required for GCP workload identity authentication (Windows code signing)
1516

1617
env:
1718
RELEASE_TAG: ${{ inputs.tag || github.event.release.tag_name || github.ref_name }}
@@ -168,7 +169,50 @@ jobs:
168169
- name: Build application
169170
run: bun run build
170171

172+
# Setup Java for jsign (EV code signing with GCP KMS)
173+
- name: Setup Java
174+
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
175+
with:
176+
distribution: "zulu"
177+
java-version: "11.0"
178+
179+
- name: Authenticate to Google Cloud
180+
id: gcloud_auth
181+
if: ${{ vars.GCP_WORKLOAD_ID_PROVIDER != '' }}
182+
uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8
183+
with:
184+
workload_identity_provider: ${{ vars.GCP_WORKLOAD_ID_PROVIDER }}
185+
service_account: ${{ vars.GCP_SERVICE_ACCOUNT }}
186+
token_format: "access_token"
187+
188+
- name: Setup code signing
189+
shell: pwsh
190+
run: |
191+
if (-not $env:EV_SIGNING_CERT) {
192+
Write-Host "⚠️ No Windows code signing certificate provided - building unsigned"
193+
exit 0
194+
}
195+
196+
# Save EV certificate to temp file
197+
$certPath = Join-Path $env:TEMP "ev_cert.pem"
198+
Set-Content -Path $certPath -Value $env:EV_SIGNING_CERT
199+
Add-Content -Path $env:GITHUB_ENV -Value "EV_CERTIFICATE_PATH=$certPath"
200+
201+
# Download jsign
202+
$jsignPath = Join-Path $env:TEMP "jsign-6.0.jar"
203+
Invoke-WebRequest -Uri "https://github.com/ebourg/jsign/releases/download/6.0/jsign-6.0.jar" -OutFile $jsignPath
204+
Add-Content -Path $env:GITHUB_ENV -Value "JSIGN_PATH=$jsignPath"
205+
206+
Write-Host "✅ Windows EV code signing configured"
207+
env:
208+
EV_SIGNING_CERT: ${{ secrets.EV_SIGNING_CERT }}
209+
171210
- name: Package and publish for Windows (.exe)
172211
run: bun x electron-builder --win --publish always
173212
env:
174213
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
214+
# EV signing environment variables (used by custom sign script if configured)
215+
EV_KEYSTORE: ${{ vars.EV_KEYSTORE }}
216+
EV_KEY: ${{ vars.EV_KEY }}
217+
EV_TSA_URL: ${{ vars.EV_TSA_URL }}
218+
GCLOUD_ACCESS_TOKEN: ${{ steps.gcloud_auth.outputs.access_token }}

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,9 @@
250250
"win": {
251251
"target": "nsis",
252252
"icon": "build/icon.png",
253-
"artifactName": "${productName}-${version}-${arch}.${ext}"
253+
"artifactName": "${productName}-${version}-${arch}.${ext}",
254+
"sign": "scripts/sign-windows.js",
255+
"signingHashAlgorithms": ["sha256"]
254256
},
255257
"npmRebuild": false,
256258
"buildDependenciesFromSource": false

scripts/sign-windows.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* Windows EV code signing script for electron-builder
3+
* Uses jsign with GCP Cloud KMS for EV certificate signing
4+
*
5+
* Required environment variables:
6+
* JSIGN_PATH - Path to jsign JAR file
7+
* EV_KEYSTORE - GCP Cloud KMS keystore URL
8+
* EV_KEY - Key alias in the keystore
9+
* EV_CERTIFICATE_PATH - Path to the EV certificate PEM file
10+
* EV_TSA_URL - Timestamp server URL
11+
* GCLOUD_ACCESS_TOKEN - GCP access token for authentication
12+
*/
13+
14+
const { execSync } = require("child_process");
15+
const path = require("path");
16+
17+
/**
18+
* @param {import("electron-builder").CustomWindowsSignTaskConfiguration} configuration
19+
* @returns {Promise<void>}
20+
*/
21+
exports.default = async function sign(configuration) {
22+
const filePath = configuration.path;
23+
24+
// Check if signing is configured
25+
if (!process.env.JSIGN_PATH || !process.env.EV_KEYSTORE) {
26+
console.log(
27+
`⚠️ Windows code signing not configured - skipping signing for ${filePath}`
28+
);
29+
return;
30+
}
31+
32+
// Validate required environment variables
33+
const requiredVars = [
34+
"JSIGN_PATH",
35+
"EV_KEYSTORE",
36+
"EV_KEY",
37+
"EV_CERTIFICATE_PATH",
38+
"EV_TSA_URL",
39+
"GCLOUD_ACCESS_TOKEN",
40+
];
41+
42+
for (const varName of requiredVars) {
43+
if (!process.env[varName]) {
44+
throw new Error(`Missing required environment variable: ${varName}`);
45+
}
46+
}
47+
48+
console.log(`Signing ${filePath} with EV certificate...`);
49+
50+
const jsignArgs = [
51+
"-jar",
52+
process.env.JSIGN_PATH,
53+
"--storetype",
54+
"GOOGLECLOUD",
55+
"--storepass",
56+
process.env.GCLOUD_ACCESS_TOKEN,
57+
"--keystore",
58+
process.env.EV_KEYSTORE,
59+
"--alias",
60+
process.env.EV_KEY,
61+
"--certfile",
62+
process.env.EV_CERTIFICATE_PATH,
63+
"--tsmode",
64+
"RFC3161",
65+
"--tsaurl",
66+
process.env.EV_TSA_URL,
67+
filePath,
68+
];
69+
70+
try {
71+
execSync(`java ${jsignArgs.map((a) => `"${a}"`).join(" ")}`, {
72+
stdio: "inherit",
73+
});
74+
console.log(`✅ Successfully signed ${filePath}`);
75+
} catch (error) {
76+
throw new Error(`Failed to sign ${filePath}: ${error.message}`);
77+
}
78+
};

0 commit comments

Comments
 (0)