mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:40:43 +00:00
* Actions: grant reusable release checks actions read * Actions: use read-all for reusable release checks * CI: add native cross-OS release checks * CI: wire Discord smoke secrets for cross-OS checks * CI: fix native cross-OS installer compatibility * CI: skip empty pnpm cache saves in matrix jobs * CI: honor workflow runner override envs * CI: finish native cross-OS update checks * CI: fix native cross-OS workflow regressions * Installer: capture Windows npm stderr safely * CI: harden cross-OS release checks * CI: resolve reusable workflow harness ref * CI: stabilize cross-OS dev update lanes * CI: tighten release-check workflow semantics * CI: repoint repaired git CLI on POSIX * CI: repair native dev-update shell handoff * CI: preserve real updater semantics * CI: harden supported release-check refs * CI: harden release-check refs and fresh mode * CI: skip dev-update for immutable tag refs * CI: repair fresh installer release checks * CI: fix native release check installer lanes * CI: install release checks from candidate artifacts * CI: use Windows cmd shims in release checks * Installer: run Windows npm shim via PowerShell * CI: pin dev update verification to candidate sha * CI: pin reusable harness and published installers * CI: isolate Windows dev-update PATH validation * CI: align Windows dev-update bootstrap validation * CI: avoid Windows installer gateway flake * CI: run cross-OS release checks via TypeScript * CI: bootstrap tsx for release-check workflow * CI: fix native release-check follow-ups * CI: tighten dev-update release checks * CI: peel annotated workflow refs * CI: harden native release checks * CI: fix release-check verifier drift * CI: fix release-check workflow drift * CI: fix release-check ref resolution * CI: harden Windows release-check gateway startup * CI: fix release-check fallback validation * CI: harden cross-os release checks * CI: pin dev-update release checks to candidate SHA * CI: resolve remote dev target refs * CI: detect cloned dev-update checkouts * CI: harden Windows release-check launcher * Windows: harden task fallback and runner overrides * Release checks: preserve Windows PATH and baseline version reads * CI: add release validation live lanes * CI: expand live and e2e release coverage * CI: add branch dispatch for live and e2e checks
487 lines
17 KiB
TypeScript
487 lines
17 KiB
TypeScript
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { createServer as createNetServer } from "node:net";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
buildWindowsDevUpdateToolchainCheckScript,
|
|
buildWindowsFreshShellVersionCheckScript,
|
|
buildWindowsPathBootstrapScript,
|
|
canConnectToLoopbackPort,
|
|
buildDiscordSmokeGuildsConfig,
|
|
buildRealUpdateEnv,
|
|
isImmutableReleaseRef,
|
|
looksLikeReleaseVersionRef,
|
|
normalizeRequestedRef,
|
|
normalizeWindowsCommandShimPath,
|
|
normalizeWindowsInstalledCliPath,
|
|
parseArgs,
|
|
packageHasScript,
|
|
readInstalledVersion,
|
|
readRunnerOverrideEnv,
|
|
resolveExplicitBaselineVersion,
|
|
resolveDevUpdateVerificationRef,
|
|
resolveInstalledPrefixDirFromCliPath,
|
|
resolvePublishedInstallerUrl,
|
|
resolveRequestedSuites,
|
|
resolveRunnerMatrix,
|
|
resolveStaticFileContentType,
|
|
shouldExerciseManagedGatewayLifecycleAfterInstall,
|
|
shouldSkipInstallerDaemonHealthCheck,
|
|
shouldStopManagedGatewayBeforeManualFallback,
|
|
shouldRunMainChannelDevUpdate,
|
|
shouldUseManagedGatewayForInstallerRuntime,
|
|
shouldUseManagedGatewayService,
|
|
verifyDevUpdateStatus,
|
|
} from "../../scripts/openclaw-cross-os-release-checks.ts";
|
|
|
|
describe("scripts/openclaw-cross-os-release-checks", () => {
|
|
it("treats explicit empty-string args as values instead of boolean flags", () => {
|
|
expect(parseArgs(["--ubuntu-runner", "", "--mode", "both"])).toEqual({
|
|
"ubuntu-runner": "",
|
|
mode: "both",
|
|
});
|
|
});
|
|
|
|
it("detects release refs and keeps branch refs out of release-only logic", () => {
|
|
expect(looksLikeReleaseVersionRef("2026.4.5")).toBe(true);
|
|
expect(looksLikeReleaseVersionRef("refs/tags/v2026.4.5-beta.1")).toBe(true);
|
|
expect(looksLikeReleaseVersionRef("v2026.4.5-beta.1")).toBe(true);
|
|
expect(looksLikeReleaseVersionRef("v2026.4.7-1")).toBe(true);
|
|
expect(looksLikeReleaseVersionRef("main")).toBe(false);
|
|
expect(looksLikeReleaseVersionRef("codex/cross-os-release-checks")).toBe(false);
|
|
});
|
|
|
|
it("normalizes full Git refs before suite and update decisions", () => {
|
|
expect(normalizeRequestedRef(" refs/heads/main ")).toBe("main");
|
|
expect(normalizeRequestedRef("refs/tags/v2026.4.14")).toBe("v2026.4.14");
|
|
expect(isImmutableReleaseRef("refs/tags/test-tag")).toBe(true);
|
|
expect(resolveRequestedSuites("both", "refs/tags/v2026.4.14")).toEqual([
|
|
"packaged-fresh",
|
|
"installer-fresh",
|
|
"packaged-upgrade",
|
|
]);
|
|
expect(resolveRequestedSuites("both", "refs/tags/test-tag")).toEqual([
|
|
"packaged-fresh",
|
|
"installer-fresh",
|
|
"packaged-upgrade",
|
|
]);
|
|
expect(shouldRunMainChannelDevUpdate("refs/heads/main")).toBe(true);
|
|
expect(shouldRunMainChannelDevUpdate("refs/tags/main")).toBe(false);
|
|
});
|
|
|
|
it("skips the dev-update suite for immutable release refs", () => {
|
|
expect(resolveRequestedSuites("both", "v2026.4.5")).toEqual([
|
|
"packaged-fresh",
|
|
"installer-fresh",
|
|
"packaged-upgrade",
|
|
]);
|
|
});
|
|
|
|
it("skips dev-update for non-main branch validation refs", () => {
|
|
expect(resolveRequestedSuites("both", "codex/cross-os-release-checks")).toEqual([
|
|
"packaged-fresh",
|
|
"installer-fresh",
|
|
"packaged-upgrade",
|
|
]);
|
|
});
|
|
|
|
it("keeps dev-update enabled for main validation refs", () => {
|
|
expect(resolveRequestedSuites("both", "main")).toEqual([
|
|
"packaged-fresh",
|
|
"installer-fresh",
|
|
"packaged-upgrade",
|
|
"dev-update",
|
|
]);
|
|
});
|
|
|
|
it("skips dev-update for pinned commit refs", () => {
|
|
expect(resolveRequestedSuites("both", "08753a1d793c040b101c8a26c43445dbbab14995")).toEqual([
|
|
"packaged-fresh",
|
|
"installer-fresh",
|
|
"packaged-upgrade",
|
|
]);
|
|
});
|
|
|
|
it("builds a suite-aware runner matrix with the beefy Windows default", () => {
|
|
const matrix = resolveRunnerMatrix({
|
|
mode: "both",
|
|
ref: "main",
|
|
ubuntuRunner: "",
|
|
windowsRunner: "",
|
|
macosRunner: "",
|
|
varUbuntuRunner: "",
|
|
varWindowsRunner: "",
|
|
varMacosRunner: "",
|
|
});
|
|
|
|
expect(matrix.include).toHaveLength(12);
|
|
expect(matrix.include).toContainEqual(
|
|
expect.objectContaining({
|
|
os_id: "windows",
|
|
runner: "blacksmith-32vcpu-windows-2025",
|
|
suite: "dev-update",
|
|
lane: "upgrade",
|
|
}),
|
|
);
|
|
expect(matrix.include).toContainEqual(
|
|
expect.objectContaining({
|
|
os_id: "ubuntu",
|
|
suite: "installer-fresh",
|
|
lane: "fresh",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("can rebuild the Windows PATH with or without current-process entries", () => {
|
|
expect(buildWindowsPathBootstrapScript()).toContain("@($userPath, $machinePath, $env:Path)");
|
|
const persistedOnlyScript = buildWindowsPathBootstrapScript({
|
|
includeCurrentProcessPath: false,
|
|
});
|
|
expect(persistedOnlyScript).toContain("@($userPath, $machinePath)");
|
|
expect(persistedOnlyScript).not.toContain("@($userPath, $machinePath, $env:Path)");
|
|
});
|
|
|
|
it("prefers the freshly installed Windows CLI under npm's prefix before PATH lookup", () => {
|
|
const script = buildWindowsFreshShellVersionCheckScript({
|
|
expectedNeedle: "2026.4.14",
|
|
});
|
|
expect(script).toContain(buildWindowsPathBootstrapScript());
|
|
expect(script).not.toContain(
|
|
buildWindowsPathBootstrapScript({ includeCurrentProcessPath: false }),
|
|
);
|
|
expect(script).toContain("Get-Command npm.cmd -ErrorAction SilentlyContinue");
|
|
expect(script).toContain('$env:Path = "$npmPrefix;$env:Path"');
|
|
expect(script).toContain("(Join-Path $npmPrefix 'openclaw.cmd')");
|
|
expect(script).toContain("$cmd = Get-Command openclaw -ErrorAction Stop");
|
|
});
|
|
|
|
it("keeps Windows dev-update toolchain checks compatible with setup-node PATH shims", () => {
|
|
const script = buildWindowsDevUpdateToolchainCheckScript();
|
|
expect(script).toContain(buildWindowsPathBootstrapScript());
|
|
expect(script).not.toContain(
|
|
buildWindowsPathBootstrapScript({ includeCurrentProcessPath: false }),
|
|
);
|
|
expect(script).toContain("$pnpmPath = Resolve-CommandPath 'pnpm'");
|
|
expect(script).toContain("$corepackPath = Resolve-CommandPath 'corepack'");
|
|
expect(script).toContain("$npmPath = Resolve-CommandPath 'npm'");
|
|
});
|
|
|
|
it("prefers workflow-injected runner override env names over legacy ones", () => {
|
|
expect(
|
|
readRunnerOverrideEnv({
|
|
VAR_UBUNTU_RUNNER: "workflow-linux",
|
|
VAR_WINDOWS_RUNNER: "workflow-windows",
|
|
VAR_MACOS_RUNNER: "workflow-macos",
|
|
OPENCLAW_RELEASE_CHECKS_UBUNTU_RUNNER: "legacy-linux",
|
|
OPENCLAW_RELEASE_CHECKS_WINDOWS_RUNNER: "legacy-windows",
|
|
OPENCLAW_RELEASE_CHECKS_MACOS_RUNNER: "legacy-macos",
|
|
}),
|
|
).toEqual({
|
|
varUbuntuRunner: "workflow-linux",
|
|
varWindowsRunner: "workflow-windows",
|
|
varMacosRunner: "workflow-macos",
|
|
});
|
|
});
|
|
|
|
it("falls back to legacy runner override env names when workflow vars are blank", () => {
|
|
expect(
|
|
readRunnerOverrideEnv({
|
|
VAR_UBUNTU_RUNNER: "",
|
|
VAR_WINDOWS_RUNNER: " ",
|
|
VAR_MACOS_RUNNER: "",
|
|
OPENCLAW_RELEASE_CHECKS_UBUNTU_RUNNER: "legacy-linux",
|
|
OPENCLAW_RELEASE_CHECKS_WINDOWS_RUNNER: "legacy-windows",
|
|
OPENCLAW_RELEASE_CHECKS_MACOS_RUNNER: "legacy-macos",
|
|
}),
|
|
).toEqual({
|
|
varUbuntuRunner: "legacy-linux",
|
|
varWindowsRunner: "legacy-windows",
|
|
varMacosRunner: "legacy-macos",
|
|
});
|
|
});
|
|
|
|
it("serves installer scripts as UTF-8 text and package payloads as binary", () => {
|
|
expect(resolveStaticFileContentType("scripts/install.sh")).toBe("text/plain; charset=utf-8");
|
|
expect(resolveStaticFileContentType("scripts/install.ps1")).toBe("text/plain; charset=utf-8");
|
|
expect(resolveStaticFileContentType("openclaw-2026.4.14.tgz")).toBe("application/octet-stream");
|
|
});
|
|
|
|
it("uses the published installer URLs for native installer lanes", () => {
|
|
expect(resolvePublishedInstallerUrl("darwin")).toBe("https://openclaw.ai/install.sh");
|
|
expect(resolvePublishedInstallerUrl("linux")).toBe("https://openclaw.ai/install.sh");
|
|
expect(resolvePublishedInstallerUrl("win32")).toBe("https://openclaw.ai/install.ps1");
|
|
});
|
|
|
|
it("uses managed gateway services only on native Windows runners", () => {
|
|
expect(shouldUseManagedGatewayService("win32")).toBe(true);
|
|
expect(shouldUseManagedGatewayService("darwin")).toBe(false);
|
|
expect(shouldUseManagedGatewayService("linux")).toBe(false);
|
|
});
|
|
|
|
it("keeps the Windows installer runtime on the manual gateway after managed lifecycle checks", () => {
|
|
expect(shouldExerciseManagedGatewayLifecycleAfterInstall("win32")).toBe(true);
|
|
expect(shouldUseManagedGatewayForInstallerRuntime("win32")).toBe(false);
|
|
expect(shouldExerciseManagedGatewayLifecycleAfterInstall("darwin")).toBe(false);
|
|
expect(shouldUseManagedGatewayForInstallerRuntime("darwin")).toBe(false);
|
|
});
|
|
|
|
it("stops the managed gateway before the manual fallback only on Windows", () => {
|
|
expect(shouldStopManagedGatewayBeforeManualFallback("win32")).toBe(true);
|
|
expect(shouldStopManagedGatewayBeforeManualFallback("darwin")).toBe(false);
|
|
expect(shouldStopManagedGatewayBeforeManualFallback("linux")).toBe(false);
|
|
});
|
|
|
|
it("skips daemon health during installed onboarding only on native Windows", () => {
|
|
expect(shouldSkipInstallerDaemonHealthCheck("win32")).toBe(true);
|
|
expect(shouldSkipInstallerDaemonHealthCheck("darwin")).toBe(false);
|
|
expect(shouldSkipInstallerDaemonHealthCheck("linux")).toBe(false);
|
|
});
|
|
|
|
it("normalizes Windows installed CLI paths to the cmd shim", () => {
|
|
expect(
|
|
normalizeWindowsInstalledCliPath(
|
|
String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.ps1`,
|
|
),
|
|
).toBe(String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.cmd`);
|
|
expect(
|
|
normalizeWindowsInstalledCliPath(
|
|
String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.cmd`,
|
|
),
|
|
).toBe(String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.cmd`);
|
|
});
|
|
|
|
it("normalizes generic Windows PowerShell shims to cmd shims", () => {
|
|
expect(normalizeWindowsCommandShimPath(String.raw`C:\Program Files\nodejs\pnpm.ps1`)).toBe(
|
|
String.raw`C:\Program Files\nodejs\pnpm.cmd`,
|
|
);
|
|
expect(normalizeWindowsCommandShimPath(String.raw`C:\Program Files\nodejs\corepack.ps1`)).toBe(
|
|
String.raw`C:\Program Files\nodejs\corepack.cmd`,
|
|
);
|
|
expect(normalizeWindowsCommandShimPath(String.raw`C:\Program Files\nodejs\node.exe`)).toBe(
|
|
String.raw`C:\Program Files\nodejs\node.exe`,
|
|
);
|
|
});
|
|
|
|
it("derives the installed prefix from resolved CLI paths", () => {
|
|
expect(
|
|
resolveInstalledPrefixDirFromCliPath(
|
|
String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.ps1`,
|
|
"win32",
|
|
),
|
|
).toBe(String.raw`C:\Users\runner\AppData\Roaming\npm`);
|
|
expect(
|
|
resolveInstalledPrefixDirFromCliPath("/Users/runner/.npm-global/bin/openclaw", "darwin"),
|
|
).toBe("/Users/runner/.npm-global");
|
|
});
|
|
|
|
it("detects whether a managed gateway listener is still reachable on loopback", async () => {
|
|
const server = createNetServer();
|
|
await new Promise((resolvePromise) => {
|
|
server.listen(0, "127.0.0.1", resolvePromise);
|
|
});
|
|
const address = server.address();
|
|
const port = typeof address === "object" && address ? address.port : 0;
|
|
expect(await canConnectToLoopbackPort(port)).toBe(true);
|
|
await new Promise((resolvePromise) => {
|
|
server.close(resolvePromise);
|
|
});
|
|
expect(await canConnectToLoopbackPort(port)).toBe(false);
|
|
});
|
|
|
|
it("writes Discord smoke config using the strict guild channel schema", () => {
|
|
expect(buildDiscordSmokeGuildsConfig("guild-123", "channel-456")).toEqual({
|
|
"guild-123": {
|
|
channels: {
|
|
"channel-456": {
|
|
enabled: true,
|
|
requireMention: false,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("keeps the dev-update lane for main only", () => {
|
|
expect(shouldRunMainChannelDevUpdate("main")).toBe(true);
|
|
expect(shouldRunMainChannelDevUpdate("08753a1d793c040b101c8a26c43445dbbab14995")).toBe(false);
|
|
expect(shouldRunMainChannelDevUpdate(" codex/cross-os-release-checks-full-native-e2e ")).toBe(
|
|
false,
|
|
);
|
|
expect(shouldRunMainChannelDevUpdate("v2026.4.14")).toBe(false);
|
|
});
|
|
|
|
it("verifies main dev updates against the prepared source sha when available", () => {
|
|
expect(resolveDevUpdateVerificationRef("main")).toBe("main");
|
|
expect(
|
|
resolveDevUpdateVerificationRef("main", "08753a1d793c040b101c8a26c43445dbbab14995"),
|
|
).toBe("08753a1d793c040b101c8a26c43445dbbab14995");
|
|
expect(
|
|
resolveDevUpdateVerificationRef(
|
|
"refs/heads/main",
|
|
"08753a1d793c040b101c8a26c43445dbbab14995",
|
|
),
|
|
).toBe("08753a1d793c040b101c8a26c43445dbbab14995");
|
|
expect(resolveDevUpdateVerificationRef("codex/cross-os-release-checks-full-native-e2e")).toBe(
|
|
"codex/cross-os-release-checks-full-native-e2e",
|
|
);
|
|
});
|
|
|
|
it("drops the bundled plugin postinstall disable flag for real updater calls", () => {
|
|
expect(
|
|
buildRealUpdateEnv({
|
|
FOO: "bar",
|
|
OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL: "1",
|
|
}),
|
|
).toEqual({
|
|
FOO: "bar",
|
|
});
|
|
});
|
|
|
|
it("only treats pinned baseline specs as exact installer version assertions", () => {
|
|
expect(resolveExplicitBaselineVersion("")).toBe("");
|
|
expect(resolveExplicitBaselineVersion("openclaw@latest")).toBe("");
|
|
expect(resolveExplicitBaselineVersion("openclaw@2026.4.10")).toBe("2026.4.10");
|
|
expect(resolveExplicitBaselineVersion("2026.4.10")).toBe("2026.4.10");
|
|
});
|
|
|
|
it("reads an installed baseline version without requiring build metadata", () => {
|
|
const prefixDir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-installed-version-"));
|
|
try {
|
|
const packageRoot =
|
|
process.platform === "win32"
|
|
? join(prefixDir, "node_modules", "openclaw")
|
|
: join(prefixDir, "lib", "node_modules", "openclaw");
|
|
mkdirSync(packageRoot, { recursive: true });
|
|
writeFileSync(
|
|
join(packageRoot, "package.json"),
|
|
JSON.stringify({
|
|
name: "openclaw",
|
|
version: "2026.4.10",
|
|
}),
|
|
"utf8",
|
|
);
|
|
|
|
expect(readInstalledVersion(prefixDir)).toBe("2026.4.10");
|
|
} finally {
|
|
rmSync(prefixDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("treats missing package scripts as optional in older refs", () => {
|
|
const packageRoot = mkdtempSync(join(tmpdir(), "openclaw-cross-os-scripts-"));
|
|
try {
|
|
writeFileSync(
|
|
join(packageRoot, "package.json"),
|
|
JSON.stringify({
|
|
name: "openclaw",
|
|
scripts: {
|
|
build: "pnpm build",
|
|
},
|
|
}),
|
|
"utf8",
|
|
);
|
|
|
|
expect(packageHasScript(packageRoot, "build")).toBe(true);
|
|
expect(packageHasScript(packageRoot, "ui:build")).toBe(false);
|
|
} finally {
|
|
rmSync(packageRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("accepts a git main dev-channel update status payload", () => {
|
|
expect(() =>
|
|
verifyDevUpdateStatus(
|
|
JSON.stringify({
|
|
update: {
|
|
installKind: "git",
|
|
git: {
|
|
branch: "main",
|
|
},
|
|
},
|
|
channel: {
|
|
value: "dev",
|
|
},
|
|
}),
|
|
),
|
|
).not.toThrow();
|
|
});
|
|
|
|
it("accepts a git dev-channel payload for a requested non-main branch", () => {
|
|
expect(() =>
|
|
verifyDevUpdateStatus(
|
|
JSON.stringify({
|
|
update: {
|
|
installKind: "git",
|
|
git: {
|
|
branch: "codex/cross-os-release-checks-full-native-e2e",
|
|
sha: "08753a1d793c040b101c8a26c43445dbbab14995",
|
|
},
|
|
},
|
|
channel: {
|
|
value: "dev",
|
|
},
|
|
}),
|
|
{ ref: "codex/cross-os-release-checks-full-native-e2e" },
|
|
),
|
|
).not.toThrow();
|
|
});
|
|
|
|
it("accepts a git dev-channel payload pinned to a prepared source sha", () => {
|
|
expect(() =>
|
|
verifyDevUpdateStatus(
|
|
JSON.stringify({
|
|
update: {
|
|
installKind: "git",
|
|
git: {
|
|
branch: "main",
|
|
sha: "08753a1d793c040b101c8a26c43445dbbab14995",
|
|
},
|
|
},
|
|
channel: {
|
|
value: "dev",
|
|
},
|
|
}),
|
|
{ ref: "08753a1d793c040b101c8a26c43445dbbab14995" },
|
|
),
|
|
).not.toThrow();
|
|
});
|
|
|
|
it("accepts uppercase requested commit shas when update status reports lowercase", () => {
|
|
expect(() =>
|
|
verifyDevUpdateStatus(
|
|
JSON.stringify({
|
|
update: {
|
|
installKind: "git",
|
|
git: {
|
|
sha: "08753a1d793c040b101c8a26c43445dbbab14995",
|
|
},
|
|
},
|
|
channel: {
|
|
value: "dev",
|
|
},
|
|
}),
|
|
{ ref: "08753A1D793C040B101C8A26C43445DBBAB14995" },
|
|
),
|
|
).not.toThrow();
|
|
});
|
|
|
|
it("rejects update status payloads that are not on dev/main git", () => {
|
|
expect(() =>
|
|
verifyDevUpdateStatus(
|
|
JSON.stringify({
|
|
update: {
|
|
installKind: "package",
|
|
git: {
|
|
branch: "release",
|
|
},
|
|
},
|
|
channel: {
|
|
value: "stable",
|
|
},
|
|
}),
|
|
),
|
|
).toThrow("git install");
|
|
});
|
|
});
|