Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions build/installer.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
!macroend

Function CodeUxBetaPage
# Silent release and deployment installs do not have an interactive desktop. Skip the custom
# page before nsDialogs loads; normal installers continue into the beta notice below.
IfSilent CodeUxBetaPageSilent CodeUxBetaPageInteractive

CodeUxBetaPageSilent:
Abort

CodeUxBetaPageInteractive:
nsDialogs::Create 1018
Pop $0

Expand Down
16 changes: 9 additions & 7 deletions docs-web/developer/building-from-source.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,15 @@ The installed-Electron smoke uses each platform's native package and waits until
backend and renderer are ready and the readiness marker is durably written. Linux and Windows then
require the isolated app probe to exit with code zero. On Windows the harness passes the NSIS
silent-install arguments verbatim so the required final `/D=` destination remains unquoted even
when that destination contains spaces. The exact transient `0xC0000005` termination sometimes seen
while a GitHub runner inspects a newly produced unsigned installer receives bounded retries after
1.5, 5, and 15 seconds with fresh destinations; other failures are not retried. On macOS the harness
accepts the DMG's embedded MIT license through `hdiutil` stdin, validates readiness, then owns probe
teardown with `SIGTERM` and a bounded `SIGKILL` fallback because Electron/AppKit can defer both
Electron and Node exit paths. Production Electron shutdowns still drain the embedded runtime
normally.
when that destination contains spaces, and it explicitly selects current-user installation. The
interactive beta notice aborts before initializing `nsDialogs` during silent installs while
remaining visible in the normal installer. The exact transient `0xC0000005` termination sometimes
seen while a GitHub runner inspects a newly produced unsigned installer receives bounded retries
after 1.5, 5, and 15 seconds with fresh destinations; other failures are not retried. On macOS the
harness accepts the DMG's embedded MIT license through `hdiutil` stdin, validates readiness, then
owns probe teardown with `SIGTERM` and a bounded `SIGKILL` fallback because Electron/AppKit can
defer both Electron and Node exit paths. Production Electron shutdowns still drain the embedded
runtime normally.

They must also include `assets/models-dev/catalog.json`. The automatic token-pricing path reads this snapshot beside the compiled runtime; without it, known models can appear unpriced only in the desktop build. Electron packaging tests pin both runtime assets and a representative GPT-5.5 catalogue rate.

Expand Down
4 changes: 2 additions & 2 deletions docs/deployment/electron-desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ macOS DMG builds include the MIT license resource through `build/license_en.txt`
- `pnpm run electron:dist:win`: build Windows targets.
- `pnpm run electron:benchmark:runtime`: launch Electron with an isolated temporary user profile, navigate dashboard routes, probe backend endpoints, and write route/API/renderer/runtime metrics under `.cache/electron-runtime-benchmark/`.
- `pnpm run electron:benchmark:win`: build Windows installers with `normal` and `store` compression and write timing/size data to `release/electron-benchmark/summary.json`.
- `pnpm run electron:smoke-installed`: select the native package in `release/electron/` whose artifact name matches the current `package.json` version, install it, start that installed app with an isolated profile, and wait until its backend and renderer are ready. Linux and Windows require the isolated app probe to exit with code zero. macOS validates the durably written readiness marker, then the parent smoke harness terminates only that validated probe with `SIGTERM` and a bounded `SIGKILL` fallback because Electron/AppKit can defer both `app.exit()` and `process.exit()` after readiness. Linux requires passwordless `sudo` and `xvfb-run`, as provided by the release runners. Windows launches the silent NSIS installer with verbatim arguments because NSIS requires its final `/D=` destination segment to remain unquoted even when the path contains spaces. GitHub Windows runners can transiently terminate a newly produced unsigned NSIS installer with `0xC0000005` while host security inspection settles, so only that exact status receives bounded retries after 1.5, 5, and 15 seconds, each with a fresh destination; every other installer failure remains immediate. macOS supplies affirmative input to `hdiutil` so the CI-owned smoke can mount a DMG containing the embedded MIT license without an interactive terminal.
- `pnpm run electron:smoke-installed`: select the native package in `release/electron/` whose artifact name matches the current `package.json` version, install it, start that installed app with an isolated profile, and wait until its backend and renderer are ready. Linux and Windows require the isolated app probe to exit with code zero. macOS validates the durably written readiness marker, then the parent smoke harness terminates only that validated probe with `SIGTERM` and a bounded `SIGKILL` fallback because Electron/AppKit can defer both `app.exit()` and `process.exit()` after readiness. Linux requires passwordless `sudo` and `xvfb-run`, as provided by the release runners. Windows explicitly selects the current-user NSIS mode and passes the silent install arguments verbatim because NSIS requires its final `/D=` destination segment to remain unquoted even when the isolated path contains spaces. The custom beta notice is skipped before `nsDialogs` initialization during silent installs; it remains visible for normal interactive installation. GitHub Windows runners can transiently terminate a newly produced unsigned NSIS installer with `0xC0000005` while host security inspection settles, so only that exact status receives bounded retries after 1.5, 5, and 15 seconds, each with a fresh destination; every other installer failure remains immediate. macOS supplies affirmative input to `hdiutil` so the CI-owned smoke can mount a DMG containing the embedded MIT license without an interactive terminal.
- `pnpm run electron:install-deps`: rebuild native app dependencies for Electron.

The release output is written to `release/electron/`.
Expand Down Expand Up @@ -129,7 +129,7 @@ Use `.github/workflows/release.yml` for published desktop releases. It is the la

The no-secret release-candidate package lane is part of `.github/workflows/ci.yml`, named `Code UX CI Pipeline`. It runs for `main` validation and manual dispatches after package smoke, keeping the full desktop package proof out of the routine `dev` lane.

The `10 Release Candidate / desktop package` matrix starts as soon as the package smoke job passes, so desktop packaging can run beside the E2E and orchestration matrices instead of waiting for them to finish. It downloads the shared `codeux-build-linux` artifact, installs the cached Electron binary, rebuilds Electron native dependencies, prepares runtime assets, and runs Electron Builder directly with `--linux`, `--mac`, or `--win` plus `--publish never`. After compilation, every native runner installs its candidate—the Linux `.deb`, Windows NSIS `.exe`, or macOS app copied from the `.dmg`—and starts that installed copy with an isolated home and dashboard port. The Windows smoke preserves NSIS's required unquoted, final `/D=` destination segment when spawning the installer. It treats only the known transient `0xC0000005` runner termination as retryable, using three progressively longer waits and a fresh destination each time so a short host-security scan cannot make an otherwise valid candidate flaky. The macOS smoke accepts the DMG's embedded MIT license through `hdiutil` stdin before mounting. Success requires the packaged backend to start, the dashboard renderer to finish loading, and an atomic readiness marker to be written. Linux and Windows then require a zero-code probe exit; on macOS the parent harness owns teardown after validation, using `SIGTERM` with a short `SIGKILL` fallback so upstream AppKit exit deferral cannot turn a healthy installed app into a false failure. Built native artifacts are uploaded whenever packaging succeeded, even if the installed-app smoke fails, preserving evidence for release debugging. Production shutdowns remain unchanged and still drain the embedded server. The package smoke job that precedes it runs `node scripts/verify-release-install.mjs` with `CODE_UX_SKIP_RELEASE_INSTALL_BUILD=1`, so the npm tarball install check uses the same compiled artifact instead of rebuilding.
The `10 Release Candidate / desktop package` matrix starts as soon as the package smoke job passes, so desktop packaging can run beside the E2E and orchestration matrices instead of waiting for them to finish. It downloads the shared `codeux-build-linux` artifact, installs the cached Electron binary, rebuilds Electron native dependencies, prepares runtime assets, and runs Electron Builder directly with `--linux`, `--mac`, or `--win` plus `--publish never`. After compilation, every native runner installs its candidate—the Linux `.deb`, Windows NSIS `.exe`, or macOS app copied from the `.dmg`—and starts that installed copy with an isolated home and dashboard port. The Windows smoke selects current-user installation, preserves NSIS's required unquoted final `/D=` destination segment, and skips the interactive beta page before its dialog plugin is initialized. It treats only the known transient `0xC0000005` runner termination as retryable, using three progressively longer waits and a fresh destination each time so a short host-security scan cannot make an otherwise valid candidate flaky. The macOS smoke accepts the DMG's embedded MIT license through `hdiutil` stdin before mounting. Success requires the packaged backend to start, the dashboard renderer to finish loading, and an atomic readiness marker to be written. Linux and Windows then require a zero-code probe exit; on macOS the parent harness owns teardown after validation, using `SIGTERM` with a short `SIGKILL` fallback so upstream AppKit exit deferral cannot turn a healthy installed app into a false failure. Built native artifacts are uploaded whenever packaging succeeded, even if the installed-app smoke fails, preserving evidence for release debugging. Production shutdowns remain unchanged and still drain the embedded server. The package smoke job that precedes it runs `node scripts/verify-release-install.mjs` with `CODE_UX_SKIP_RELEASE_INSTALL_BUILD=1`, so the npm tarball install check uses the same compiled artifact instead of rebuilding.

Release-candidate packaging sets `CSC_IDENTITY_AUTO_DISCOVERY=false` for unsigned Electron packaging and passes `--publish never` to Electron Builder. It does not require provider API keys, npm publishing credentials, Docker credentials, GitHub Release events, or real project state. When Electron output exists, the workflow uploads files from `release/electron/` as workflow artifacts only; it does not publish to npm or attach files to a GitHub Release.

Expand Down
5 changes: 4 additions & 1 deletion scripts/smoke-installed-electron.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ async function installWindowsCandidate(temporaryRoot) {
// NSIS requires /D= to be the final, completely unquoted command-line segment even when the
// destination contains spaces. Node otherwise quotes that argument on Windows and the generated
// installer can terminate before extracting the application.
const result = runResult(installer, ["/S", `/D=${installDirectory}`], {
// Force the current-user mode used by the shipped installer and keep the smoke isolated under
// its temporary root. The custom NSIS beta page explicitly aborts in silent mode, so the
// release probe never initializes interactive nsDialogs while no desktop is attached.
const result = runResult(installer, ["/S", "/currentuser", `/D=${installDirectory}`], {
windowsVerbatimArguments: true,
});
if (result.status === 0) {
Expand Down
11 changes: 10 additions & 1 deletion tests/backend/electron-builder-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,14 @@ describe("electron-builder packaged defaults", () => {
path.join(process.cwd(), "scripts", "smoke-installed-electron.mjs"),
"utf8",
);
const installerInclude = fs.readFileSync(
path.join(process.cwd(), "build", "installer.nsh"),
"utf8",
);
const mainProcessSource = fs.readFileSync(path.join(process.cwd(), "src/electron/main.ts"), "utf8");

expect(installerSmoke).toContain('findArtifact(".deb")');
expect(installerSmoke).toContain('["/S", `/D=${installDirectory}`]');
expect(installerSmoke).toContain('["/S", "/currentuser", `/D=${installDirectory}`]');
expect(installerSmoke).toContain("windowsVerbatimArguments: true");
expect(installerSmoke).toContain("WINDOWS_ACCESS_VIOLATION");
expect(installerSmoke).toContain("WINDOWS_INSTALL_RETRY_DELAYS_MS = [1_500, 5_000, 15_000]");
Expand All @@ -152,6 +156,11 @@ describe("electron-builder packaged defaults", () => {
expect(installerSmoke).toContain("terminateValidatedMacProbe");
expect(installerSmoke).toContain("stopped.exit.code === 0");
expect(installerSmoke).toContain("stopped.exit.signal === stopped.termination");
expect(installerInclude).toContain(
"IfSilent CodeUxBetaPageSilent CodeUxBetaPageInteractive",
);
expect(installerInclude).toContain("CodeUxBetaPageSilent:\n Abort");
expect(installerInclude).toContain("CodeUxBetaPageInteractive:\n nsDialogs::Create 1018");
expect(mainProcessSource).toContain('window.webContents.once("did-finish-load"');
expect(mainProcessSource).toContain("writeElectronStartupSmoke");
expect(mainProcessSource).toContain('CODE_UX_ELECTRON_STARTUP_SMOKE_EXIT === "1"');
Expand Down
Loading