Skip to content

Windows distribution matrix: ARM64, MSI, and signing#635

Merged
cwillisf merged 9 commits into
developfrom
feature/windows-distribution-matrix
Jun 9, 2026
Merged

Windows distribution matrix: ARM64, MSI, and signing#635
cwillisf merged 9 commits into
developfrom
feature/windows-distribution-matrix

Conversation

@cwillisf

@cwillisf cwillisf commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Expands the Windows shipping matrix to seven artifacts across three formats and three architectures, and restores Windows code signing using Azure Artifact Signing.

  • NSIS direct download: gains x64 and ARM64 (was previously ia32-only)
  • MSI: new target for managed deployment via Intune, SCCM, or Group Policy; x64
  • Store AppX: gains ARM64 alongside existing ia32 and x64

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 by scripts/electron-builder-wrapper.js and authenticated with an Azure client secret. Signing runs only in release-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 Windows nsis-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

  • CI green on all platforms
  • Release-candidate run produces signed NSIS (ia32+x64+arm64) and MSI (x64), unsigned Store AppX (ia32+x64+arm64), and signed/notarized macOS builds
  • Manual install on Windows x64 and Windows-on-ARM, confirming native ARM64 execution
  • MSI silent install (msiexec /i "Scratch <version> x64.msi" /quiet) succeeds; uninstall clean
  • UAC publisher string shows "Scratch Foundation" on the signed installers

@cwillisf cwillisf force-pushed the feature/windows-distribution-matrix branch 6 times, most recently from e8d2bbf to 31cceda Compare June 1, 2026 18:49
@cwillisf cwillisf had a problem deploying to release-candidate June 1, 2026 19:03 — with GitHub Actions Failure
@cwillisf cwillisf temporarily deployed to release-candidate June 1, 2026 19:03 — with GitHub Actions Inactive
@cwillisf cwillisf temporarily deployed to release-candidate June 1, 2026 19:03 — with GitHub Actions Inactive
@cwillisf cwillisf had a problem deploying to release-candidate June 1, 2026 19:03 — with GitHub Actions Failure
@cwillisf cwillisf temporarily deployed to release-candidate June 1, 2026 19:03 — with GitHub Actions Inactive
@cwillisf cwillisf force-pushed the feature/windows-distribution-matrix branch from 31cceda to 8d7a65c Compare June 1, 2026 19:34
@cwillisf cwillisf temporarily deployed to release-candidate June 1, 2026 19:39 — with GitHub Actions Inactive
@cwillisf cwillisf had a problem deploying to release-candidate June 1, 2026 19:39 — with GitHub Actions Failure
@cwillisf cwillisf temporarily deployed to release-candidate June 1, 2026 19:39 — with GitHub Actions Inactive
@cwillisf cwillisf had a problem deploying to release-candidate June 1, 2026 19:39 — with GitHub Actions Failure
@cwillisf cwillisf temporarily deployed to release-candidate June 1, 2026 19:39 — with GitHub Actions Inactive
@cwillisf cwillisf had a problem deploying to release-candidate June 1, 2026 21:19 — with GitHub Actions Failure
@cwillisf cwillisf temporarily deployed to release-candidate June 1, 2026 21:19 — with GitHub Actions Inactive
@cwillisf cwillisf temporarily deployed to release-candidate June 1, 2026 21:19 — with GitHub Actions Inactive
@cwillisf cwillisf had a problem deploying to release-candidate June 1, 2026 21:19 — with GitHub Actions Failure
@cwillisf cwillisf temporarily deployed to release-candidate June 1, 2026 21:19 — with GitHub Actions Inactive
@cwillisf cwillisf temporarily deployed to release-candidate June 8, 2026 17:18 — with GitHub Actions Inactive
@cwillisf cwillisf temporarily deployed to release-candidate June 8, 2026 17:18 — with GitHub Actions Inactive
@cwillisf cwillisf had a problem deploying to release-candidate June 8, 2026 17:18 — with GitHub Actions Failure
@cwillisf cwillisf had a problem deploying to release-candidate June 8, 2026 17:18 — with GitHub Actions Failure
@cwillisf cwillisf temporarily deployed to release-candidate June 8, 2026 17:30 — with GitHub Actions Inactive
@cwillisf cwillisf temporarily deployed to release-candidate June 8, 2026 17:30 — with GitHub Actions Inactive
@cwillisf cwillisf temporarily deployed to release-candidate June 8, 2026 17:30 — with GitHub Actions Inactive
@cwillisf cwillisf changed the title Windows distribution matrix: ARM64, MSI, and signing prep Windows distribution matrix: ARM64, MSI, and signing Jun 9, 2026
@cwillisf cwillisf requested a review from Copilot June 9, 2026 13:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Comment thread scripts/electron-builder-wrapper.js
Comment thread scripts/electron-builder-wrapper.js
Comment thread .github/workflows/ci.yml
@cwillisf cwillisf force-pushed the feature/windows-distribution-matrix branch from fe60dd5 to f5c4b08 Compare June 9, 2026 14:03
@cwillisf cwillisf requested a review from Copilot June 9, 2026 14:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread scripts/electron-builder-wrapper.js Outdated
Comment thread scripts/electron-builder-wrapper.js Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread scripts/electron-builder-wrapper.js
Comment thread docs/windows-build-matrix.md

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread scripts/electron-builder-wrapper.js Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread .github/workflows/release-candidate.yml
Comment thread README.md

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread README.md Outdated
Comment thread README.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

cwillisf added 9 commits June 9, 2026 10:20
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).
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants