Closing the main window hides it on macOS and Windows; the page and the
Host keep running and the next show resumes the same document. Windows
gets a permanent tray icon (open / quit) and a one-time notification the
first time the window is hidden after installation.
Every ordinary quit entry asks the Host over a new quit-inspection IPC
request for active tasks (same rule as the update restart check) and
armed scheduled reminders (schedule family of workspace/session-activity
for loaded sessions). Nothing to interrupt quits silently; otherwise a
native message box explains what stops, with Quit as the default and
Cancel on Esc. Inspection failure or a missed two-second deadline counts
as running tasks. The installer restart, fatal recovery, the development
restart command, and OS session end skip the confirmation.
Also: defer the downloaded-update confirmation while the window is hidden,
render the tray ICO from the vector icon, and point the installer's
running-application message at the tray.
Closes#4013
The installer showed "Failed to decompress files" and discarded 7-Zip's
error output, so a failure such as security software quarantining an
extracted file could not be told apart from a full disk or a broken
download.
- InstallerExtract returns negated Win32 errors for launch failures so
they no longer collide with 7-Zip exit codes; 7-Zip writes UTF-8 errors.
- window-frame.dll composes a report (result, archive, destination,
Windows version, full 7-Zip output), saves it under the updater cache
directory, and presents a task dialog with the first error line, a
collapsible bounded excerpt, and a Copy error details button. Silent
installs keep only the report; a missing task dialog falls back to a
message box.
- Unsigned Windows builds name their installer *-unsigned.exe so a
shared file cannot pass for a release artifact; version discovery
still counts them.
- The directory smoke asserts the saved report on a broken archive and
smoke-windows.ps1 accepts the prepared DLL.
Refs #4893
Review found that `apps/desktop/README.md#release-versions` and the
2026-09-16 version-derivation note already specify this convention, and
that three of their requirements were reversed without saying so. The
rules themselves are right; what was wrong was leaving them undocumented
and rejecting one of the forms they prescribe.
A stable base takes `-test.YYYYMMDD.index`, which validation refused
because such a version ranks below its base. That ordering is the point:
the form belongs to a test feed that never serves the production
release. Both READMEs, the older note, and a new note now record which
requirements this change reverses and why.
Three defects the review found, each reproducible:
- `afterPack` compared the bundled runtime against the product version,
which broke installed-update qualification: that flow passes a runtime
whose version was deliberately rewritten. The expected version is now
a parameter the preparer supplies, and `verifyDesktopRuntime` says
"bundled X is not the expected Y" rather than naming Electron.
- Upload could only learn the published version from a shell variable,
conflicting with "release fields come only from the target file". It
reads the completion record packaging wrote, and a mismatch now prints
the version that record holds.
- Automatic numbering read one listing page and one fixed directory. It
now follows every page, discards a listing it could not finish rather
than reusing an index that would overwrite a published installer,
bounds the request through the client's own timeout, and reads the
directory an unsigned build actually writes.
Also: `npm run` and `yarn run` set `npm_execpath` too, so the pnpm
resolution checks the path names pnpm; the argument separator can arrive
after a run script's preset arguments, not only first; and the toolchain
probe, parser, build commit, and tarball readers now have specs.
The concrete-terms gate forbids "provenance": the repository asks for the
exact source, field, or identity a value holds. The module records one
commit and whether its tree was dirty, so it and its callers say that.
The toolchain step also has to stay out of the way of the packaging
entry's error-recording test, which fakes the host platform and would
otherwise report the absent Windows compilers rather than the failure it
exercises. The test mocks the probe, as it already does for the release
environment and the signing keychain, and expects the new stage.
Injecting a build version renamed the Electron shell but not the dsh
runtime packed into it, and afterPack required the two to be equal, so
every build carrying a build version failed after packaging finished.
The bundled runtime is the dsh package at the product version, which is
what the check now compares against; the app already reports the shell
version and the bundled runtime version separately to the update policy.
Rename the flag to --build-version. The repository already has
DOWNLOAD_TEST_RELEASE_ID for the opaque test feed directory, so calling
a version a release id invited a reader to confuse the two. Accept a
forwarded `--` as well, because `pnpm run <script> -- --build-version x`
passes the separator through as an argument and every following option
would otherwise parse as a second target.
Packaging a build for a test feed or a colleague required bumping the
version in 295 manifests plus the lockfile first, because the version a
build publishes was only ever read from the checkout. Those edits cannot
be committed, so every such build left the working tree dirty, and the
`<version>.<date>.<sequence>` convention behind them lived only in the
operator's memory.
The version a build publishes is now an input. `--release-id` names it,
`--release-id auto` numbers it after the builds already published to the
destination bucket (falling back to local artifacts, since a fresh CI
checkout has none), and it reaches electron-builder through
extraMetadata, the update feed, and the upload validation as one value.
The manifests keep the product version, aligned with the dsh npm
package, and a release still publishes exactly that.
Validation goes through `semver` itself, because `electron-updater`
compares feed versions with `semver.gt` against the installed
`app.getVersion()`: a build version must outrank the product version it
extends, which holds for `0.1.6-alpha.2.20260921.1` over
`0.1.6-alpha.2` and cannot hold on a stable version, where a fourth
release number is not a version at all.
Two things a build could not previously answer for itself:
- Artifacts now carry the commit they were packaged from and whether
that checkout was dirty, so a build handed to a colleague is traceable
without a tag.
- A production upload tags that commit as `desktop-v<version>` once the
artifacts are public, and reports what to run by hand rather than
failing an upload that already completed. Test and local builds are
deliberately left untagged.
A toolchain step now probes tar and the Windows installer compiler
before the first build step, so a host that cannot finish the sequence
says so in seconds rather than minutes.