mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(scripts): avoid downgrade release upgrade baselines
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user