feat(cli): per-node auto-update channel (decouple testnet from latest)#1295
Conversation
Adds `autoUpdate.channel` — an npm dist-tag a node tracks for auto-updates.
When set, the updater follows ONLY `dist-tags[channel]` instead of the
default `max(latest, dev, beta, next)`, so a cohort tracks its own release
line without being captured by whatever `latest` points at.
Why: mainnet will ship on `latest` as the official product, but the existing
testnet fleet also follows `latest` and must NOT roll onto mainnet (junk
graphs, unexpected upgrades). Channel pinning decouples the two — testnet
nodes track the `testnet` tag, freeing `latest` to become mainnet later.
Code:
- `AutoUpdateConfig.channel` + `NetworkConfig.autoUpdate.channel`, threaded
through `resolveAutoUpdateConfig`.
- `resolveLatestNpmVersion(log, allowPrerelease, channel?)`: when channel is
set, resolve exactly that tag (still honouring allowPrerelease; graceful
no-op when the tag does not exist yet). Default path unchanged.
- threaded through `checkForNpmVersionUpdate`, the daemon poll loop, and the
manual `dkg update` path.
- 5 unit tests incl. the isolation property + missing-tag no-op.
Config (Option Y — decouple, do not hard-freeze):
- testnet.json: `channel: "testnet"` (stays live on its own tag).
- mainnet-{base,gnosis,neuroweb}.json: `enabled: false` (held off until
launch; no node chases `latest`).
Rollout (NOT performed here): publish this build to BOTH `latest` (so the
fleet picks it up via the old logic on its last hop) AND `testnet` (so pinned
nodes have a target). Afterwards `latest` is free to point at mainnet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
F14 ("--no-start → faucet skipped when daemon not reachable") probed the
shared default port 9200, assuming nothing listens there. Under full-suite
parallelism another test file's daemon can bind 9200 the instant F14's
`/api/status` probe runs, making the daemon look reachable → faucet fires →
the assertion fails. The race is latent on main and surfaced by any change
that perturbs test scheduling (here: the added auto-update channel tests).
Fix: probe a guaranteed-unused ephemeral port (bind → capture → close) so the
connect() ECONNREFUSEs deterministically. Preserves the "real round-trip, no
fetch stub" design; only the probed port changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per review: rather than hold mainnet frozen (enabled:false) and later rely on
`latest`=mainnet, give mainnet its own dist-tag channel. Now NO network
depends on what `latest` points at — it's fully decoupled from auto-update
(free for default `npm install` convenience only).
- mainnet-{base,gnosis,neuroweb}: `channel: "mainnet"`, `allowPrerelease: false`
(production = stable-only), enabled. No-ops until a build is published to the
`mainnet` tag, so it's launch-ready with no later enable-flip needed (removes
the "frozen node can't re-enable itself" wrinkle).
- testnet stays on `channel: "testnet"`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
otReviewAgent
left a comment
There was a problem hiding this comment.
Operational Notice: Review Agent could not complete this review.
Business logic reviewer failed: retry_exhausted
`dkg init` rebuilds the autoUpdate block when auto-update is enabled, preserving `buildTimeoutMs` and `sshCommand` but (until now) dropping a locally-pinned `channel`. An operator who pinned a node to a custom cohort (e.g. "staging"/"qa") lost the pin on any rerun → silently fell back to the network default (or legacy `latest` behaviour), defeating the per-node channel guarantee. Preserve it alongside the other advanced unprompted fields. Scope: only the rebuild-on-enable path (init.ts) was affected. The disabled path keeps `existing.autoUpdate` verbatim; `ensure-dkg-node-config` never rebuilds an existing block; the openclaw strip only removes a local field that equals the network default (a differing pin is kept). Reported in #1295 review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
otReviewAgent
left a comment
There was a problem hiding this comment.
Operational Notice: Review Agent could not complete this review.
Business logic reviewer failed: retry_exhausted
…er guard Addresses PR #1295 review round 2 (P1/P2/P3): P1 — a pinned channel with no acceptable target (tag unpublished / prerelease rejected / non-semver) was reported as a false "up-to-date". It now resolves to a distinct `no-target` status, surfaced in `dkg update --check`, a daemon WARNING log, and `/api/status` (`updateChannelTargetMissing`). The runCheck→status mapping is extracted to a pure, unit-tested `deriveUpdateCheckState`: `no-target` reports `upToDate:true` so `/api/status` does NOT flip to `updateAvailable:true` (the trap), with `channelTargetMissing` carrying the signal and cleared on the available/up-to-date paths. The `dkg update` apply path handles `no-target` too. (The node already behaved correctly — never updated to a missing target — this adds visibility.) P2 — disabled-auto-apply + manual `dkg update`, and the daemon version-check-only path, hardcoded `allowPrerelease:true` and dropped the channel, letting an operator bypass the shipped `allowPrerelease:false` intent. Both now read allowPrerelease + channel from config/network. P3 — channel dist-tag values are validated as semver (`isLikelySemver`) before use; malformed → `no-target` instead of an update attempt on garbage (compareSemver NaN slipping the forward-only gate). The same guard covers the default tag set at the gate without changing its candidate selection. The `dkg init` channel-preservation finding was already fixed in 9f0eaed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for both reviews — all findings addressed across two commits. Round 1 —
|
otReviewAgent
left a comment
There was a problem hiding this comment.
Operational Notice: Review Agent could not complete this review.
Business logic reviewer failed: retry_exhausted
…aleness, real semver Addresses PR #1295 review round 3: P1 — `dkg init` could not persist "auto-update disabled": declining left the local block absent, so resolveAutoUpdateConfig fell through to the enabled network default. The persist decision is now a pure, exported `buildInitAutoUpdate` (enable + decline paths) consumed unconditionally, so declining writes `{ enabled: false }` (preserving existing advanced fields). Covered by test/init.test.ts (decline→disabled, decline-preserves-channel, enable-preserves-channel, persist-only-if-differs) — closing the init test gap. P2 — the daemon version-check-only path resolved allowPrerelease/channel network-before-config, reversing the resolver precedence (a round-2 regression). Now config-before-network, matching resolveAutoUpdateConfig + manual dkg update. P2 — `/api/status` could show a stale `latestVersion`. `deriveUpdateCheckState` always emits `latestVersion`, cleared on up-to-date / no-target; daemon always writes it. P2/P3 — semver hardening: official semver grammar (rejects e.g. `10.0.0-alpha..1`); build-metadata-aware prerelease detection so stable `10.0.0+mainnet-build.1` is not rejected under allowPrerelease=false. P3 — typed the auto-update status group (incl. `updateChannelTargetMissing`) on `DaemonStatusResponse`. P3 — OpenClaw setup strip no longer removes an explicit `channel` pin equal to the network default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Round 3 addressed — commit
Remaining nit (not in this commit): Full |
otReviewAgent
left a comment
There was a problem hiding this comment.
Operational Notice: Review Agent could not complete this review.
Business logic reviewer failed: retry_exhausted
|
Round-4 findings fixed in a follow-up PR (this one is merged): #1296.
All four verified + tested; full |
Summary
Adds
autoUpdate.channel— an npm dist-tag a node tracks for auto-updates — so each network tracks its own release line on the same npm package, and no node depends on whatlatestpoints at.Why: the node auto-updater follows
dist-tags.latest(+dev/beta/next). Without per-node channels, the momentlatestbecomes mainnet every testnet node auto-updates onto it (junk graphs, surprise upgrades). Dedicated channels decouplelatestfrom auto-update entirely.Commits
feat(cli): per-node auto-update channel— thechannelfield, threaded through the resolver,resolveLatestNpmVersion/checkForNpmVersionUpdate, the daemon poll loop, and manualdkg update; 5 unit tests.test(cli): harden mcp-setup F14 probe— a pre-existing port-race flake surfaced by this PR's test-scheduling shift (F14 probed the shared default 9200 for an "unreachable" assertion). Now probes a guaranteed-free ephemeral port. Independent — cherry-pickable.feat(config): mainnet tracks a dedicatedmainnetchannel— see below.Final config (symmetric —
latestfully decoupled)testnet.json→channel: "testnet"mainnet-{base,gnosis,neuroweb}.json→channel: "mainnet",allowPrerelease: false(stable-only), enabled. No-ops until a build is published to themainnettag — launch-ready with no later enable-flip.latestis followed by no auto-updating node (free for defaultnpm install).Resolution semantics
channelset → follow ONLYdist-tags[channel](honoursallowPrerelease; forward-only via the existingcompareSemvergate; graceful no-op if the tag is absent).channelunset → unchanged legacymax(latest, dev, beta, next).Rollout (manual — NOT performed here)
latest(existing testnet fleet's last old-logic hop) and to thetestnettag (target for the now-pinned nodes).latest. Long-offline nodes (still on old code followinglatest) need a manualdkg update.mainnettag; mainnet nodes (alreadychannel:mainnet, enabled) converge on it. Optionally pointlatest→mainnet for default installs.Verification
Bura: clisuite green locally;tsc --noEmitclean.🤖 Generated with Claude Code