Skip to content

Commit acbde84

Browse files
authored
🤖 ci: add Windows build to PR/merge queue and EV code signing to releases (#1020)
Adds Windows EV code signing using jsign + GCP Cloud KMS (same approach as coder/coder). ## Changes - Custom signing script at `scripts/sign-windows.js` for electron-builder - Release workflow authenticates to GCP and runs jsign for EV signing - Gracefully skips signing if secrets not configured ## Required repo configuration (already done) - Variables: `EV_KEYSTORE`, `EV_KEY`, `EV_TSA_URL`, `GCP_WORKLOAD_ID_PROVIDER`, `GCP_SERVICE_ACCOUNT` - Secrets: `EV_SIGNING_CERT` _Generated with `mux`_
1 parent a293ba7 commit acbde84

File tree

4 files changed

+155
-1
lines changed

4 files changed

+155
-1
lines changed

.github/workflows/pr.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,33 @@ jobs:
282282
retention-days: 30
283283
if-no-files-found: error
284284

285+
build-windows:
286+
name: Build / Windows
287+
needs: [changes]
288+
if: ${{ needs.changes.outputs.src == 'true' || needs.changes.outputs.config == 'true' }}
289+
runs-on: windows-latest
290+
steps:
291+
- uses: actions/checkout@v4
292+
with:
293+
fetch-depth: 0
294+
- uses: ./.github/actions/setup-mux
295+
- name: Install GNU Make
296+
run: choco install -y make
297+
- name: Verify tools
298+
shell: bash
299+
run: |
300+
make --version
301+
bun --version
302+
magick --version | head -1
303+
- run: bun run build
304+
- run: make dist-win
305+
- uses: actions/upload-artifact@v4
306+
with:
307+
name: build-windows
308+
path: release/*.exe
309+
retention-days: 30
310+
if-no-files-found: error
311+
285312
build-vscode:
286313
name: Build / VS Code
287314
needs: [changes]
@@ -327,6 +354,7 @@ jobs:
327354
- smoke-docker
328355
- build-linux
329356
- build-macos
357+
- build-windows
330358
- build-vscode
331359
- codex-comments
332360
runs-on: 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,10 +169,53 @@ 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 }}
175219

176220
notify-discord:
177221
name: Notify Discord

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,11 @@
257257
"win": {
258258
"target": "nsis",
259259
"icon": "build/icon.png",
260-
"artifactName": "${productName}-${version}-${arch}.${ext}"
260+
"artifactName": "${productName}-${version}-${arch}.${ext}",
261+
"sign": "scripts/sign-windows.js",
262+
"signingHashAlgorithms": [
263+
"sha256"
264+
]
261265
},
262266
"npmRebuild": false,
263267
"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)