Skip to content

Commit 31cceda

Browse files
committed
build: enable Azure Artifact Signing for Windows installers
Sign EXE and MSI via electron-builder's win.azureSignOptions, authorized by OIDC federated credentials between GitHub Actions and Azure. The federated credential is scoped to the release-candidate workflow's environment, so the human-gated environment approval and the Azure auth are layered. AppX is excluded via signExts: the Microsoft Store re-signs during certification with a Store-issued publisher cert, and matching its CN at build time isn't feasible. The wrapper appends --c.win.signExecutable=false when --mode=dev, so PR-time CI and local dev builds skip signing without needing Azure auth. Removes the legacy WIN_CSC_LINK / WIN_CSC_KEY_PASSWORD flow.
1 parent b68335e commit 31cceda

3 files changed

Lines changed: 56 additions & 25 deletions

File tree

.github/workflows/release-candidate.yml

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ jobs:
2525
environment: release-candidate
2626
permissions:
2727
contents: read
28+
# Required for the azure/login OIDC token exchange that authorizes
29+
# Azure Artifact Signing on the Windows runners.
30+
id-token: write
2831
strategy:
2932
# Don't cancel sibling matrix cells when one fails; let each one
3033
# finish so we get artifacts and full failure signal from every cell.
@@ -52,20 +55,19 @@ jobs:
5255
run:
5356
shell: bash
5457
env:
55-
# Temporary workaround: Windows signing is currently broken due to
56-
# a security-policy change. See
57-
# https://github.com/electron/windows-installer/issues/473
58-
# While this is true, the release-candidate job still runs Windows
59-
# through the build (so we get an installer artifact) but skips
60-
# the signing step.
61-
SCRATCH_SHOULD_SIGN: ${{ matrix.os != 'windows-latest' }}
6258
# Organization-specific identifiers used by electron-builder.yaml's ${env.X}
6359
# interpolation. Kept in repo Variables so a fork builds with its own values
6460
# (or fails loudly if unset) rather than colliding with our published artifacts.
6561
APPX_IDENTITY_NAME: ${{ vars.APPX_IDENTITY_NAME }}
6662
APPX_PUBLISHER: ${{ vars.APPX_PUBLISHER }}
6763
APPX_PUBLISHER_DISPLAY_NAME: ${{ vars.APPX_PUBLISHER_DISPLAY_NAME }}
6864
MSI_UPGRADE_CODE: ${{ vars.MSI_UPGRADE_CODE }}
65+
# Azure Artifact Signing parameters for electron-builder's win.azureSignOptions.
66+
# OIDC credentials for the signing call are set by the "Azure login" step below.
67+
AZURE_SIGNING_ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }}
68+
AZURE_SIGNING_ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
69+
AZURE_SIGNING_PROFILE_NAME: ${{ vars.AZURE_SIGNING_PROFILE_NAME }}
70+
AZURE_SIGNING_PUBLISHER_NAME: ${{ vars.AZURE_SIGNING_PUBLISHER_NAME }}
6971
# App Store Connect API key — fastlane and electron-builder read
7072
# different env var names but use the same credentials. The .p8
7173
# file path (APPLE_API_KEY / APP_STORE_CONNECT_API_KEY_KEY_FILEPATH)
@@ -149,18 +151,21 @@ jobs:
149151
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
150152
if: matrix.os == 'macos-latest'
151153
run: bundle exec fastlane prepare_signing
154+
- name: Azure login
155+
# OIDC exchange that gives electron-builder's Azure Artifact Signing call the
156+
# credentials it needs. Federated credential is scoped to this workflow's
157+
# 'release-candidate' environment, so no client secret is involved.
158+
if: matrix.os == 'windows-latest'
159+
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3
160+
with:
161+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
162+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
163+
allow-no-subscriptions: true
152164
- name: Build (signed)
153165
timeout-minutes: 30 # macOS notarization can take a while
154166
env:
155167
NODE_OPTIONS: --max-old-space-size=4096
156-
# Only expose Windows code-signing secrets to runs that will use them.
157-
# SCRATCH_SHOULD_SIGN is currently false on Windows (temporary workaround above);
158-
# both expressions evaluate to '' until Windows signing is re-enabled.
159-
WIN_CSC_LINK: ${{ matrix.os == 'windows-latest' && env.SCRATCH_SHOULD_SIGN == 'true' && secrets.WIN_CSC_LINK ||
160-
'' }}
161-
WIN_CSC_KEY_PASSWORD: ${{ matrix.os == 'windows-latest' && env.SCRATCH_SHOULD_SIGN == 'true' && secrets.WIN_CSC_KEY_PASSWORD
162-
|| '' }}
163-
run: npm run ${{ env.SCRATCH_SHOULD_SIGN == 'true' && 'dist' || 'distDev' }} -- --target=${{ matrix.target }}
168+
run: npm run dist -- --target=${{ matrix.target }}
164169
- name: Zip MAS-Dev build
165170
if: matrix.target == 'mas-dev'
166171
run: |

electron-builder.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ win:
4646
target:
4747
- appx
4848
- nsis
49+
# AppX is excluded from build-time signing: the Microsoft Store re-signs during
50+
# certification with a Store-issued publisher cert, and our build-time cert's
51+
# subject would not match the manifest's Publisher attribute.
52+
signExts: ["exe", "msi", "!appx", "!appxbundle", "!appxupload"]
53+
# azureSignOptions is supplied by scripts/electron-builder-wrapper.js only when
54+
# signing is enabled (--mode=dist). Declaring it here would also activate it for
55+
# PR CI and local --mode=dev builds, where Azure auth isn't available — and some
56+
# signing paths (notably the NSIS uninstaller) ignore signAndEditExecutable, so
57+
# gating per-target isn't reliable. Keeping the config off entirely is cleaner.
4958
appx:
5059
# Organization-specific identifiers come from environment variables so a fork can build
5160
# without colliding with our published artifacts. See README for required env vars.

scripts/electron-builder-wrapper.js

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
* Running this script with no command line parameters should build all targets for the current platform.
44
* Pass `--target=<short-name>` to build exactly one target instead of the platform default set; the matrix in
55
* `release-candidate.yml` uses this to fan a multi-target serial pass out into one runner per target. Valid short
6-
* names: `mas`, `mas-dev`, `dmg`, `appx`, `nsis`.
7-
* On Windows, make sure to set CSC_* or WIN_CSC_* environment variables or the NSIS build will fail.
6+
* names: `mas`, `mas-dev`, `dmg`, `appx`, `nsis`, `msi`, `installers`, `nsis-x64`.
7+
* Windows signing uses Azure Artifact Signing, configured in `electron-builder.yaml`'s `win.azureSignOptions`.
8+
* Auth comes from OIDC federated credentials in `release-candidate.yml`; signing is disabled when --mode=dev.
89
* On Mac, the CSC_* variables are optional but will be respected if present.
910
* See also: https://www.electron.build/code-signing
1011
*/
@@ -46,18 +47,12 @@ const getPlatformFlag = function () {
4647
* Run `electron-builder` once to build one or more target(s).
4748
* @param {object} wrapperConfig - overall configuration object for the wrapper script.
4849
* @param {object} target - the target to build in this call.
49-
* If the `target.name` is `'nsis'` then the environment must contain code-signing config (CSC_* or WIN_CSC_*).
50-
* If the `target.name` is `'appx'` then code-signing config will be stripped from the environment if present.
5150
*/
5251
const runBuilder = function (wrapperConfig, target) {
53-
// the AppX build fails if CSC_* or WIN_CSC_* variables are set
52+
// Strip CSC_* env vars for AppX (would conflict with the Store-managed cert) and for unsigned builds.
53+
// Windows signing itself now goes through Azure (not CSC), so these vars are typically absent anyway.
5454
const shouldStripCSC = (target.name.indexOf('appx') === 0) || (!wrapperConfig.doSign);
5555
const childEnvironment = shouldStripCSC ? stripCSC(process.env) : process.env;
56-
if (wrapperConfig.doSign &&
57-
(target.name.indexOf('nsis') === 0) &&
58-
!(childEnvironment.CSC_LINK || childEnvironment.WIN_CSC_LINK)) {
59-
throw new Error(`Signing NSIS build requires CSC_LINK or WIN_CSC_LINK`);
60-
}
6156
const platformFlag = getPlatformFlag();
6257
let allArgs = [platformFlag, target.name];
6358
if (target.platform === 'darwin') {
@@ -75,6 +70,28 @@ const runBuilder = function (wrapperConfig, target) {
7570
// APPLE_API_KEY / APPLE_API_KEY_ID / APPLE_API_ISSUER are present in
7671
// the environment.
7772
}
73+
if (target.platform === 'win32' && wrapperConfig.doSign) {
74+
// Supply Azure Artifact Signing config via CLI rather than electron-builder.yaml. Putting it
75+
// in YAML would activate signing for every build, including PR CI and local dev where Azure
76+
// auth isn't available — and some signing paths (notably the NSIS uninstaller) ignore the
77+
// win.signAndEditExecutable flag, so per-target gating isn't reliable.
78+
const required = [
79+
'AZURE_SIGNING_ENDPOINT',
80+
'AZURE_SIGNING_ACCOUNT_NAME',
81+
'AZURE_SIGNING_PROFILE_NAME',
82+
'AZURE_SIGNING_PUBLISHER_NAME'
83+
];
84+
const missing = required.filter(name => !childEnvironment[name]);
85+
if (missing.length > 0) {
86+
throw new Error(`Azure signing requires env vars: ${missing.join(', ')}`);
87+
}
88+
allArgs.push(
89+
`--c.win.azureSignOptions.endpoint=${childEnvironment.AZURE_SIGNING_ENDPOINT}`,
90+
`--c.win.azureSignOptions.codeSigningAccountName=${childEnvironment.AZURE_SIGNING_ACCOUNT_NAME}`,
91+
`--c.win.azureSignOptions.certificateProfileName=${childEnvironment.AZURE_SIGNING_PROFILE_NAME}`,
92+
`--c.win.azureSignOptions.publisherName=${childEnvironment.AZURE_SIGNING_PUBLISHER_NAME}`
93+
);
94+
}
7895
if (!wrapperConfig.doPackage) {
7996
allArgs.push('--dir', '--c.compression=store');
8097
}

0 commit comments

Comments
 (0)