Windows distribution matrix: ARM64, MSI, and signing#635
Merged
Conversation
e8d2bbf to
31cceda
Compare
31cceda to
8d7a65c
Compare
fe60dd5 to
f5c4b08
Compare
scratch-desktop is open source, but several identifiers in
electron-builder.yaml are organization-specific: a fork that
builds without changing them would publish artifacts that collide
with ours (same Store identityName, same publisher CN).
Move the AppX identifiers to environment variables sourced from
GitHub repository Variables. Both workflows export them at job
level so electron-builder's \${env.X} interpolation finds them
during build. Forks that don't set the variables get a build
failure rather than a silent collision with our identifiers.
Variables introduced:
- APPX_IDENTITY_NAME
- APPX_PUBLISHER
- APPX_PUBLISHER_DISPLAY_NAME
Windows now ships across three formats and three architectures:
- NSIS direct download: x64, arm64, and existing ia32
- MSI for managed deployment: x64 only
- Microsoft Store AppX: new arm64 alongside existing ia32 and x64
The wrapper organizes the Windows targets into two electron-builder
passes: AppX in one (intentionally unsigned, since the Microsoft
Store re-signs at certification), and NSIS+MSI together in a single
signed pass via the new windowsInstallers entry. The release-
candidate matrix mirrors this with two Windows runners.
NSIS artifactName now includes \${arch} so the three multi-arch
builds don't collide on the same filename. The ia32 NSIS filename
changes from "Scratch X.Y.Z Setup.exe" to "Scratch X.Y.Z ia32
Setup.exe"; the scratch-www download-page link will need updating.
MSI upgradeCode sources from a repository Variable (MSI_UPGRADE_CODE)
so a fork generates and uses its own GUID rather than inheriting
ours. Future versions upgrade in place when the GUID stays stable.
Initial arch scope is x64 only; ARM64 institutional deployments use
the Store AppX via Intune, avoiding the WiX 4 ARM64 limitation
electron-builder currently carries.
Captures the 7-artifact shipping matrix (NSIS x ia32+x64+arm64, MSI x64, AppX x ia32+x64+arm64), the rationale for the empty cells (no ARM64 or ia32 MSI), how the wrapper organizes Windows into two electron-builder passes, and a short guide for identifying which build a user has when triaging reports. Linked from README.
The previous release-candidate uploads bundled multiple installers into single artifacts via a generic upload step driven by matrix variables. Splitting per-artifact lets consumers grab just the cell they care about instead of pulling a multi-installer zip. Also sets compression-level: 0 on every upload. The artifact files (.appx, .exe, .msi, .dmg, .pkg, .zip) are all already-compressed containers; re-gzipping costs CPU for ~0% size reduction.
The PR-time CI workflow previously built the full Windows installer matrix (NSIS x3 + MSI + AppX x3) on every push. That's a lot of billed minutes for installer packaging that rarely changes. Now each platform builds one representative installer that non- developer teammates can grab and try from any PR: NSIS x64 on Windows (via a new 'nsis-x64' wrapper shortname) and DMG on macOS. Both platforms specify their target explicitly so the wrapper's default behavior isn't load-bearing. The full multi-arch matrix still runs on release-candidate dispatch where the artifacts actually ship. Estimated savings: Windows CI job drops from ~11 min to ~4 min, with proportional reductions in billed compute minutes.
workflow_dispatch runs don't get a commit message in the GHA expression context, so by default the run list shows only the workflow name. Setting run-name to branch + SHA gives each manual dispatch a distinct, scannable title without requiring the dispatcher to fill in an input.
Sign EXE and MSI via electron-builder's win.azureSignOptions, authenticated with an Azure App Registration client secret (AZURE_CLIENT_SECRET alongside the tenant and client IDs). electron-builder validates a complete Azure.Identity EnvironmentCredential before signing and does not fall back to an ambient CLI session, so a client secret is required rather than a login session; the credential is scoped to the release-candidate environment, which layers its human-gated approval with the Azure auth. The Azure config and credentials are set on the installers cell's build step only (gated on matrix.target), so they never enter the env of cells that don't sign with Azure — the macOS targets and the unsigned AppX cell. 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 skips signing in --mode=dev, so PR-time CI and local dev builds build without needing Azure auth. Removes the legacy WIN_CSC_LINK / WIN_CSC_KEY_PASSWORD flow.
electron-builder only expands ${env.X} in filename fields like artifactName,
not in config fields, so appx.identityName / appx.publisher /
appx.publisherDisplayName / msi.upgradeCode were being passed to WiX and the
AppX manifest as literal "${env.*}" strings — failing GUID and identity
validation. This was never caught because PR CI only builds nsis-x64 and
earlier release-candidate runs failed at signing before reaching MSI/AppX
compilation.
Inject the four identifiers as electron-builder CLI overrides from the
wrapper, the same mechanism already used for azureSignOptions, with values
double-quoted so a CN= publisher string's spaces survive shell tokenization.
Update the ci.yml and release-candidate.yml env comments, which still
described the old ${env.X}-interpolation mechanism, to point at the wrapper.
Add per-cell signature gates that run after the build and before upload, so a bad signature fails the cell instead of publishing an artifact we only assume is signed. Each gate is placed where a failure is still fixable — in CI, on the build that produced it — rather than surfacing downstream at a store upload or on a user's machine: - installers: signtool verify /pa (validity + trust chain + timestamp) on each Setup.exe and the MSI, plus a signer-identity check that it's our cert. - appx: assert the packages are NOT signed — the Microsoft Store re-signs during certification, so this catches the inverse regression. - dmg: mount the image and validate the .app inside (stapler validate + spctl --assess --type execute). electron-builder staples the .app, not the dmg container, so the app is what carries the ticket and what Gatekeeper checks. - mas: pkgutil confirms the package is signed with Apple distribution certs, catching a broken signing/match setup before App Store Connect upload. No spctl here — MAS distribution certs aren't Gatekeeper-valid for direct launch. - mas-dev: codesign validity plus an embedded development provisioning profile, the two things a sandboxed MAS build needs to launch on a registered device. These are automated regression gates, not a replacement for on-device checks (SmartScreen reputation, UAC publisher string, ARM64 native execution).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Expands the Windows shipping matrix to seven artifacts across three formats and three architectures, and restores Windows code signing using Azure Artifact Signing.
Full grid and rationale in
docs/windows-build-matrix.md.Signing
Windows EXE and MSI are signed with Azure Artifact Signing via electron-builder's
win.azureSignOptions, injected byscripts/electron-builder-wrapper.jsand authenticated with an Azure client secret. Signing runs only inrelease-candidate.yml, scoped to the Windows installers cell. AppX ships unsigned because the Microsoft Store re-signs during certification.PR-time CI (
ci.yml) never receives the signing credentials; it builds a representative unsigned subset (a macOS DMG and Windowsnsis-x64), not the full matrix. The full multi-arch, multi-format Windows matrix is produced only on release-candidate dispatch.The release-candidate workflow verifies each artifact's signature before upload: Authenticode on the NSIS and MSI installers, notarization on the macOS builds, and an explicit unsigned check on AppX.
Test plan
msiexec /i "Scratch <version> x64.msi" /quiet) succeeds; uninstall clean