feat(cli): add `npx e2e upgrade`
Adds e2e upgrade, with e2e update as an alias. It moves a project's e2e to the latest release, a dist-tag, or one version, and every @e2e-dev/* package the project lists along with it.
$ npx e2e upgrade # latest
$ npx e2e upgrade 0.21.0 # one version
$ npx e2e upgrade nightly # a dist-tag
$ npx e2e update --dry-run # the plan, nothing written
flowchart LR
A[nearest package.json<br/>that lists e2e] --> B[registry metadata<br/>e2e + @e2e-dev/*]
B --> C[e2e target<br/>tag or version]
C --> D[each sibling: same tag if its<br/>peer ranges accept, else newest that does]
D --> E[write specs into<br/>package.json]
E --> F[one manager install]
F -- fails or Ctrl-C --> G[put back package.json<br/>and changed lockfiles]
- Siblings follow
e2e. A deprecated release is never picked, and a prerelease only whene2egets one. An integration resolves after the engine it peers on, so@e2e-dev/kernelfollows@e2e-dev/webdown on a downgrade. Peer ranges are read by the newpackages/e2e/src/internal/semver.ts, which covers the shapes the packages publish (>=0.15.0 <1, an exact prerelease pin) and node-semver's prerelease rule. - One install. The new specs go into
package.json, each in its section, then one<manager> installresolves them together. An add command per package meets the olde2espec in npm's peer check (ERESOLVE) when packages sit in different sections. A named version or a prerelease is pinned exactly; a dist-tag keeps the old spec's^or~. pnpm gets--no-frozen-lockfileand YarnYARN_ENABLE_IMMUTABLE_INSTALLS=false, so CI works. - A failed or interrupted install puts back
package.jsonand every lockfile it changed, byte for byte (bun.lockbis binary). SIGINT and SIGTERM are held offe2ewhile the package manager runs, so Ctrl-C stops the install and the restore still runs. - Edge cases: a package listed in two sections is rewritten in both; the whole sibling plan's peer ranges are checked before anything is written; a Windows path spec (
..\\web) counts as linked. - Linked packages are left alone (
workspace:,file:,catalog:, a path, git). A linkede2eexits 2. - Package manager detection (
detectWorkspacePackageManagerinpackages/e2e/src/internal/package-manager.ts): the package's ownpackageManagerfield or lockfile (or pnpm at a pnpm workspace root), else those of a workspace root above whose globs include it (pnpm-workspace.yamlin block or flow form, orworkspaces). A lockfile above a project no workspace lists does not count;examples/with-viteotherwise resolved to this repository's pnpm. - The update notice from #1238 now suggests
e2e upgradefor the package that listse2e, through the same detection, instead ofe2e@latestalone, which left the engine behind. It stays quiet aftere2e upgradeitself. The skill saysnpx e2e@latest upgrade, since a skill fromnpx skills addcan be newer than the project's e2e.olderThanorders versions through the new semver module. - Telemetry: one
e2e_upgrade_completedevent per run: the result, the target kind or a dist-tag e2e publishes, thee2eversions before and after, the package manager, the sibling names inside the@e2e-devscope, and package counts (docs/telemetry.mdx). The security pages list the registry requests. e2e --helplists commands without their aliases;upgrade|updatewidened the column and wrapped every summary. The summary says(alias: update).
Exit codes: 0 installed, already up to date, or --dry-run; 2 for a range, an unknown version or tag, no project, a linked e2e, no fitting sibling release, or a failed install; 3 when the registry does not answer; 130 on Ctrl-C or SIGTERM during the install.
Verified
Ran it locally: yes. The built CLI against the live npm registry, in scratch projects and examples/with-vite.
npm, packages split across sections with an exact pin beside ^, the case where per-section add commands hit ERESOLVE:
$ e2e upgrade
Upgrading with npm:
e2e 0.15.0 → 0.21.2
@e2e-dev/web 0.12.0 → 0.15.2
@e2e-dev/github 0.3.3 → 0.4.2
package.json: "e2e": "0.21.2", "@e2e-dev/web": "~0.15.2", "@e2e-dev/github": "^0.4.2"
$ npm install
...
Upgraded: e2e 0.21.2, @e2e-dev/web 0.15.2, @e2e-dev/github 0.4.2
Release notes: https://github.com/tester-army/e2e/releases/tag/e2e%400.21.2
package.json kept its order and indentation; npm ls showed the new versions. Other runs:
| Case | Result |
|---|---|
upgrade 0.15.0 --dry-run on 0.21.2 | e2e 0.21.2 → 0.15.0 (downgrade), @e2e-dev/web → 0.12.0, exact pins |
upgrade nightly --dry-run | every package to its 0.x.y-nightly-20261019081103 build |
pnpm workspace member, unindented packages: list, run under npm_config_user_agent=npm | pnpm install --no-frozen-lockfile, no stray package-lock.json in the member |
Yarn 4 (Plug'n'Play) with CI=true | lockfile updated to e2e@npm:^0.21.2; a second run printed Already up to date |
npm project whose postinstall exits 1 | package.json and package-lock.json are back as before, both byte-identical, npm ci passes |
fake npm that sleeps, SIGINT sent to the process group | exit 130, package.json byte-identical |
an unresolvable dependency beside e2e | exit 2, package.json back |
upgrade ^0.20, upgrade 9.9.9, upgrade beta2, apps/testbed (workspace:*), no project | exit 2, nothing written |
yarn 1 with .agents/skills/e2e | upgraded, then refresh it with yarn e2e init |
Telemetry, from a copy of the build outside the checkout with E2E_TELEMETRY_DEBUG=1:
[telemetry] {"event":"e2e_upgrade_completed", ... "result":"dry-run","target":"version","from_version":"0.21.2","to_version":"0.20.0","prerelease":false,"manager":"npm","siblings":["kernel","web"],"packages_moved":0,"packages_linked":0, ...}
[telemetry] {"event":"e2e_upgrade_completed", ... "result":"unknown-target","target":"other","from_version":"0.21.2","to_version":null, ...}
Two fresh-context review rounds ran before this PR, and cubic's 22 findings after it (21 fixed, 1 disproved in its thread); the pre-PR findings (the ERESOLVE above, exact pins turned into ^, the notice after an upgrade, ^ eaten by cmd.exe, YAML list forms, Ctrl-C, lockfiles after a failed script, Yarn Berry in CI, Plug'n'Play) are fixed and covered by tests that fail without the fix.
Not verified: bun (not installed here), Windows, and the update notice on a real terminal; its unit tests cover the command it prints.
Found while verifying, not fixed here: @e2e-dev/web 0.11.2 declares e2e >=0.15.0 <1 but imports rejectUnknownOptions, which e2e first exports in 0.16.0, so a fresh install of examples/with-vite on main fails with CONFIG_LOAD_FAILED. e2e upgrade trusts published peer ranges; deprecating the affected @e2e-dev/web releases on npm would keep it from picking them.