fix(scripts): avoid downgrade release upgrade baselines

This commit is contained in:
Vincent Koc
2026-06-16 09:35:16 +02:00
parent c06b7959ec
commit 1ae0eacf4b
5 changed files with 194 additions and 5 deletions
+1 -1
View File
@@ -824,7 +824,7 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or
- Release user journey smoke: `pnpm test:docker:release-user-journey` installs the packed OpenClaw tarball globally in a clean Docker home, runs onboarding, configures a mocked OpenAI provider, runs an agent turn, installs/uninstalls external plugins, configures ClickClack against a local fixture, verifies outbound/inbound messaging, restarts Gateway, and runs doctor.
- Release typed onboarding smoke: `pnpm test:docker:release-typed-onboarding` installs the packed tarball, drives `openclaw onboard` through a real TTY, configures OpenAI as an env-ref provider, verifies no raw key persistence, and runs a mocked agent turn.
- Release media/memory smoke: `pnpm test:docker:release-media-memory` installs the packed tarball, verifies image understanding from a PNG attachment, OpenAI-compatible image generation output, memory search recall, and recall survival across Gateway restart.
- Release upgrade user journey smoke: `pnpm test:docker:release-upgrade-user-journey` installs `openclaw@latest` by default, configures provider/plugin/ClickClack state on the published package, upgrades to the candidate tarball, then reruns the core agent/plugin/channel journey. Override the baseline with `OPENCLAW_RELEASE_UPGRADE_BASELINE_SPEC=openclaw@<version>`.
- Release upgrade user journey smoke: `pnpm test:docker:release-upgrade-user-journey` installs the newest published baseline older than the candidate tarball by default, configures provider/plugin/ClickClack state on the published package, upgrades to the candidate tarball, then reruns the core agent/plugin/channel journey. If no older published baseline exists, it reuses the candidate version. Override the baseline with `OPENCLAW_RELEASE_UPGRADE_BASELINE_SPEC=openclaw@<version>`.
- Release plugin marketplace smoke: `pnpm test:docker:release-plugin-marketplace` installs from a local fixture marketplace, updates the installed plugin, uninstalls it, and verifies the plugin CLI disappears with install metadata pruned.
- Skill install smoke: `pnpm test:docker:skill-install` installs the packed OpenClaw tarball globally in Docker, disables uploaded archive installs in config, resolves the current live ClawHub skill slug from search, installs it with `openclaw skills install`, and verifies the installed skill plus `.clawhub` origin/lock metadata.
- Update channel switch smoke: `pnpm test:docker:update-channel-switch` installs the packed OpenClaw tarball globally in Docker, switches from package `stable` to git `dev`, verifies the persisted channel and plugin post-update work, then switches back to package `stable` and checks update status.
@@ -40,13 +40,17 @@ CLICKCLACK_SERVER_LOG="$LOG_DIR/clickclack-server.log"
GATEWAY_LOG="$LOG_DIR/gateway.log"
MOCK_REQUEST_LOG="$scenario_tmp/openai-requests.jsonl"
CLICKCLACK_STATE="$scenario_tmp/clickclack.json"
BASELINE_SPEC="${OPENCLAW_RELEASE_UPGRADE_BASELINE_SPEC:-openclaw@latest}"
export SUCCESS_MARKER MOCK_REQUEST_LOG CLICKCLACK_STATE
candidate_version="$(
tar -xOf "${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_TGZ}" package/package.json |
node -e 'let raw = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { raw += chunk; }); process.stdin.on("end", () => { process.stdout.write(JSON.parse(raw).version); });'
)"
if [ -n "${OPENCLAW_RELEASE_UPGRADE_BASELINE_SPEC:-}" ]; then
BASELINE_SPEC="$OPENCLAW_RELEASE_UPGRADE_BASELINE_SPEC"
else
BASELINE_SPEC="$(node scripts/lib/release-upgrade-baseline.mjs --candidate-version "$candidate_version")"
fi
mock_pid=""
clickclack_pid=""
@@ -24,11 +24,17 @@ docker_e2e_build_or_reuse "$IMAGE_NAME" release-upgrade-user-journey "$ROOT_DIR/
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 release-upgrade-user-journey empty)"
run_log="$(docker_e2e_run_log release-upgrade-user-journey)"
DOCKER_ENV_ARGS=(
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64"
)
if [ -n "${OPENCLAW_RELEASE_UPGRADE_BASELINE_SPEC:-}" ]; then
DOCKER_ENV_ARGS+=(-e "OPENCLAW_RELEASE_UPGRADE_BASELINE_SPEC=$OPENCLAW_RELEASE_UPGRADE_BASELINE_SPEC")
fi
echo "Running release upgrade user journey Docker E2E..."
if ! docker_e2e_run_with_harness \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
-e "OPENCLAW_RELEASE_UPGRADE_BASELINE_SPEC=${OPENCLAW_RELEASE_UPGRADE_BASELINE_SPEC:-openclaw@latest}" \
"${DOCKER_ENV_ARGS[@]}" \
"${DOCKER_E2E_PACKAGE_ARGS[@]}" \
-i "$IMAGE_NAME" bash scripts/e2e/lib/release-upgrade-user-journey/scenario.sh >"$run_log" 2>&1; then
docker_e2e_print_log "$run_log"
+115
View File
@@ -0,0 +1,115 @@
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { parseReleaseVersion } from "./npm-publish-plan.mjs";
function parseVersion(version) {
return parseReleaseVersion(String(version ?? "").trim()) ?? undefined;
}
export function compareOpenClawVersions(leftVersion, rightVersion) {
const left = parseVersion(leftVersion);
const right = parseVersion(rightVersion);
if (!left || !right) {
throw new Error(`cannot compare OpenClaw versions: ${leftVersion} ${rightVersion}`);
}
for (const key of ["year", "month", "patch"]) {
const delta = left[key] - right[key];
if (delta !== 0) {
return delta;
}
}
const channelRank = { alpha: 0, beta: 1, stable: 2 };
const channelDelta = channelRank[left.channel] - channelRank[right.channel];
if (channelDelta !== 0) {
return channelDelta;
}
if (left.channel === "alpha") {
return (left.alphaNumber ?? 0) - (right.alphaNumber ?? 0);
}
if (left.channel === "beta") {
return (left.betaNumber ?? 0) - (right.betaNumber ?? 0);
}
return (left.correctionNumber ?? 0) - (right.correctionNumber ?? 0);
}
function normalizePublishedVersions(publishedVersions) {
return [...new Set(publishedVersions.map((version) => String(version).trim()).filter(Boolean))]
.filter((version) => parseVersion(version))
.toSorted((left, right) => compareOpenClawVersions(right, left));
}
export function resolveDefaultReleaseUpgradeBaseline(candidateVersion, publishedVersions) {
const candidate = parseVersion(candidateVersion);
if (!candidate) {
throw new Error(`invalid candidate OpenClaw version: ${candidateVersion}`);
}
const versions = normalizePublishedVersions(publishedVersions);
const older = versions.find((version) => compareOpenClawVersions(version, candidate.version) < 0);
if (older) {
return `openclaw@${older}`;
}
const same = versions.find(
(version) => compareOpenClawVersions(version, candidate.version) === 0,
);
if (same) {
return `openclaw@${same}`;
}
throw new Error(`no published OpenClaw baseline is <= candidate ${candidate.version}`);
}
function parseArgs(argv) {
const args = new Map();
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (!arg.startsWith("--")) {
throw new Error(`unexpected argument: ${arg}`);
}
const key = arg.slice(2);
const value = argv[index + 1];
if (value === undefined || value.startsWith("--")) {
throw new Error(`missing value for --${key}`);
}
args.set(key, value);
index += 1;
}
return args;
}
function readPublishedVersions(args) {
const versionsJson = args.get("versions-json");
if (versionsJson) {
const parsed = JSON.parse(readFileSync(versionsJson, "utf8"));
if (!Array.isArray(parsed)) {
throw new Error(`npm versions list must be a JSON array: ${versionsJson}`);
}
return parsed;
}
const raw = execFileSync("npm", ["view", "openclaw", "versions", "--json", "--silent"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "inherit"],
});
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) {
throw new Error("npm returned a non-array openclaw versions payload");
}
return parsed;
}
const isMain = process.argv[1] ? fileURLToPath(import.meta.url) === process.argv[1] : false;
if (isMain) {
const args = parseArgs(process.argv.slice(2));
const candidateVersion = args.get("candidate-version");
if (!candidateVersion) {
throw new Error("--candidate-version is required");
}
const baseline = resolveDefaultReleaseUpgradeBaseline(
candidateVersion,
readPublishedVersions(args),
);
process.stdout.write(`${baseline}\n`);
}
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import {
compareOpenClawVersions,
resolveDefaultReleaseUpgradeBaseline,
} from "../../scripts/lib/release-upgrade-baseline.mjs";
describe("release upgrade baseline resolver", () => {
it("prefers the newest published baseline older than the candidate across channels", () => {
expect(
resolveDefaultReleaseUpgradeBaseline("2026.6.2", [
"2026.5.30",
"2026.6.2",
"2026.6.6",
"2026.6.2-beta.1",
"2026.6.1",
]),
).toBe("openclaw@2026.6.2-beta.1");
expect(resolveDefaultReleaseUpgradeBaseline("2026.6.7", ["2026.6.6", "2026.6.7-beta.2"])).toBe(
"openclaw@2026.6.7-beta.2",
);
});
it("uses prerelease baselines only when no stable baseline can satisfy the candidate", () => {
expect(
resolveDefaultReleaseUpgradeBaseline("2026.6.2-beta.2", ["2026.6.2", "2026.6.2-beta.1"]),
).toBe("openclaw@2026.6.2-beta.1");
});
it("prefers older prerelease baselines over same-version stable baselines", () => {
expect(resolveDefaultReleaseUpgradeBaseline("2026.6.2", ["2026.6.2", "2026.6.1-beta.1"])).toBe(
"openclaw@2026.6.1-beta.1",
);
});
it("treats numeric correction releases as stable baselines", () => {
expect(resolveDefaultReleaseUpgradeBaseline("2026.5.3-1", ["2026.5.2", "2026.5.3"])).toBe(
"openclaw@2026.5.3",
);
expect(
resolveDefaultReleaseUpgradeBaseline("2026.5.3-2", ["2026.5.2", "2026.5.3", "2026.5.3-1"]),
).toBe("openclaw@2026.5.3-1");
});
it("falls back to the candidate version when no older baseline exists", () => {
expect(resolveDefaultReleaseUpgradeBaseline("2026.6.2", ["2026.6.2", "2026.6.6"])).toBe(
"openclaw@2026.6.2",
);
});
it("does not pick a newer stable release for a prerelease candidate", () => {
expect(
resolveDefaultReleaseUpgradeBaseline("2026.6.7-beta.1", [
"2026.6.6",
"2026.6.7",
"2026.6.7-beta.2",
]),
).toBe("openclaw@2026.6.6");
});
it("compares prerelease versions with semver ordering", () => {
expect(compareOpenClawVersions("2026.6.7-beta.2", "2026.6.7-beta.10")).toBeLessThan(0);
expect(compareOpenClawVersions("2026.6.7", "2026.6.7-beta.10")).toBeGreaterThan(0);
});
});