mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 00:11:19 +00:00
feat(update): notify extended-stable availability (#100438)
* docs(update): clarify extended-stable notifications * feat(update): notify extended-stable availability * fix(update): isolate notification state * fix(docs): align update channel table * fix(update): refresh after channel switch * fix(update): satisfy notification checks * test(update): prove loopback notification path * test(update): fix loopback server lifecycle * fix(update): refresh extended stable without cache * fix(update): retain extended stable throttle marker
This commit is contained in:
parent
ad833d7883
commit
e3c40efeb0
8 changed files with 687 additions and 83 deletions
|
|
@ -155,9 +155,11 @@ from outside the Gateway process tree. If the handoff is unavailable,
|
|||
`update.run` returns a structured response with the safe shell command to run
|
||||
manually.
|
||||
|
||||
Extended-stable is deliberately excluded from startup checks and background
|
||||
auto-update scheduling. Explicit foreground updates, bare foreground updates
|
||||
with stored `update.channel: "extended-stable"`, on-demand status, and managed
|
||||
Stored extended-stable selections receive read-only startup and 24-hour update
|
||||
hints when `update.checkOnStart` is enabled. These checks never apply an update,
|
||||
start a handoff, restart the Gateway, use stable delay/jitter, or use beta
|
||||
polling cadence. Explicit foreground updates, bare foreground updates with
|
||||
stored `update.channel: "extended-stable"`, on-demand status, and their managed
|
||||
Gateway handoff remain supported.
|
||||
|
||||
When a local managed Gateway service is installed and restart is enabled,
|
||||
|
|
|
|||
|
|
@ -1212,12 +1212,12 @@ rows. Query them with [`openclaw audit`](/cli/audit) or the
|
|||
}
|
||||
```
|
||||
|
||||
- `channel`: release channel - `"stable"`, `"extended-stable"`, `"beta"`, or `"dev"`. Extended-stable is a package-only, foreground/on-demand channel; it is skipped by startup checks and background auto-update.
|
||||
- `checkOnStart`: check for npm updates when the gateway starts (default: `true`).
|
||||
- `auto.enabled`: enable background auto-update for package installs (default: `false`).
|
||||
- `channel`: release channel - `"stable"`, `"extended-stable"`, `"beta"`, or `"dev"`. Extended-stable is package-only: foreground commands own installation, while the Gateway may emit read-only update hints.
|
||||
- `checkOnStart`: check for npm updates when the gateway starts (default: `true`). Stored extended-stable selections use the same read-only hint and 24-hour hint schedule.
|
||||
- `auto.enabled`: enable background auto-update for stable and beta package installs (default: `false`). Extended-stable never applies automatically.
|
||||
- `auto.stableDelayHours`: minimum delay in hours before stable-channel auto-apply (default: `6`; max: `168`).
|
||||
- `auto.stableJitterHours`: extra stable-channel rollout spread window in hours (default: `12`; max: `168`).
|
||||
- `auto.betaCheckIntervalHours`: how often beta-channel checks run in hours (default: `1`; max: `24`).
|
||||
- `auto.betaCheckIntervalHours`: how often beta-channel checks run in hours (default: `1`; max: `24`). Stable delay/jitter and beta polling settings do not apply to extended-stable.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ OpenClaw ships four update channels:
|
|||
|
||||
- **stable**: npm dist-tag `latest`. Recommended for most users.
|
||||
- **extended-stable**: npm dist-tag `extended-stable`. A net-new, trailing
|
||||
supported-month package channel. It is package-only and foreground-only in
|
||||
this release.
|
||||
supported-month package channel. It is package-only, and installation is
|
||||
foreground-only. A stored selection receives read-only update hints when
|
||||
`update.checkOnStart` is enabled, but never applies automatically.
|
||||
- **beta**: npm dist-tag `beta`. Falls back to `latest` when `beta` is missing
|
||||
or older than the current stable release.
|
||||
- **dev**: moving head of `main` (git). npm dist-tag `dev` when published. `main`
|
||||
|
|
|
|||
|
|
@ -34,11 +34,14 @@ when the beta tag is missing or its version is older than the latest stable
|
|||
release. Use `--tag beta` for a one-off package update pinned to the raw npm
|
||||
beta dist-tag instead.
|
||||
|
||||
`--channel extended-stable` is package-only and foreground-only. OpenClaw reads
|
||||
the public npm `extended-stable` selector, verifies the selected exact package,
|
||||
and installs that exact version. Missing or inconsistent registry data fails
|
||||
closed; it never falls back to `latest`. If the selected version is older than
|
||||
the installed version, the normal downgrade confirmation still applies.
|
||||
`--channel extended-stable` is package-only, and installation remains
|
||||
foreground-only. OpenClaw reads the public npm `extended-stable` selector,
|
||||
verifies the selected exact package, and installs that exact version. Missing
|
||||
or inconsistent registry data fails closed; it never falls back to `latest`.
|
||||
If the selected version is older than the installed version, the normal
|
||||
downgrade confirmation still applies. The CLI persists the channel after a
|
||||
successful core update; a direct `npm install -g openclaw@extended-stable`
|
||||
does not update `update.channel`.
|
||||
After the core swap, eligible official npm plugins with bare/default or
|
||||
`latest` intent converge to that exact core version. Exact pins and explicit
|
||||
non-`latest` tags, third-party plugins, and non-npm sources remain unchanged.
|
||||
|
|
@ -213,12 +216,14 @@ Off by default. Enable it in `~/.openclaw/openclaw.json`:
|
|||
| Channel | Behavior |
|
||||
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `stable` | Waits `stableDelayHours` (default: 6), then applies with deterministic jitter across `stableJitterHours` (default: 12) for a spread rollout. |
|
||||
| `extended-stable` | No startup check or automatic apply. Use `openclaw update` or `openclaw update status` manually. |
|
||||
| `extended-stable` | Checks for a read-only update hint on startup and every 24 hours when `checkOnStart` is enabled. Never applies automatically. |
|
||||
| `beta` | Checks every `betaCheckIntervalHours` (default: 1) and applies immediately. |
|
||||
| `dev` | No automatic apply. Use `openclaw update` manually. |
|
||||
|
||||
The gateway also logs an update hint on startup (disable with `update.checkOnStart: false`).
|
||||
Stored extended-stable selections skip startup and background resolution entirely.
|
||||
The gateway also logs an update hint on startup (disable with
|
||||
`update.checkOnStart: false`). Stored extended-stable selections use this
|
||||
read-only hint path and the existing 24-hour hint interval, but never invoke
|
||||
automatic installation, handoff, restart, stable delay/jitter, or beta polling.
|
||||
For downgrade or incident recovery, set `OPENCLAW_NO_AUTO_UPDATE=1` in the gateway environment to block automatic applies even when `update.auto.enabled` is configured. Startup update hints can still run unless `update.checkOnStart` is also disabled.
|
||||
|
||||
Package-manager updates requested through the live Gateway control-plane
|
||||
|
|
|
|||
|
|
@ -71,9 +71,11 @@ export const FIELD_HELP: Record<string, string> = {
|
|||
update:
|
||||
"Update-channel and startup-check behavior for keeping OpenClaw runtime versions current. Use conservative channels in production and more experimental channels only in controlled environments.",
|
||||
"update.channel":
|
||||
'Update channel for git + npm installs ("stable", "extended-stable", "beta", or "dev"). Extended-stable is package-only and foreground-only.',
|
||||
"update.checkOnStart": "Check for npm updates when the gateway starts (default: true).",
|
||||
"update.auto.enabled": "Enable background auto-update for package installs (default: false).",
|
||||
'Update channel for git + npm installs ("stable", "extended-stable", "beta", or "dev"). Extended-stable is package-only: installation is foreground-only, with optional read-only startup hints.',
|
||||
"update.checkOnStart":
|
||||
"Check for npm updates when the gateway starts, including read-only extended-stable hints (default: true).",
|
||||
"update.auto.enabled":
|
||||
"Enable background auto-update for stable and beta package installs; extended-stable never auto-applies (default: false).",
|
||||
"update.auto.stableDelayHours":
|
||||
"Minimum delay before stable-channel auto-apply starts (default: 6).",
|
||||
"update.auto.stableJitterHours":
|
||||
|
|
|
|||
109
src/infra/update-startup.integration.test.ts
Normal file
109
src/infra/update-startup.integration.test.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// Proves startup update discovery through the real extended-stable registry resolver.
|
||||
import http from "node:http";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
|
||||
import {
|
||||
createOpenClawTestState,
|
||||
type OpenClawTestState,
|
||||
} from "../test-utils/openclaw-test-state.js";
|
||||
import type { UpdateCheckResult } from "./update-check.js";
|
||||
|
||||
vi.mock("./openclaw-root.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./openclaw-root.js")>("./openclaw-root.js");
|
||||
return {
|
||||
...actual,
|
||||
resolveOpenClawPackageRoot: vi.fn(async () => "/opt/openclaw"),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./update-check.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("./update-check.js")>("./update-check.js");
|
||||
return {
|
||||
...actual,
|
||||
checkUpdateStatus: vi.fn(async () => ({
|
||||
root: "/opt/openclaw",
|
||||
installKind: "package",
|
||||
packageManager: "npm",
|
||||
}) satisfies UpdateCheckResult),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../version.js", () => ({
|
||||
VERSION: "1.0.0",
|
||||
}));
|
||||
|
||||
describe("extended-stable startup update integration", () => {
|
||||
let testState: OpenClawTestState;
|
||||
let server: http.Server | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
server = undefined;
|
||||
testState = await createOpenClawTestState({
|
||||
layout: "state-only",
|
||||
prefix: "openclaw-update-startup-integration-",
|
||||
env: {
|
||||
NODE_ENV: "test",
|
||||
NPM_CONFIG_REGISTRY: undefined,
|
||||
OPENCLAW_UPDATE_PACKAGE_SPEC: undefined,
|
||||
VITEST: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const activeServer = server;
|
||||
if (activeServer) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
activeServer.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
}
|
||||
closeOpenClawStateDatabaseForTest();
|
||||
await testState.cleanup();
|
||||
});
|
||||
|
||||
it("emits a read-only hint after verifying a newer exact loopback package", async () => {
|
||||
const requests: string[] = [];
|
||||
const registryServer = http.createServer((request, response) => {
|
||||
requests.push(request.url ?? "");
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify({ version: "2.0.0" }));
|
||||
});
|
||||
server = registryServer;
|
||||
await new Promise<void>((resolve) => {
|
||||
registryServer.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const address = registryServer.address();
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("expected loopback registry address");
|
||||
}
|
||||
process.env.OPENCLAW_UPDATE_PACKAGE_SPEC = "openclaw";
|
||||
process.env.NPM_CONFIG_REGISTRY = `http://127.0.0.1:${address.port}/`;
|
||||
|
||||
const { runGatewayUpdateCheck, resetUpdateAvailableStateForTest } =
|
||||
await import("./update-startup.js");
|
||||
resetUpdateAvailableStateForTest();
|
||||
const log = { info: vi.fn() };
|
||||
const onUpdateAvailableChange = vi.fn();
|
||||
const runAutoUpdate = vi.fn();
|
||||
|
||||
await runGatewayUpdateCheck({
|
||||
cfg: { update: { channel: "extended-stable", auto: { enabled: true } } },
|
||||
log,
|
||||
isNixMode: false,
|
||||
allowInTests: true,
|
||||
onUpdateAvailableChange,
|
||||
runAutoUpdate,
|
||||
});
|
||||
|
||||
expect(requests).toEqual(["/openclaw/extended-stable", "/openclaw/2.0.0"]);
|
||||
expect(onUpdateAvailableChange).toHaveBeenCalledWith({
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: "2.0.0",
|
||||
channel: "extended-stable",
|
||||
});
|
||||
expect(log.info).toHaveBeenCalledWith(
|
||||
"update available (extended-stable): v2.0.0 (current v1.0.0). Run: openclaw update",
|
||||
);
|
||||
expect(runAutoUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -337,6 +337,72 @@ describe("update-startup", () => {
|
|||
};
|
||||
}
|
||||
|
||||
function createExtendedStableConfig(params?: {
|
||||
checkOnStart?: boolean;
|
||||
autoEnabled?: boolean;
|
||||
}) {
|
||||
return {
|
||||
update: {
|
||||
...(params?.checkOnStart === false ? { checkOnStart: false } : {}),
|
||||
channel: "extended-stable" as const,
|
||||
...(params?.autoEnabled ? { auto: { enabled: true } } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function runExtendedStableUpdateCheck(params?: {
|
||||
cfg?: ReturnType<typeof createExtendedStableConfig>;
|
||||
log?: Parameters<typeof runGatewayUpdateCheck>[0]["log"];
|
||||
onUpdateAvailableChange?: Parameters<
|
||||
typeof runGatewayUpdateCheck
|
||||
>[0]["onUpdateAvailableChange"];
|
||||
runAutoUpdate?: ReturnType<typeof createAutoUpdateSuccessMock>;
|
||||
isNixMode?: boolean;
|
||||
}) {
|
||||
const log = params?.log ?? { info: vi.fn() };
|
||||
await runGatewayUpdateCheck({
|
||||
cfg: params?.cfg ?? createExtendedStableConfig(),
|
||||
log,
|
||||
isNixMode: params?.isNixMode ?? false,
|
||||
allowInTests: true,
|
||||
...(params?.onUpdateAvailableChange
|
||||
? { onUpdateAvailableChange: params.onUpdateAvailableChange }
|
||||
: {}),
|
||||
...(params?.runAutoUpdate ? { runAutoUpdate: params.runAutoUpdate } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
async function seedExtendedStableAvailability(params?: {
|
||||
onUpdateAvailableChange?: Parameters<
|
||||
typeof runGatewayUpdateCheck
|
||||
>[0]["onUpdateAvailableChange"];
|
||||
}) {
|
||||
mockPackageInstallStatus();
|
||||
mockNpmChannelTag("extended-stable", "2.0.0");
|
||||
await runExtendedStableUpdateCheck({
|
||||
onUpdateAvailableChange: params?.onUpdateAvailableChange,
|
||||
});
|
||||
}
|
||||
|
||||
function seedStableAutoRolloutState() {
|
||||
writePersistedUpdateCheckState({
|
||||
...readPersistedUpdateCheckState(),
|
||||
autoInstallId: "stable-install-id",
|
||||
autoFirstSeenVersion: "3.0.0",
|
||||
autoFirstSeenTag: "latest",
|
||||
autoFirstSeenAt: "2026-01-16T10:00:00.000Z",
|
||||
});
|
||||
}
|
||||
|
||||
function expectStableAutoRolloutStatePreserved() {
|
||||
expect(readPersistedUpdateCheckState()).toMatchObject({
|
||||
autoInstallId: "stable-install-id",
|
||||
autoFirstSeenVersion: "3.0.0",
|
||||
autoFirstSeenTag: "latest",
|
||||
autoFirstSeenAt: "2026-01-16T10:00:00.000Z",
|
||||
});
|
||||
}
|
||||
|
||||
async function runAutoUpdateCheckWithDefaults(params: {
|
||||
cfg: { update?: Record<string, unknown> };
|
||||
runAutoUpdate?: ReturnType<typeof createAutoUpdateSuccessMock>;
|
||||
|
|
@ -422,35 +488,171 @@ describe("update-startup", () => {
|
|||
expect(parsed?.lastAvailableVersion).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("hydrates cached update from persisted state during throttle window", async () => {
|
||||
it.each([
|
||||
{
|
||||
channel: "stable" as const,
|
||||
persistedTag: undefined,
|
||||
expectedTag: "latest",
|
||||
preflightsInstallKind: false,
|
||||
},
|
||||
{
|
||||
channel: "stable" as const,
|
||||
persistedTag: "latest",
|
||||
expectedTag: "latest",
|
||||
preflightsInstallKind: false,
|
||||
},
|
||||
{
|
||||
channel: "beta" as const,
|
||||
persistedTag: "beta",
|
||||
expectedTag: "beta",
|
||||
preflightsInstallKind: false,
|
||||
},
|
||||
{
|
||||
channel: "beta" as const,
|
||||
persistedTag: "latest",
|
||||
expectedTag: "latest",
|
||||
preflightsInstallKind: false,
|
||||
},
|
||||
{
|
||||
channel: "extended-stable" as const,
|
||||
persistedTag: "extended-stable",
|
||||
expectedTag: "extended-stable",
|
||||
preflightsInstallKind: true,
|
||||
},
|
||||
{
|
||||
channel: "dev" as const,
|
||||
persistedTag: "dev",
|
||||
expectedTag: "dev",
|
||||
preflightsInstallKind: false,
|
||||
},
|
||||
])(
|
||||
"hydrates $channel cached availability from its compatible $expectedTag tag",
|
||||
async ({ channel, persistedTag, expectedTag, preflightsInstallKind }) => {
|
||||
writePersistedUpdateCheckState({
|
||||
lastCheckedAt: new Date(Date.now()).toISOString(),
|
||||
lastAvailableVersion: "2.0.0",
|
||||
lastAvailableTag: persistedTag,
|
||||
});
|
||||
const onUpdateAvailableChange = vi.fn();
|
||||
|
||||
await runGatewayUpdateCheck({
|
||||
cfg: { update: { channel } },
|
||||
log: { info: vi.fn() },
|
||||
isNixMode: false,
|
||||
allowInTests: true,
|
||||
onUpdateAvailableChange,
|
||||
});
|
||||
|
||||
expect(checkUpdateStatus).toHaveBeenCalledTimes(preflightsInstallKind ? 1 : 0);
|
||||
expect(resolveNpmChannelTag).not.toHaveBeenCalled();
|
||||
expect(onUpdateAvailableChange).toHaveBeenCalledWith({
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: "2.0.0",
|
||||
channel: expectedTag,
|
||||
});
|
||||
expect(getUpdateAvailable()).toEqual({
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: "2.0.0",
|
||||
channel: expectedTag,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ channel: "stable" as const, persistedTag: "beta" },
|
||||
{ channel: "stable" as const, persistedTag: "extended-stable" },
|
||||
{ channel: "beta" as const, persistedTag: undefined },
|
||||
{ channel: "beta" as const, persistedTag: "extended-stable" },
|
||||
{ channel: "dev" as const, persistedTag: "latest" },
|
||||
])(
|
||||
"suppresses $persistedTag persisted availability on the $channel channel",
|
||||
async ({ channel, persistedTag }) => {
|
||||
writePersistedUpdateCheckState({
|
||||
lastCheckedAt: new Date(Date.now()).toISOString(),
|
||||
lastAvailableVersion: "2.0.0",
|
||||
lastAvailableTag: persistedTag,
|
||||
});
|
||||
const onUpdateAvailableChange = vi.fn();
|
||||
|
||||
await runGatewayUpdateCheck({
|
||||
cfg: { update: { channel } },
|
||||
log: { info: vi.fn() },
|
||||
isNixMode: false,
|
||||
allowInTests: true,
|
||||
onUpdateAvailableChange,
|
||||
});
|
||||
|
||||
expect(checkUpdateStatus).not.toHaveBeenCalled();
|
||||
expect(resolveNpmChannelTag).not.toHaveBeenCalled();
|
||||
expect(onUpdateAvailableChange).not.toHaveBeenCalled();
|
||||
expect(getUpdateAvailable()).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["latest", "beta"])(
|
||||
"bypasses the shared throttle for mismatched %s availability on extended-stable",
|
||||
async (persistedTag) => {
|
||||
writePersistedUpdateCheckState({
|
||||
lastCheckedAt: new Date(Date.now()).toISOString(),
|
||||
lastAvailableVersion: "2.0.0",
|
||||
lastAvailableTag: persistedTag,
|
||||
});
|
||||
mockPackageUpdateStatus("extended-stable", "2.0.0");
|
||||
const onUpdateAvailableChange = vi.fn();
|
||||
|
||||
await runExtendedStableUpdateCheck({ onUpdateAvailableChange });
|
||||
|
||||
expect(checkUpdateStatus).toHaveBeenCalledTimes(1);
|
||||
expect(resolveNpmChannelTag).toHaveBeenCalledWith({
|
||||
channel: "extended-stable",
|
||||
timeoutMs: 2500,
|
||||
});
|
||||
expect(onUpdateAvailableChange).toHaveBeenCalledWith({
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: "2.0.0",
|
||||
channel: "extended-stable",
|
||||
});
|
||||
expect(readPersistedUpdateCheckState()).toMatchObject({
|
||||
lastAvailableVersion: "2.0.0",
|
||||
lastAvailableTag: "extended-stable",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("bypasses a recent empty prior-channel check on extended-stable", async () => {
|
||||
writePersistedUpdateCheckState({
|
||||
lastCheckedAt: new Date(Date.now()).toISOString(),
|
||||
lastAvailableVersion: "2.0.0",
|
||||
lastAvailableTag: "latest",
|
||||
});
|
||||
mockPackageUpdateStatus("extended-stable", "2.0.0");
|
||||
|
||||
const onUpdateAvailableChange = vi.fn();
|
||||
await runGatewayUpdateCheck({
|
||||
cfg: { update: { channel: "stable" } },
|
||||
log: { info: vi.fn() },
|
||||
isNixMode: false,
|
||||
allowInTests: true,
|
||||
onUpdateAvailableChange,
|
||||
});
|
||||
await runExtendedStableUpdateCheck();
|
||||
|
||||
expect(vi.mocked(checkUpdateStatus)).not.toHaveBeenCalled();
|
||||
expect(onUpdateAvailableChange).toHaveBeenCalledWith({
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: "2.0.0",
|
||||
channel: "latest",
|
||||
expect(checkUpdateStatus).toHaveBeenCalledTimes(1);
|
||||
expect(resolveNpmChannelTag).toHaveBeenCalledWith({
|
||||
channel: "extended-stable",
|
||||
timeoutMs: 2500,
|
||||
});
|
||||
expect(getUpdateAvailable()).toEqual({
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: "2.0.0",
|
||||
channel: "latest",
|
||||
channel: "extended-stable",
|
||||
});
|
||||
});
|
||||
|
||||
it("honors the shared throttle after a recent extended-stable check marker", async () => {
|
||||
writePersistedUpdateCheckState({
|
||||
lastCheckedAt: new Date(Date.now()).toISOString(),
|
||||
lastAvailableVersion: "1.0.0",
|
||||
lastAvailableTag: "extended-stable",
|
||||
});
|
||||
mockPackageUpdateStatus("extended-stable", "2.0.0");
|
||||
|
||||
await runExtendedStableUpdateCheck();
|
||||
|
||||
expect(resolveNpmChannelTag).not.toHaveBeenCalled();
|
||||
expect(getUpdateAvailable()).toBeNull();
|
||||
});
|
||||
|
||||
it("emits update change callback when update state clears", async () => {
|
||||
mockPackageInstallStatus();
|
||||
vi.mocked(resolveNpmChannelTag)
|
||||
|
|
@ -492,21 +694,68 @@ describe("update-startup", () => {
|
|||
await expectPathMissing(path.join(tempDir, "update-check.json"));
|
||||
});
|
||||
|
||||
it("skips all startup and background work for extended-stable", async () => {
|
||||
it("discovers and deduplicates an exact extended-stable update without auto-applying", async () => {
|
||||
const onUpdateAvailableChange = vi.fn();
|
||||
const runAutoUpdate = createAutoUpdateSuccessMock();
|
||||
mockPackageUpdateStatus("extended-stable", "2.0.0");
|
||||
const log = { info: vi.fn() };
|
||||
|
||||
await runExtendedStableUpdateCheck({
|
||||
cfg: createExtendedStableConfig({ autoEnabled: true }),
|
||||
log,
|
||||
onUpdateAvailableChange,
|
||||
runAutoUpdate,
|
||||
});
|
||||
vi.setSystemTime(new Date("2026-01-18T11:00:00Z"));
|
||||
await runExtendedStableUpdateCheck({
|
||||
cfg: createExtendedStableConfig({ autoEnabled: true }),
|
||||
log,
|
||||
onUpdateAvailableChange,
|
||||
runAutoUpdate,
|
||||
});
|
||||
|
||||
expect(resolveNpmChannelTag).toHaveBeenCalledTimes(2);
|
||||
expect(resolveNpmChannelTag).toHaveBeenNthCalledWith(1, {
|
||||
channel: "extended-stable",
|
||||
timeoutMs: 2500,
|
||||
});
|
||||
expect(log.info).toHaveBeenCalledTimes(1);
|
||||
expect(log.info).toHaveBeenCalledWith(
|
||||
`update available (extended-stable): v2.0.0 (current v1.0.0). Run: ${formatCliCommand("openclaw update")}`,
|
||||
);
|
||||
expect(onUpdateAvailableChange).toHaveBeenCalledTimes(1);
|
||||
expect(onUpdateAvailableChange).toHaveBeenCalledWith({
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: "2.0.0",
|
||||
channel: "extended-stable",
|
||||
});
|
||||
expect(getUpdateAvailable()).toEqual({
|
||||
currentVersion: "1.0.0",
|
||||
latestVersion: "2.0.0",
|
||||
channel: "extended-stable",
|
||||
});
|
||||
expect(readPersistedUpdateCheckState()).toMatchObject({
|
||||
lastNotifiedVersion: "2.0.0",
|
||||
lastNotifiedTag: "extended-stable",
|
||||
lastAvailableVersion: "2.0.0",
|
||||
lastAvailableTag: "extended-stable",
|
||||
});
|
||||
expect(runAutoUpdate).not.toHaveBeenCalled();
|
||||
expect(startManagedServiceUpdateHandoffMock).not.toHaveBeenCalled();
|
||||
expect(scheduleGatewaySigusr1RestartMock).not.toHaveBeenCalled();
|
||||
expect(readPersistedUpdateCheckState()?.autoFirstSeenVersion).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does no extended-stable hint or auto work when checkOnStart is false", async () => {
|
||||
await seedExtendedStableAvailability();
|
||||
vi.mocked(resolveOpenClawPackageRoot).mockClear();
|
||||
vi.mocked(checkUpdateStatus).mockClear();
|
||||
vi.mocked(resolveNpmChannelTag).mockClear();
|
||||
const onUpdateAvailableChange = vi.fn();
|
||||
const runAutoUpdate = createAutoUpdateSuccessMock();
|
||||
|
||||
await runGatewayUpdateCheck({
|
||||
cfg: {
|
||||
update: {
|
||||
channel: "extended-stable",
|
||||
checkOnStart: true,
|
||||
auto: { enabled: true },
|
||||
},
|
||||
},
|
||||
log: { info: vi.fn() },
|
||||
isNixMode: false,
|
||||
allowInTests: true,
|
||||
await runExtendedStableUpdateCheck({
|
||||
cfg: createExtendedStableConfig({ checkOnStart: false, autoEnabled: true }),
|
||||
onUpdateAvailableChange,
|
||||
runAutoUpdate,
|
||||
});
|
||||
|
|
@ -515,8 +764,139 @@ describe("update-startup", () => {
|
|||
expect(checkUpdateStatus).not.toHaveBeenCalled();
|
||||
expect(resolveNpmChannelTag).not.toHaveBeenCalled();
|
||||
expect(runAutoUpdate).not.toHaveBeenCalled();
|
||||
expect(readPersistedUpdateCheckState()).toBeNull();
|
||||
expect(startManagedServiceUpdateHandoffMock).not.toHaveBeenCalled();
|
||||
expect(scheduleGatewaySigusr1RestartMock).not.toHaveBeenCalled();
|
||||
expect(onUpdateAvailableChange).toHaveBeenCalledOnce();
|
||||
expect(onUpdateAvailableChange).toHaveBeenCalledWith(null);
|
||||
expect(getUpdateAvailable()).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "equal", version: "1.0.0" },
|
||||
{ name: "older", version: "0.9.0" },
|
||||
])("clears stale extended-stable availability for an $name target", async ({ version }) => {
|
||||
const onUpdateAvailableChange = vi.fn();
|
||||
await seedExtendedStableAvailability({ onUpdateAvailableChange });
|
||||
seedStableAutoRolloutState();
|
||||
onUpdateAvailableChange.mockClear();
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "extended-stable",
|
||||
version,
|
||||
});
|
||||
vi.setSystemTime(new Date("2026-01-18T11:00:00Z"));
|
||||
const log = { info: vi.fn() };
|
||||
|
||||
await runExtendedStableUpdateCheck({ log, onUpdateAvailableChange });
|
||||
|
||||
expect(log.info).not.toHaveBeenCalled();
|
||||
expect(onUpdateAvailableChange).toHaveBeenCalledOnce();
|
||||
expect(onUpdateAvailableChange).toHaveBeenCalledWith(null);
|
||||
expect(getUpdateAvailable()).toBeNull();
|
||||
expect(readPersistedUpdateCheckState()).toMatchObject({
|
||||
lastNotifiedVersion: "2.0.0",
|
||||
lastNotifiedTag: "extended-stable",
|
||||
});
|
||||
expect(readPersistedUpdateCheckState()?.lastAvailableVersion).toBeUndefined();
|
||||
expect(readPersistedUpdateCheckState()?.lastAvailableTag).toBe("extended-stable");
|
||||
expectStableAutoRolloutStatePreserved();
|
||||
});
|
||||
|
||||
it.each(["selector_missing", "selector_query_failed", "exact_package_mismatch"] as const)(
|
||||
"clears stale extended-stable availability when exact resolution fails with %s",
|
||||
async (failure) => {
|
||||
const onUpdateAvailableChange = vi.fn();
|
||||
await seedExtendedStableAvailability({ onUpdateAvailableChange });
|
||||
seedStableAutoRolloutState();
|
||||
onUpdateAvailableChange.mockClear();
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "extended-stable",
|
||||
version: null,
|
||||
reason: failure,
|
||||
});
|
||||
vi.setSystemTime(new Date("2026-01-18T11:00:00Z"));
|
||||
const log = { info: vi.fn() };
|
||||
|
||||
await runExtendedStableUpdateCheck({ log, onUpdateAvailableChange });
|
||||
|
||||
expect(log.info).not.toHaveBeenCalled();
|
||||
expect(onUpdateAvailableChange).toHaveBeenCalledOnce();
|
||||
expect(onUpdateAvailableChange).toHaveBeenCalledWith(null);
|
||||
expect(getUpdateAvailable()).toBeNull();
|
||||
expect(readPersistedUpdateCheckState()?.lastAvailableVersion).toBeUndefined();
|
||||
expect(readPersistedUpdateCheckState()?.lastAvailableTag).toBe("extended-stable");
|
||||
expectStableAutoRolloutStatePreserved();
|
||||
expect(startManagedServiceUpdateHandoffMock).not.toHaveBeenCalled();
|
||||
expect(scheduleGatewaySigusr1RestartMock).not.toHaveBeenCalled();
|
||||
|
||||
await runExtendedStableUpdateCheck({ log, onUpdateAvailableChange });
|
||||
expect(resolveNpmChannelTag).toHaveBeenCalledTimes(2);
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves cross-channel persisted availability when extended-stable resolution fails", async () => {
|
||||
writePersistedUpdateCheckState({
|
||||
lastCheckedAt: "2026-01-16T10:00:00.000Z",
|
||||
lastAvailableVersion: "2.0.0",
|
||||
lastAvailableTag: "latest",
|
||||
});
|
||||
mockPackageInstallStatus();
|
||||
vi.mocked(resolveNpmChannelTag).mockResolvedValue({
|
||||
tag: "extended-stable",
|
||||
version: null,
|
||||
reason: "selector_query_failed",
|
||||
});
|
||||
const onUpdateAvailableChange = vi.fn();
|
||||
|
||||
await runExtendedStableUpdateCheck({ onUpdateAvailableChange });
|
||||
|
||||
expect(onUpdateAvailableChange).not.toHaveBeenCalled();
|
||||
expect(getUpdateAvailable()).toBeNull();
|
||||
expect(readPersistedUpdateCheckState()).toMatchObject({
|
||||
lastAvailableVersion: "2.0.0",
|
||||
lastAvailableTag: "latest",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not resolve the npm channel for an extended-stable Git install", async () => {
|
||||
await seedExtendedStableAvailability();
|
||||
seedStableAutoRolloutState();
|
||||
resetUpdateAvailableStateForTest();
|
||||
vi.mocked(resolveOpenClawPackageRoot).mockClear();
|
||||
vi.mocked(checkUpdateStatus).mockClear();
|
||||
vi.mocked(resolveNpmChannelTag).mockClear();
|
||||
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue("/opt/openclaw");
|
||||
vi.mocked(checkUpdateStatus).mockResolvedValue({
|
||||
root: "/opt/openclaw",
|
||||
installKind: "git",
|
||||
packageManager: "unknown",
|
||||
} satisfies UpdateCheckResult);
|
||||
const runAutoUpdate = createAutoUpdateSuccessMock();
|
||||
const onUpdateAvailableChange = vi.fn();
|
||||
|
||||
await runExtendedStableUpdateCheck({ onUpdateAvailableChange, runAutoUpdate });
|
||||
|
||||
expect(checkUpdateStatus).toHaveBeenCalledTimes(1);
|
||||
expect(resolveNpmChannelTag).not.toHaveBeenCalled();
|
||||
expect(runAutoUpdate).not.toHaveBeenCalled();
|
||||
expect(onUpdateAvailableChange).not.toHaveBeenCalled();
|
||||
expect(getUpdateAvailable()).toBeNull();
|
||||
expect(readPersistedUpdateCheckState()).toMatchObject({
|
||||
lastAvailableVersion: "2.0.0",
|
||||
lastAvailableTag: "extended-stable",
|
||||
});
|
||||
expectStableAutoRolloutStatePreserved();
|
||||
});
|
||||
|
||||
it("skips all extended-stable work in Nix mode", async () => {
|
||||
const runAutoUpdate = createAutoUpdateSuccessMock();
|
||||
|
||||
await runExtendedStableUpdateCheck({ isNixMode: true, runAutoUpdate });
|
||||
|
||||
expect(resolveOpenClawPackageRoot).not.toHaveBeenCalled();
|
||||
expect(checkUpdateStatus).not.toHaveBeenCalled();
|
||||
expect(resolveNpmChannelTag).not.toHaveBeenCalled();
|
||||
expect(runAutoUpdate).not.toHaveBeenCalled();
|
||||
expect(readPersistedUpdateCheckState()).toBeNull();
|
||||
});
|
||||
|
||||
it("defers stable auto-update until rollout window is due", async () => {
|
||||
|
|
@ -765,17 +1145,44 @@ describe("update-startup", () => {
|
|||
stop();
|
||||
});
|
||||
|
||||
it("does not schedule recurring checks for extended-stable", async () => {
|
||||
it("schedules an initial and recurring 24-hour extended-stable hint check with cleanup", async () => {
|
||||
mockPackageUpdateStatus("extended-stable", "2.0.0");
|
||||
const previousNodeEnv = process.env.NODE_ENV;
|
||||
process.env.NODE_ENV = "production";
|
||||
const stop = scheduleGatewayUpdateCheck({
|
||||
cfg: { update: { channel: "extended-stable" } },
|
||||
log: { info: vi.fn() },
|
||||
isNixMode: false,
|
||||
});
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
try {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(resolveNpmChannelTag).toHaveBeenCalledTimes(1);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(24 * 60 * 60 * 1000);
|
||||
expect(resolveNpmChannelTag).toHaveBeenCalledTimes(2);
|
||||
|
||||
stop();
|
||||
await vi.advanceTimersByTimeAsync(24 * 60 * 60 * 1000);
|
||||
expect(resolveNpmChannelTag).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
stop();
|
||||
process.env.NODE_ENV = previousNodeEnv;
|
||||
}
|
||||
});
|
||||
|
||||
it("does not schedule extended-stable polling when checkOnStart is false", async () => {
|
||||
const stop = scheduleGatewayUpdateCheck({
|
||||
cfg: { update: { channel: "extended-stable", checkOnStart: false } },
|
||||
log: { info: vi.fn() },
|
||||
isNixMode: false,
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(48 * 60 * 60 * 1000);
|
||||
|
||||
expect(resolveOpenClawPackageRoot).not.toHaveBeenCalled();
|
||||
expect(checkUpdateStatus).not.toHaveBeenCalled();
|
||||
expect(resolveNpmChannelTag).not.toHaveBeenCalled();
|
||||
stop();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,7 +30,12 @@ import {
|
|||
scheduleGatewaySigusr1Restart,
|
||||
} from "./restart.js";
|
||||
import { detectRespawnSupervisor, type RespawnSupervisor } from "./supervisor-markers.js";
|
||||
import { normalizeUpdateChannel, DEFAULT_PACKAGE_CHANNEL } from "./update-channels.js";
|
||||
import {
|
||||
channelToNpmTag,
|
||||
normalizeUpdateChannel,
|
||||
DEFAULT_PACKAGE_CHANNEL,
|
||||
type UpdateChannel,
|
||||
} from "./update-channels.js";
|
||||
import { compareSemverStrings, resolveNpmChannelTag, checkUpdateStatus } from "./update-check.js";
|
||||
import { CONTROL_PLANE_UPDATE_HANDOFF_STARTED_REASON } from "./update-control-plane-sentinel.js";
|
||||
import { startManagedServiceUpdateHandoff } from "./update-managed-service-handoff.js";
|
||||
|
|
@ -233,23 +238,51 @@ function setUpdateAvailableCache(params: {
|
|||
params.onUpdateAvailableChange?.(params.next);
|
||||
}
|
||||
|
||||
function resolvePersistedUpdateAvailable(state: UpdateCheckState): UpdateAvailable | null {
|
||||
function isPersistedAvailabilityForChannel(params: {
|
||||
state: UpdateCheckState;
|
||||
channel: UpdateChannel;
|
||||
}): boolean {
|
||||
const tag = params.state.lastAvailableTag?.trim();
|
||||
if (params.channel === "stable") {
|
||||
return !tag || tag === "latest";
|
||||
}
|
||||
if (params.channel === "beta") {
|
||||
return tag === "beta" || tag === "latest";
|
||||
}
|
||||
return tag === params.channel;
|
||||
}
|
||||
|
||||
function resolvePersistedUpdateAvailable(
|
||||
state: UpdateCheckState,
|
||||
channel: UpdateChannel,
|
||||
): UpdateAvailable | null {
|
||||
const latestVersion = state.lastAvailableVersion?.trim();
|
||||
if (!latestVersion) {
|
||||
if (!latestVersion || !isPersistedAvailabilityForChannel({ state, channel })) {
|
||||
return null;
|
||||
}
|
||||
const cmp = compareSemverStrings(VERSION, latestVersion);
|
||||
if (cmp == null || cmp >= 0) {
|
||||
return null;
|
||||
}
|
||||
const channel = state.lastAvailableTag?.trim() || DEFAULT_PACKAGE_CHANNEL;
|
||||
const persistedTag = state.lastAvailableTag?.trim() || channelToNpmTag(channel);
|
||||
return {
|
||||
currentVersion: VERSION,
|
||||
latestVersion,
|
||||
channel,
|
||||
channel: persistedTag,
|
||||
};
|
||||
}
|
||||
|
||||
function clearPersistedAvailabilityForChannel(
|
||||
nextState: UpdateCheckState,
|
||||
channel: UpdateChannel,
|
||||
): void {
|
||||
if (!isPersistedAvailabilityForChannel({ state: nextState, channel })) {
|
||||
return;
|
||||
}
|
||||
delete nextState.lastAvailableVersion;
|
||||
delete nextState.lastAvailableTag;
|
||||
}
|
||||
|
||||
function resolveStableJitterMs(params: {
|
||||
installId: string;
|
||||
version: string;
|
||||
|
|
@ -466,6 +499,21 @@ function clearAutoState(nextState: UpdateCheckState): void {
|
|||
delete nextState.autoFirstSeenAt;
|
||||
}
|
||||
|
||||
async function resolveStartupInstallStatus() {
|
||||
const root = await resolveOpenClawPackageRoot({
|
||||
moduleUrl: import.meta.url,
|
||||
argv1: process.argv[1],
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
const status = await checkUpdateStatus({
|
||||
root,
|
||||
timeoutMs: 2500,
|
||||
fetchGit: false,
|
||||
includeRegistry: false,
|
||||
});
|
||||
return { root, status };
|
||||
}
|
||||
|
||||
export async function runGatewayUpdateCheck(params: {
|
||||
cfg: OpenClawConfig;
|
||||
log: { info: (msg: string, meta?: Record<string, unknown>) => void };
|
||||
|
|
@ -487,28 +535,45 @@ export async function runGatewayUpdateCheck(params: {
|
|||
}
|
||||
const configuredChannel =
|
||||
normalizeUpdateChannel(params.cfg.update?.channel) ?? DEFAULT_PACKAGE_CHANNEL;
|
||||
if (configuredChannel === "extended-stable") {
|
||||
setUpdateAvailableCache({
|
||||
next: null,
|
||||
onUpdateAvailableChange: params.onUpdateAvailableChange,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const auto = resolveAutoUpdatePolicy(params.cfg);
|
||||
const autoDisabledByEnv = isTruthyEnvValue(process.env.OPENCLAW_NO_AUTO_UPDATE);
|
||||
const shouldRunAutoUpdate = auto.enabled && !autoDisabledByEnv;
|
||||
const isAutoUpdateChannel = configuredChannel === "stable" || configuredChannel === "beta";
|
||||
const shouldRunAutoUpdate = isAutoUpdateChannel && auto.enabled && !autoDisabledByEnv;
|
||||
const shouldRunUpdateHints = params.cfg.update?.checkOnStart !== false;
|
||||
if (!shouldRunUpdateHints && !shouldRunAutoUpdate) {
|
||||
if (configuredChannel === "extended-stable") {
|
||||
setUpdateAvailableCache({
|
||||
next: null,
|
||||
onUpdateAvailableChange: params.onUpdateAvailableChange,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let installStatus: Awaited<ReturnType<typeof resolveStartupInstallStatus>> | undefined;
|
||||
if (configuredChannel === "extended-stable") {
|
||||
installStatus = await resolveStartupInstallStatus();
|
||||
if (installStatus.status.installKind !== "package") {
|
||||
setUpdateAvailableCache({
|
||||
next: null,
|
||||
onUpdateAvailableChange: params.onUpdateAvailableChange,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const state = await readState();
|
||||
const rawNow = Date.now();
|
||||
const now = resolveUpdateCheckNowMs(rawNow);
|
||||
const rawNowIsValid = asDateTimestampMs(rawNow) !== undefined;
|
||||
const lastCheckedAt = state.lastCheckedAt ? Date.parse(state.lastCheckedAt) : null;
|
||||
const persistedAvailable = shouldRunUpdateHints
|
||||
? resolvePersistedUpdateAvailable(state, configuredChannel)
|
||||
: null;
|
||||
const hasExtendedStableCheckMarker = state.lastAvailableTag?.trim() === "extended-stable";
|
||||
const shouldBypassSharedThrottle =
|
||||
configuredChannel === "extended-stable" && !hasExtendedStableCheckMarker;
|
||||
if (shouldRunUpdateHints) {
|
||||
const persistedAvailable = resolvePersistedUpdateAvailable(state);
|
||||
setUpdateAvailableCache({
|
||||
next: persistedAvailable,
|
||||
onUpdateAvailableChange: params.onUpdateAvailableChange,
|
||||
|
|
@ -522,23 +587,19 @@ export async function runGatewayUpdateCheck(params: {
|
|||
const checkIntervalMs = shouldRunAutoUpdate
|
||||
? resolveCheckIntervalMs(params.cfg)
|
||||
: UPDATE_CHECK_INTERVAL_MS;
|
||||
if (rawNowIsValid && lastCheckedAt && Number.isFinite(lastCheckedAt)) {
|
||||
if (
|
||||
!shouldBypassSharedThrottle &&
|
||||
rawNowIsValid &&
|
||||
lastCheckedAt &&
|
||||
Number.isFinite(lastCheckedAt)
|
||||
) {
|
||||
if (now - lastCheckedAt < checkIntervalMs) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const root = await resolveOpenClawPackageRoot({
|
||||
moduleUrl: import.meta.url,
|
||||
argv1: process.argv[1],
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
const status = await checkUpdateStatus({
|
||||
root,
|
||||
timeoutMs: 2500,
|
||||
fetchGit: false,
|
||||
includeRegistry: false,
|
||||
});
|
||||
installStatus ??= await resolveStartupInstallStatus();
|
||||
const { root, status } = installStatus;
|
||||
|
||||
const nextState: UpdateCheckState = {
|
||||
...state,
|
||||
|
|
@ -560,6 +621,16 @@ export async function runGatewayUpdateCheck(params: {
|
|||
const resolved = await resolveNpmChannelTag({ channel, timeoutMs: 2500 });
|
||||
const tag = resolved.tag;
|
||||
if (!resolved.version) {
|
||||
if (channel === "extended-stable") {
|
||||
clearPersistedAvailabilityForChannel(nextState, channel);
|
||||
if (!nextState.lastAvailableVersion) {
|
||||
nextState.lastAvailableTag = channel;
|
||||
}
|
||||
setUpdateAvailableCache({
|
||||
next: null,
|
||||
onUpdateAvailableChange: params.onUpdateAvailableChange,
|
||||
});
|
||||
}
|
||||
await writeState(nextState);
|
||||
return;
|
||||
}
|
||||
|
|
@ -589,7 +660,7 @@ export async function runGatewayUpdateCheck(params: {
|
|||
nextState.lastNotifiedTag = tag;
|
||||
}
|
||||
|
||||
if (auto.enabled && autoDisabledByEnv) {
|
||||
if (channel !== "extended-stable" && auto.enabled && autoDisabledByEnv) {
|
||||
params.log.info("auto-update disabled by OPENCLAW_NO_AUTO_UPDATE", {
|
||||
version: resolved.version,
|
||||
tag,
|
||||
|
|
@ -673,9 +744,16 @@ export async function runGatewayUpdateCheck(params: {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
delete nextState.lastAvailableVersion;
|
||||
delete nextState.lastAvailableTag;
|
||||
clearAutoState(nextState);
|
||||
if (channel === "extended-stable") {
|
||||
clearPersistedAvailabilityForChannel(nextState, channel);
|
||||
if (!nextState.lastAvailableVersion) {
|
||||
nextState.lastAvailableTag = channel;
|
||||
}
|
||||
} else {
|
||||
delete nextState.lastAvailableVersion;
|
||||
delete nextState.lastAvailableTag;
|
||||
clearAutoState(nextState);
|
||||
}
|
||||
setUpdateAvailableCache({
|
||||
next: null,
|
||||
onUpdateAvailableChange: params.onUpdateAvailableChange,
|
||||
|
|
@ -692,7 +770,7 @@ export function scheduleGatewayUpdateCheck(params: {
|
|||
onUpdateAvailableChange?: (updateAvailable: UpdateAvailable | null) => void;
|
||||
}): () => void {
|
||||
const channel = normalizeUpdateChannel(params.cfg.update?.channel) ?? DEFAULT_PACKAGE_CHANNEL;
|
||||
if (channel === "extended-stable") {
|
||||
if (channel === "extended-stable" && params.cfg.update?.checkOnStart === false) {
|
||||
return () => {};
|
||||
}
|
||||
let stopped = false;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue