test: add update migration package gate

This commit is contained in:
Peter Steinberger
2026-05-02 00:34:28 +01:00
parent 3f4ca7c53b
commit 682e05532d
16 changed files with 349 additions and 14 deletions

View File

@@ -6,6 +6,7 @@ const SCENARIOS = new Set([
"base",
"feishu-channel",
"bootstrap-persona",
"plugin-deps-cleanup",
"tilde-log-path",
"versioned-runtime-deps",
]);

View File

@@ -53,6 +53,7 @@ BASELINE_INSTALL_LOG="$ARTIFACT_ROOT/baseline-install.log"
UPDATE_JSON="$ARTIFACT_ROOT/update.json"
UPDATE_ERR="$ARTIFACT_ROOT/update.err"
DOCTOR_LOG="$ARTIFACT_ROOT/doctor.log"
BASELINE_DOCTOR_LOG="$ARTIFACT_ROOT/baseline-doctor.log"
GATEWAY_LOG="$ARTIFACT_ROOT/gateway.log"
HEALTHZ_JSON="$ARTIFACT_ROOT/healthz.json"
READYZ_JSON="$ARTIFACT_ROOT/readyz.json"
@@ -260,6 +261,123 @@ legacy_runtime_deps_symlink_source() {
"$plugin"
}
plugin_deps_cleanup_enabled() {
[ "$SCENARIO" = "plugin-deps-cleanup" ]
}
plugin_deps_cleanup_plugins() {
printf '%s\n' "${OPENCLAW_UPGRADE_SURVIVOR_PLUGIN_DEPS_CLEANUP_PLUGINS:-discord telegram}"
}
legacy_plugin_dependency_probe_paths() {
local plugin="$1"
local plugin_dir
plugin_dir="$(package_root)/dist/extensions/$plugin"
printf '%s\n' \
"$plugin_dir/node_modules" \
"$plugin_dir/.openclaw-runtime-deps.json" \
"$plugin_dir/.openclaw-runtime-deps-stamp.json" \
"$plugin_dir/.openclaw-runtime-deps-copy-upgrade-survivor" \
"$plugin_dir/.openclaw-install-stage-upgrade-survivor" \
"$plugin_dir/.openclaw-pnpm-store" \
"$(package_root)/.local/bundled-plugin-runtime-deps/$plugin-upgrade-survivor" \
"$OPENCLAW_STATE_DIR/.local/bundled-plugin-runtime-deps/$plugin-upgrade-survivor" \
"$OPENCLAW_STATE_DIR/plugin-runtime-deps/$plugin-upgrade-survivor"
}
install_baseline_plugin_dependencies() {
plugin_deps_cleanup_enabled || return 0
echo "Running baseline doctor to install configured plugin dependencies before update."
if ! openclaw doctor --fix --non-interactive >"$BASELINE_DOCTOR_LOG" 2>&1; then
echo "baseline openclaw doctor failed while preparing plugin dependency cleanup scenario" >&2
cat "$BASELINE_DOCTOR_LOG" >&2 || true
return 1
fi
}
seed_legacy_plugin_dependency_debris() {
plugin_deps_cleanup_enabled || return 0
local found=0
local plugin
for plugin in $(plugin_deps_cleanup_plugins); do
local plugin_dir
plugin_dir="$(package_root)/dist/extensions/$plugin"
if [ ! -d "$plugin_dir" ]; then
continue
fi
found=1
mkdir -p \
"$plugin_dir/node_modules/openclaw-upgrade-survivor-dep" \
"$plugin_dir/.openclaw-runtime-deps-copy-upgrade-survivor/node_modules/openclaw-upgrade-survivor-dep" \
"$plugin_dir/.openclaw-install-stage-upgrade-survivor" \
"$plugin_dir/.openclaw-pnpm-store" \
"$(package_root)/.local/bundled-plugin-runtime-deps/$plugin-upgrade-survivor/node_modules/openclaw-upgrade-survivor-dep" \
"$OPENCLAW_STATE_DIR/.local/bundled-plugin-runtime-deps/$plugin-upgrade-survivor/node_modules/openclaw-upgrade-survivor-dep" \
"$OPENCLAW_STATE_DIR/plugin-runtime-deps/$plugin-upgrade-survivor/node_modules/openclaw-upgrade-survivor-dep"
printf '{"name":"openclaw-upgrade-survivor-dep","version":"0.0.0"}\n' \
>"$plugin_dir/node_modules/openclaw-upgrade-survivor-dep/package.json"
printf '{"plugin":"%s","scenario":"plugin-deps-cleanup"}\n' "$plugin" \
>"$plugin_dir/.openclaw-runtime-deps.json"
printf '{"plugin":"%s","scenario":"plugin-deps-cleanup","stale":true}\n' "$plugin" \
>"$plugin_dir/.openclaw-runtime-deps-stamp.json"
printf '{"name":"openclaw-upgrade-survivor-dep","version":"0.0.0"}\n' \
>"$plugin_dir/.openclaw-runtime-deps-copy-upgrade-survivor/node_modules/openclaw-upgrade-survivor-dep/package.json"
printf '{"name":"openclaw-upgrade-survivor-dep","version":"0.0.0"}\n' \
>"$(package_root)/.local/bundled-plugin-runtime-deps/$plugin-upgrade-survivor/node_modules/openclaw-upgrade-survivor-dep/package.json"
printf '{"name":"openclaw-upgrade-survivor-dep","version":"0.0.0"}\n' \
>"$OPENCLAW_STATE_DIR/.local/bundled-plugin-runtime-deps/$plugin-upgrade-survivor/node_modules/openclaw-upgrade-survivor-dep/package.json"
printf '{"name":"openclaw-upgrade-survivor-dep","version":"0.0.0"}\n' \
>"$OPENCLAW_STATE_DIR/plugin-runtime-deps/$plugin-upgrade-survivor/node_modules/openclaw-upgrade-survivor-dep/package.json"
echo "Seeded legacy plugin dependency debris for configured plugin: $plugin"
done
if [ "$found" -ne 1 ]; then
echo "plugin-deps-cleanup scenario could not find a packaged Discord or Telegram plugin directory" >&2
find "$(package_root)/dist" -maxdepth 3 -type d 2>/dev/null >&2 || true
return 1
fi
}
assert_legacy_plugin_dependency_debris_present() {
plugin_deps_cleanup_enabled || return 0
local found=0
local plugin
for plugin in $(plugin_deps_cleanup_plugins); do
local probe
while IFS= read -r probe; do
if [ -e "$probe" ] || [ -L "$probe" ]; then
found=1
fi
done < <(legacy_plugin_dependency_probe_paths "$plugin")
done
if [ "$found" -ne 1 ]; then
echo "plugin-deps-cleanup scenario did not create legacy plugin dependency debris" >&2
return 1
fi
}
assert_legacy_plugin_dependency_debris_cleaned() {
plugin_deps_cleanup_enabled || return 0
local remaining=0
local plugin
for plugin in $(plugin_deps_cleanup_plugins); do
local probe
while IFS= read -r probe; do
if [ -e "$probe" ] || [ -L "$probe" ]; then
echo "legacy plugin dependency debris survived update/doctor: $probe" >&2
remaining=1
fi
done < <(legacy_plugin_dependency_probe_paths "$plugin")
done
if [ "$remaining" -ne 0 ]; then
return 1
fi
echo "Legacy plugin dependency debris cleaned for configured plugin dependencies."
}
seed_legacy_runtime_deps_symlink() {
local plugin
plugin="$(legacy_runtime_deps_symlink_plugin)" || {
@@ -532,11 +650,16 @@ phase install-baseline install_baseline
phase seed-state seed_state
phase apply-baseline-config-recipe apply_baseline_config_recipe
phase validate-baseline-config validate_baseline_config
phase install-baseline-plugin-dependencies install_baseline_plugin_dependencies
phase seed-legacy-plugin-dependency-debris seed_legacy_plugin_dependency_debris
phase assert-legacy-plugin-dependency-debris assert_legacy_plugin_dependency_debris_present
phase assert-baseline assert_baseline_state
phase seed-legacy-runtime-deps-symlink seed_legacy_runtime_deps_symlink
phase resolve-candidate resolve_candidate_version
phase update-candidate update_candidate
phase assert-legacy-plugin-dependency-debris-before-doctor assert_legacy_plugin_dependency_debris_present
phase doctor run_doctor
phase assert-legacy-plugin-dependency-debris-cleaned assert_legacy_plugin_dependency_debris_cleaned
phase assert-legacy-runtime-deps-symlink-repaired assert_legacy_runtime_deps_symlink_repaired
phase validate-post-doctor-config validate_post_doctor_config
phase assert-survival assert_survival

View File

@@ -73,6 +73,7 @@ export const UPGRADE_SURVIVOR_SCENARIOS = [
"base",
"feishu-channel",
"bootstrap-persona",
"plugin-deps-cleanup",
"tilde-log-path",
"versioned-runtime-deps",
];
@@ -153,7 +154,7 @@ export function expandUpgradeSurvivorBaselineLanes(poolLanes, rawBaselineSpecs,
return poolLanes;
}
return poolLanes.flatMap((poolLane) => {
if (poolLane.name !== "published-upgrade-survivor") {
if (poolLane.name !== "published-upgrade-survivor" && poolLane.name !== "update-migration") {
return [poolLane];
}
const matrixBaselines = baselineSpecs.length > 0 ? baselineSpecs : [undefined];

View File

@@ -222,6 +222,11 @@ export const mainLanes = [
weight: 3,
},
),
npmLane("update-migration", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:update-migration", {
stateScenario: "upgrade-survivor",
timeoutMs: 30 * 60 * 1000,
weight: 3,
}),
lane("plugins", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugins", {
resources: ["npm", "service"],
stateScenario: "empty",

View File

@@ -50,6 +50,29 @@ function stableVersionFromTag(tagName) {
return version;
}
function parseStableVersion(version) {
const match = /^([0-9]{4})\.([0-9]+)\.([0-9]+)(?:-([0-9]+))?$/u.exec(String(version ?? ""));
if (!match) {
return undefined;
}
return match.slice(1).map((part) => Number.parseInt(part ?? "0", 10));
}
function compareStableVersions(left, right) {
const leftParts = parseStableVersion(left);
const rightParts = parseStableVersion(right);
if (!leftParts || !rightParts) {
throw new Error(`cannot compare release versions: ${left} ${right}`);
}
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
const delta = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
if (delta !== 0) {
return delta;
}
}
return 0;
}
function npmPublishedVersion(version, publishedVersions) {
if (!version || !publishedVersions) {
return version;
@@ -105,6 +128,20 @@ export function resolveReleaseHistory(args) {
return dedupeSpecs(versions);
}
export function resolveAllSince(args, minimumVersion) {
const releasesJson = args.get("releases-json");
if (!releasesJson) {
throw new Error("--releases-json is required when requested baselines include all-since-*");
}
const publishedVersions = readPublishedVersions(args.get("npm-versions-json"));
const releases = readStableReleases(releasesJson, publishedVersions);
return dedupeSpecs(
releases
.map((release) => release.version)
.filter((version) => compareStableVersions(version, minimumVersion) >= 0),
);
}
export function resolveBaselines(args) {
const requested = args.get("requested") ?? "";
const fallback = args.get("fallback") ?? "openclaw@latest";
@@ -117,6 +154,12 @@ export function resolveBaselines(args) {
for (const token of requestedTokens) {
if (token === "release-history") {
resolved.push(...resolveReleaseHistory(args));
} else if (token.startsWith("all-since-")) {
const minimumVersion = token.slice("all-since-".length);
if (!parseStableVersion(minimumVersion)) {
throw new Error(`invalid all-since baseline token: ${token}`);
}
resolved.push(...resolveAllSince(args, minimumVersion));
} else {
exactTokens.push(token);
}