Maintainer ruling: only Hermes and only its packaged package managers
(uv/pip/node/npm) are ever used; no PATH fallback when the managed tool is
missing, no "prefer the user's if new enough".
- hermes_constants.find_node_executable: node/npm/npx resolve to PM's
installed copy or None. Every caller already pm.ensure()s on None, so a
missing runtime is now provisioned instead of silently borrowing the
user's Node (native-addon ABI / npm cache mismatches).
- agent/lsp/install._install_npm: pm.ensure('npm') when PM npm is absent,
instead of failing over to whatever npm is on PATH.
- gateway._append_node_dir_for_service: stop baking the invoker's PATH node
dir into generated systemd/launchd units.
- main_install_repair._resolve_node_runtime_npm: drop the PATH re-scan for
another npm.
- source_build.source_product_current: run the freshness reader only with
PM's node.
- doctor: Node/npm rows and npm audit use PM's copies (Termux APT distro
keeps its system Node).
- install.sh ensure_uv / install.ps1 Get-Uv: always stage the pinned uv
artifact; delete the "uv on PATH if new enough" developer shortcut.
Get-PinnedGit no longer invokes tar at all (no $inboxTar seam exists since
the PortableGit pin), so the C# stub tar.exe and the Invoke-Native override
that bound it were never reached: the test compiled a stub for nothing and
its docstring described a mechanism the installer does not have. Keep the
real-download Get-PinnedGit run under a bzip2-less PATH and the git.exe +
bash.exe contract checks; that is what the test proves.
Windows 10 boxes whose System32 tar.exe cannot run the bzip2 filter die
at stage=prerequisites with "unable to run program bzip2 -d" while
extracting the pinned Git-2.53.0.3 tar.bz2 (#122512). Repin git for
both win32 targets to git-for-windows' PortableGit self-extracting 7z,
which carries its own extractor and the bundled usr/bin/bash.exe:
- pm/lock.json: new artifact urls + sha256 (the pin authority)
- scripts/install.ps1: generated fragment regenerated; Get-PinnedGit
downloads the SFX and waits on it explicitly (the stub is a
GUI-subsystem exe, so PowerShell's & does not wait); the System32
tar invocation and its msys symlink excludes go away
- pm/packages.py: Git.fetch_url/Git.unpack run the self-extractor
after the sha256-verified download
- pm/store.py: drop the now-callerless git_msys branch of extract_tar
- tests: RED->GREEN test runs the real Get-PinnedGit against a
bzip2-less System32 tar.exe stub with no bzip2 on PATH; the three
obsolete tar-contract tests and the install.ps1/PM msys-links parity
test are replaced by a no-external-decompressor contract test
Closes#122512
(cherry picked from commit 4915304213)
Address review on #125371: the binding test passed the script-level
-NonInteractive alongside the alias, so deleting the fold line kept it
green. The new test runs -Stage setup -Json with only -SkipSetup and
asserts the skipped/needs-user-input frame, which only the fold can
produce (without it the stage dispatches interactively and fails under
a non-interactive host). Also document the deprecated alias in the
zh-Hans windows-native guide, which still said -SkipSetup is rejected.
(cherry picked from commit b97fc4561a)
- tests/scripts/install/test_install_ps1_skipsetup_alias.py: Windows-lane
test driving the real installer with -SkipSetup -ShowResolvedPaths —
the side-effect-free contract that still exercises parameter binding.
Red on main (NamedParameterNotFound), green with the alias restored.
- tests/pm/test_ffmpeg_pin_liveness.py: walks every BtbN URL in the
lockfile against the live upstream (HEAD); catches the next pin rot
before a fresh install 404s. Skips without network.
(cherry picked from commit 736b7ff4c1; ffmpeg lock/liveness hunks dropped, superseded by #125468)
Keep one PM invariant (a musl ELF userland resolves to linux-<arch>-musl
and the default closure is satisfiable with musl-native artifacts) and
one installer invariant (uv_bootstrap_target picks musl uv from a musl
ELF interpreter even when ldd says glibc). Drop the lock-content and
URL-shape change-detectors.
Fall back to the musl loader if ldd cannot identify libc, while honoring
an explicit GNU libc report on hosts with a secondary musl toolchain.
Cover both paths against the pinned uv URL and digest.
Refs: #123682
Skip glibc-linked FFmpeg on musl in both default install and update roots.
Select musl uv during the standalone shell bootstrap, and let the native
userland resolve libc before bootstrap Python build metadata.
Add regression checks for the closure, target precedence, and installer pins.
wire_shell_path's existing-setup regex required a character before PATH=, so it missed bare assignments such as Fedora's ~/.bashrc ( PATH="$HOME/.local/bin:$HOME/bin:$PATH") and Debian's ~/.profile. The installer then appended its own line to .bashrc, .profile and .bash_profile, and because Fedora's .bash_profile sources .bashrc, login shells got ~/.local/bin on PATH several times.
Match bare assignments too, and make the appended line a no-op when PATH already contains ~/.local/bin.
electron-builder names the unpacked output <os>-unpacked on x64 and
<os>-<arch>-unpacked elsewhere (linux-arm64-unpacked, win-arm64-unpacked).
install.sh desktop_product_present and install.ps1
Test-DesktopProductPresent only listed the x64 names, so a rerun on an
ARM64 desktop install skipped the desktop rebuild and left a bundle built
from the previous code. List every unpacked dir main_desktop.py already
resolves, in both installers.
electron-builder names the unpacked dir linux-unpacked on x86_64 but
linux-<arch>-unpacked on every other arch (linux-arm64-unpacked is what
ARM ships). The gate hardcoded the x86_64 name, so a healthy ARM install
false-gated as "skew" on EVERY update, telling the user to reinstall an
app that was already correct. The ostree/symlink half was fixed earlier
(d3b090a3); this lands the remaining arch-dir half.
Resolve the unpacked dir the running binary actually lives in by scanning
the release dir's linux*-unpacked candidates (canonicalised both sides
before the compare, so the symlink fix's semantics are preserved, with a
first-found fallback so foreign targets keep gating as skew.
Tests drive the real --self-test-gate entry point; on BSD-readlink hosts
a PATH shim provides GNU so the gate logic runs everywhere
(the existing linux_only symlink matrix covers -dependent paths).
Co-authored-by: C-Est-Dept <cestdept@example.com>
Co-authored-by: Sahilvishnaliya <sahil@example.com>
EOF
)
The setup stage installs the gateway service through
ensure_gateway_service. On Windows that asks the start-now, Scheduled
Task and UAC questions. The gateway stage then ran `hermes gateway
install`, which asked them all again.
`gateway install --if-missing` does nothing when a service is already
installed. Both installers' gateway stages use it, so they ask only
when setup did not install the service.
Steps inherited the hand-off console's stdin, so any step that asks a
question blocked forever. Its prompt went to the captured stdout, which
is shown only after the step exits. `gateway start --all` did exactly
this: it saw an interactive console and asked "Install it now so the
gateway starts on login?". The update stopped after `hermes update exit
code: 0` and never relaunched the app.
Steps now read NUL. Prompts see a non-interactive stdin and take their
defaults. The working-directory self-test also checks that a step's
stdin is not a console, and a new test runs it under a real console.
PR #122161 stopped boot from activating the in-tree venv or .venv when
PM has committed no environment. Six Linux tests and four Windows tests
still used that tree to supply their probe modules.
- Launcher tests put the probe in a committed generation. A custom
HERMES_HOME is its own dependency root, so it gets its own commit.
- The legacy row of test_pre_pm_base_dependencies_activate_only_at_boot
asserted the removed behavior. test_boot_never_activates_the_pre_pm_venv
now covers the inverse. The payload row stays.
- The mint payload fixture writes manifest.json as real payloads do, so
boot selects the payload venv.
- The PowerShell activate test expects PYTHONPATH to be the checkout
alone. The bootstrap .venv packages do not leak in.
The flag used to exit 1 as retired. Now that PM installs the browser tools
by default, it maps to `pm.cli install --without agent-browser`, which later
installs and `hermes update` honour.
dc11e3b3bc made scripts/releases/versioning.py import the new sibling
scripts/releases/semver.py. Three fixtures hand-copy a file list of that
package into a temp tree, so importing versioning there now dies with
`ModuleNotFoundError: No module named 'scripts.releases.semver'`
(JS & TS checks: channel-build-version.test.ts MSIX manifest case; the
same import chain runs in test_source_build_env.py and
test_commit_stamp_identity.py via distance -> versioning).
Copy the package as a tree, like the fixtures already do for pm/, so the
next intra-package import cannot silently break them again.
The pm-era installer printed "[hermes]" lines between raw git, uv and pm
output. Restore the pre-pm installer's look (→ ✓ ⚠ ✗, banner on the full
ladder) and route every child command through run_logged: on a terminal it
shows one status line rewritten with the command's newest output line
(git clone phases via --progress), appends everything to
$HERMES_HOME/logs/install.log, and on failure prints the last 20 lines plus
the log path. CI, --verbose, HERMES_INSTALL_VERBOSE and a non-terminal
stdout (the Hermes-Setup --json driver, E2E transcripts) keep the full
stream, so their parsers see what they saw before. Errors stay on stderr.
release.py was 2,804 lines, 2,076 of them the frozen legacy author dict.
The map and its resolver now live in scripts/releases/: authors.py holds
the directory loader, the merged AUTHOR_MAP and resolve_author, and
authors_legacy.py holds only the frozen LEGACY_AUTHOR_MAP literal (same
1,895 entries, same order). release.py drops to 707 lines.
The contributor-check job and audit_pr_attribution.py grep the legacy
file for quoted emails, so both now read authors_legacy.py. The
importers (contributor_audit.py, add_contributor.py), contributors/README
and the tests read the defining modules. No behaviour change.
test_stable_release_graph and test_desktop_build_cache asserted gate text:
`== "always()"`, `== "false"`, `"conclusion != 'success'" in`,
`"!cancelled()" in`. Both files now evaluate the gate with the shared
evaluator, which also resolves `steps.*` now.
- The stable gates (acceptance, publication, complete) must run when every
ancestor failed.
- Deferred desktop e2e never runs.
- The publication reconciler runs after a failed, dispatched Stable Release
of this repository and on manual dispatch. It refuses a successful run, a
push-triggered run, and a fork's run.
- No desktop-build-cache step runs during cancellation. The service-failure
report runs when restore or save failed and stays quiet otherwise.
pm imported runtime_state's private helpers (_lock, _atomic_bytes, _bytes,
_digest) at a dozen sites while runtime_state imports pm.environments at
module top. The primitives are pm's: move them into the stdlib-only
pm.filesystem as lock_fd, durable_write_bytes, read_bytes_or_none and
file_digest, and repoint every pm caller.
runtime_state keeps the private names only as import aliases: it still
calls them through its own globals, and pre-PM updaters load them by these
names mid-swap (tests/compat/old_updater_surface.json).
Boot-subset test fixtures now copy pm/filesystem.py, since runtime_state
imports it at process boot; worker-injection tests patch the name
pm.publication now reads.
`release.py release` gains two flags. They can be used together.
--skip-bundles ships only the claim, the GitHub release, the final tag
and the Docker image. No desktop, Termux or PM bundle job runs. The
final tag records candidateManifestSha256: null. Publication moves only
the Docker stable/latest aliases. The R2 stable head, feeds, APT, the
downloads page, the signed-package baseline and the Store stay on the
previous bundle release.
--skip-tests builds, signs and publishes every artifact and runs no
test job: source CI, Nix, PM bundle check, Termux, Windows live,
install/update E2E, bootstrap identity, native smokes, upgrade
acceptance, tests/docker and the in-build vitest step. The candidate
manifest records each smoke as skipped, never as passed.
The flags live in the claim message (skipBundles, skipTests), next to
autopublish. They are not workflow inputs, so a rerun cannot change
them. admit emits them, and every job condition and gate reads them.
stable.validate_claim and stable.validate_final are now the one shape
check for stable.py and the sequencer.
The gates stay strict. SKIPPED_BY in stable.py maps each job to the
flags that remove it. `gate` requires those jobs to report skipped and
every other gated job to report success. A job that ran although a flag
removes it blocks the release.
A release that skipped bundles never moves the R2 stable head. Two
readers depended on that head:
- The next version was derived from it, so the next cut would reuse the
version. It now takes the newer of the R2 head and the newest
published non-prerelease GitHub release with a vX.Y.Z tag. Bare v*
tags do not count, because those refs are not protected yet.
- The sequencer used it to decide which published releases still need
their publication pass, so a bundle-less release would re-advance
every 15 minutes. The head is now the newer of the R2 head and the
published release whose final tag binds the Docker stable alias
digest.
`release` also refuses a cut when its next version already has a final
tag. That closes the window between the final tag and the public
release, where the published identity still names the old version.
Tests: 42 release test files, 546 passed. Three tests fail on this
Windows host, and they fail the same way on a clean HEAD worktree:
- test_stable_release_graph::test_docker_recovery_refuses_to_replace_a_divergent_version_tag
- test_release_artifacts::test_windows_metadata_is_read_from_package_and_stale_stamp_is_rejected
- test_tag_builds_summary::test_admitted_failure_publishes_tag_info_without_promoting_channel[True]
Not verified: no real Stable Release dispatch ran with either flag, and
actionlint is not installed on this host. The workflow changes are
checked by the graph tests and by running the phase-result step script.
`irm | iex` runs install.ps1 as text, which execution policy never
checks, but Invoke-InstalledHermes then dot-sourced runtime.ps1 from
disk. That is a file load, and the default Restricted policy (Windows
Sandbox, fresh machines) refused it right after "hermes command
installed". Load the helper from its text instead.
That failure hid a second one on the same path: the `$command` local
was shadowed inside Invoke-Native by its case-insensitive `$Command`
parameter, so `& $command[0]` invoked the scriptblock itself until the
call depth overflowed. Rename the local.
linux_only / macos_only / windows_only were replaced by platforms(...)
and conftest now rejects them, but test docstrings and comments still
explained gating in terms of the old names, which points readers at a
marker they cannot use. Reword them to platforms("<os>") and the
-m platforms lane. ci.yaml's comment already says platforms("windows").
utf-8-sig exists to tolerate BOMs that Windows tooling adds to files
users edit. /proc and /sys files are generated by the Linux kernel, never
BOM'd and absent on Windows, so -sig there only muddies the read/write
policy. Switch every literal /proc/ and /sys/ read to utf-8 and teach
the footgun read rule that string literals starting with /proc/ or
/sys/ are exempt (user-edited files keep utf-8-sig).
Stable Release Publication ran every 15 minutes (96 runs a day, each
checking out full history, setting up node and buildx, logging into
Docker Hub, and taking the release-signing environment) only because the
sequencer held a failed run for a 15-minute backoff that the failure
event could never satisfy, so the cron was what actually retried.
Drop the backoff: the reconcile pass started by a failed Stable Release
reruns its failed jobs right away. MAX_ATTEMPTS burning, oldest-first
retry ordering, the attempt-entry check, and the needs_retarget repair
stay. The schedule trigger goes; workflow_run and workflow_dispatch
remain the recovery paths.
The shared stable-release concurrency group cannot deadlock: the rerun
waits as pending behind this job, and the sequencer only confirms the
new attempt is queued before it exits and frees the group.
The documented one-liner, iex (irm .../install.ps1), runs the installer
inside the user's own session. Fail ended with exit 1, so any failed
stage closed the user's PowerShell window.
Fail now throws. The two entry points own reporting and the exit code:
-Stage prints the reason, emits the -Json frame and exits 1, as before;
the full install exits 1 only when it runs from a script file, and under
iex it prints the reason, sets LASTEXITCODE=1 and returns. A scriptblock
literal's File tells the two apart: $MyInvocation.MyCommand.Path names the
caller's script under iex.
PM launchers run python -I, which ignores PYTHONPATH, so the macOS
hermes-desktop-app-update route never loaded the sitecustomize capture hook.
Route PM launchers through pm-launch.py as the Linux driver already does, and
cover the packaged .app shape the macOS route launches.
The current source checker reports updateAvailable with behind null when GitHub
compare cannot count staged commits; the app-update predicate demanded an integer
behind and refused every HEAD->NEXT leg. Require behind > 0 only for the historical
shape without updateAvailable.
v2026.6.19's DMG bootstrap runs the same install.sh that writes .install_method,
so its macOS leg saw a dirty tree. Share the Linux driver's guarded exclude through
source-driver.sh and apply it before every app-driven macOS update.
Allow read-only merged-tag queries through the live-system guard, route checkpoint rekeying to its real ref-deletion owner, and update stale fixtures to exercise current update and PM boundaries. Fix the shutdown test wait by patching the bound server global.