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

@@ -34,17 +34,17 @@ on:
default: 1
type: number
published_upgrade_survivor_baseline:
description: Published OpenClaw package baseline for the published-upgrade-survivor Docker lane
description: Published OpenClaw package baseline for the published-upgrade-survivor/update-migration Docker lane
required: false
default: openclaw@latest
type: string
published_upgrade_survivor_baselines:
description: Optional exact baseline list for published-upgrade-survivor lane expansion
description: Optional exact baseline list for published-upgrade-survivor/update-migration lane expansion
required: false
default: ""
type: string
published_upgrade_survivor_scenarios:
description: Optional scenario list for published-upgrade-survivor lane expansion
description: Optional scenario list for published-upgrade-survivor/update-migration lane expansion
required: false
default: ""
type: string
@@ -129,17 +129,17 @@ on:
default: 1
type: number
published_upgrade_survivor_baseline:
description: Published OpenClaw package baseline for the published-upgrade-survivor Docker lane
description: Published OpenClaw package baseline for the published-upgrade-survivor/update-migration Docker lane
required: false
default: openclaw@latest
type: string
published_upgrade_survivor_baselines:
description: Optional exact baseline list for published-upgrade-survivor lane expansion
description: Optional exact baseline list for published-upgrade-survivor/update-migration lane expansion
required: false
default: ""
type: string
published_upgrade_survivor_scenarios:
description: Optional scenario list for published-upgrade-survivor lane expansion
description: Optional scenario list for published-upgrade-survivor/update-migration lane expansion
required: false
default: ""
type: string

View File

@@ -70,12 +70,12 @@ on:
default: openclaw@latest
type: string
published_upgrade_survivor_baselines:
description: Optional baseline list for published-upgrade-survivor; use release-history for last 6 plus key legacy releases
description: Optional baseline list for published-upgrade-survivor/update-migration; use release-history or all-since-2026.4.23
required: false
default: ""
type: string
published_upgrade_survivor_scenarios:
description: Optional scenario list for published-upgrade-survivor; use reported-issues for known upgrade failure shapes
description: Optional scenario list for published-upgrade-survivor/update-migration; use reported-issues for known upgrade failure shapes
required: false
default: ""
type: string
@@ -150,12 +150,12 @@ on:
default: openclaw@latest
type: string
published_upgrade_survivor_baselines:
description: Optional baseline list for published-upgrade-survivor; use release-history for last 6 plus key legacy releases
description: Optional baseline list for published-upgrade-survivor/update-migration; use release-history or all-since-2026.4.23
required: false
default: ""
type: string
published_upgrade_survivor_scenarios:
description: Optional scenario list for published-upgrade-survivor; use reported-issues for known upgrade failure shapes
description: Optional scenario list for published-upgrade-survivor/update-migration; use reported-issues for known upgrade failure shapes
required: false
default: ""
type: string
@@ -442,7 +442,7 @@ jobs:
fi
releases_json=""
npm_versions_json=""
if [[ "$REQUESTED_BASELINES" == *"release-history"* ]]; then
if [[ "$REQUESTED_BASELINES" == *"release-history"* || "$REQUESTED_BASELINES" == *"all-since-"* ]]; then
releases_json=".artifacts/package-candidate-input/openclaw-releases.json"
npm_versions_json=".artifacts/package-candidate-input/openclaw-npm-versions.json"
mkdir -p "$(dirname "$releases_json")"

46
.github/workflows/update-migration.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Update Migration
on:
workflow_dispatch:
inputs:
workflow_ref:
description: Trusted workflow/harness ref
default: main
required: true
type: string
package_ref:
description: Branch, tag, or SHA to package as the update target
default: main
required: true
type: string
baselines:
description: Published baselines to migrate; use all-since-2026.4.23 for full coverage
default: all-since-2026.4.23
required: true
type: string
scenarios:
description: Update survivor scenarios
default: plugin-deps-cleanup
required: true
type: string
permissions:
actions: read
contents: read
packages: write
pull-requests: read
jobs:
update_migration:
name: Update migration matrix
uses: ./.github/workflows/package-acceptance.yml
with:
workflow_ref: ${{ inputs.workflow_ref }}
source: ref
package_ref: ${{ inputs.package_ref }}
suite_profile: custom
docker_lanes: update-migration
published_upgrade_survivor_baselines: ${{ inputs.baselines }}
published_upgrade_survivor_scenarios: ${{ inputs.scenarios }}
telegram_mode: none
secrets: inherit

View File

@@ -192,7 +192,7 @@ For the dedicated update and plugin testing policy, including local commands,
Docker lanes, Package Acceptance inputs, release defaults, and failure triage,
see [Testing updates and plugins](/help/testing-updates-plugins).
Release checks call Package Acceptance with `source=artifact`, the prepared release package artifact, `suite_profile=custom`, `docker_lanes='doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor plugins-offline plugin-update'`, `published_upgrade_survivor_baselines=release-history`, `published_upgrade_survivor_scenarios=reported-issues`, and `telegram_mode=mock-openai`. This keeps package migration, update, stale-plugin-dependency cleanup, offline plugin, plugin-update, and Telegram proof on the same resolved package tarball. Cross-OS release checks still cover OS-specific onboarding, installer, and platform behavior; package/update product validation should start with Package Acceptance. The `published-upgrade-survivor` Docker lane validates one published package baseline per run. In Package Acceptance, the resolved `package-under-test` tarball is always the candidate and `published_upgrade_survivor_baseline` selects the fallback published baseline, defaulting to `openclaw@latest`; failed-lane rerun commands preserve that baseline. Set `published_upgrade_survivor_baselines=release-history` to expand the lane across a deduped history matrix: the latest six stable releases, `2026.4.23`, and the latest stable release before `2026-03-15`. Set `published_upgrade_survivor_scenarios=reported-issues` to expand the same baselines across issue-shaped fixtures for Feishu config, preserved bootstrap/persona files, tilde log paths, and stale legacy plugin dependency roots. Local aggregate runs can pass exact package specs with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`, keep a single lane with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC` such as `openclaw@2026.4.15`, or set `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS` for the scenario matrix. The published lane configures the baseline with a baked `openclaw config set` command recipe, records recipe steps in `summary.json`, and probes `/healthz`, `/readyz`, plus RPC status after Gateway start. The Windows packaged and installer fresh lanes also verify that an installed package can import a browser-control override from a raw absolute Windows path. The OpenAI cross-OS agent-turn smoke defaults to `OPENCLAW_CROSS_OS_OPENAI_MODEL` when set, otherwise `openai/gpt-5.5`, so the install and gateway proof stays on the preferred GPT-5 test model.
Release checks call Package Acceptance with `source=artifact`, the prepared release package artifact, `suite_profile=custom`, `docker_lanes='doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor plugins-offline plugin-update'`, `published_upgrade_survivor_baselines=release-history`, `published_upgrade_survivor_scenarios=reported-issues`, and `telegram_mode=mock-openai`. This keeps package migration, update, stale-plugin-dependency cleanup, offline plugin, plugin-update, and Telegram proof on the same resolved package tarball. Cross-OS release checks still cover OS-specific onboarding, installer, and platform behavior; package/update product validation should start with Package Acceptance. The `published-upgrade-survivor` Docker lane validates one published package baseline per run. In Package Acceptance, the resolved `package-under-test` tarball is always the candidate and `published_upgrade_survivor_baseline` selects the fallback published baseline, defaulting to `openclaw@latest`; failed-lane rerun commands preserve that baseline. Set `published_upgrade_survivor_baselines=release-history` to expand the lane across a deduped history matrix: the latest six stable releases, `2026.4.23`, and the latest stable release before `2026-03-15`. Set `published_upgrade_survivor_scenarios=reported-issues` to expand the same baselines across issue-shaped fixtures for Feishu config, preserved bootstrap/persona files, tilde log paths, and stale legacy plugin dependency roots. The separate `Update Migration` workflow uses the `update-migration` Docker lane with `all-since-2026.4.23` and `plugin-deps-cleanup` when the question is exhaustive published update cleanup, not normal Full Release CI breadth. Local aggregate runs can pass exact package specs with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`, keep a single lane with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC` such as `openclaw@2026.4.15`, or set `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS` for the scenario matrix. The published lane configures the baseline with a baked `openclaw config set` command recipe, records recipe steps in `summary.json`, and probes `/healthz`, `/readyz`, plus RPC status after Gateway start. The Windows packaged and installer fresh lanes also verify that an installed package can import a browser-control override from a raw absolute Windows path. The OpenAI cross-OS agent-turn smoke defaults to `OPENCLAW_CROSS_OS_OPENAI_MODEL` when set, otherwise `openai/gpt-5.5`, so the install and gateway proof stays on the preferred GPT-5 test model.
### Legacy compatibility windows

View File

@@ -77,6 +77,7 @@ pnpm test:docker:plugins
pnpm test:docker:plugin-update
pnpm test:docker:upgrade-survivor
pnpm test:docker:published-upgrade-survivor
pnpm test:docker:update-migration
```
Important lanes:
@@ -95,6 +96,12 @@ Important lanes:
configures it through a baked `openclaw config set` recipe, updates it to the
candidate tarball, runs doctor, checks legacy cleanup, starts the Gateway, and
probes `/healthz`, `/readyz`, and RPC status.
- `test:docker:update-migration` is the cleanup-heavy published-update lane. It
starts from a configured Discord/Telegram-style user state, runs baseline
doctor so configured plugin dependencies have a chance to materialize, seeds
legacy plugin dependency debris for a configured packaged plugin, updates to
the candidate tarball, and requires post-update doctor to remove the legacy
dependency roots.
Useful published-upgrade survivor variants:
@@ -109,10 +116,24 @@ pnpm test:docker:published-upgrade-survivor
```
Available scenarios are `base`, `feishu-channel`, `bootstrap-persona`,
`tilde-log-path`, and `versioned-runtime-deps`. In aggregate runs,
`plugin-deps-cleanup`, `tilde-log-path`, and `versioned-runtime-deps`. In aggregate runs,
`OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS=reported-issues` expands to all reported
issue-shaped scenarios.
Full update migration is intentionally separate from Full Release CI. Use the
manual `Update Migration` workflow when the release question is "can every
published stable release from 2026.4.23 onward update to this candidate and
clean up plugin dependency debris?":
```bash
gh workflow run update-migration.yml \
--ref main \
-f workflow_ref=main \
-f package_ref=main \
-f baselines=all-since-2026.4.23 \
-f scenarios=plugin-deps-cleanup
```
## Package Acceptance
Package Acceptance is the GitHub-native package gate. It resolves one candidate
@@ -148,6 +169,11 @@ This keeps package migration, update channel switching, stale plugin dependency
cleanup, offline plugin coverage, plugin update behavior, and Telegram package
QA on the same resolved artifact.
`release-history` is a bounded release-check sample: latest six stable releases,
`2026.4.23`, and one older pre-date anchor. For exhaustive published update
migration coverage, use `all-since-2026.4.23` in the separate Update Migration
workflow instead of Full Release CI.
Run a package profile manually when validating a candidate before release:
```bash

View File

@@ -445,6 +445,8 @@ The canonical checklist for update and plugin validation is
[Testing updates and plugins](/help/testing-updates-plugins). Use it when
deciding which local, Docker, Package Acceptance, or release-check lane proves a
plugin install/update, doctor cleanup, or published-package migration change.
Exhaustive published update migration from every stable `2026.4.23+` package is
a separate manual `Update Migration` workflow, not part of Full Release CI.
Legacy package-acceptance leniency is intentionally time boxed. Packages through
`2026.4.25` may use the compatibility path for metadata gaps already published

View File

@@ -45,6 +45,7 @@ title: "Tests"
- `pnpm test:docker:mcp-channels`: Starts a seeded Gateway container and a second client container that spawns `openclaw mcp serve`, then verifies routed conversation discovery, transcript reads, attachment metadata, live event queue behavior, outbound send routing, and Claude-style channel + permission notifications over the real stdio bridge. The Claude notification assertion reads the raw stdio MCP frames directly so the smoke reflects what the bridge actually emits.
- `pnpm test:docker:upgrade-survivor`: Installs the packed OpenClaw tarball over a dirty old-user fixture, runs package update plus non-interactive doctor without live provider or channel keys, then starts a loopback Gateway and checks that agents, channel config, plugin allowlists, workspace/session files, stale legacy plugin dependency state, startup, and RPC status survive.
- `pnpm test:docker:published-upgrade-survivor`: Installs `openclaw@latest` by default, seeds realistic existing-user files without live provider or channel keys, configures that baseline with a baked `openclaw config set` command recipe, updates that published install to the packed OpenClaw tarball, runs non-interactive doctor, writes `.artifacts/upgrade-survivor/summary.json`, then starts a loopback Gateway and checks that configured intents, workspace/session files, stale plugin config and legacy dependency state, startup, `/healthz`, `/readyz`, and RPC status survive or repair cleanly. Override one baseline with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC`, expand an exact matrix with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS`, or add scenario fixtures with `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS=reported-issues`; Package Acceptance exposes those as `published_upgrade_survivor_baseline`, `published_upgrade_survivor_baselines`, and `published_upgrade_survivor_scenarios`.
- `pnpm test:docker:update-migration`: Runs the published-upgrade survivor harness in the cleanup-heavy `plugin-deps-cleanup` scenario, starting at `openclaw@2026.4.23` by default. The separate `Update Migration` workflow expands this lane with `baselines=all-since-2026.4.23` so every stable published package from `.23` onward updates to the candidate and proves configured-plugin dependency cleanup outside Full Release CI.
## Local PR gate

View File

@@ -1518,6 +1518,7 @@
"test:docker:session-runtime-context": "bash scripts/e2e/session-runtime-context-docker.sh",
"test:docker:timings": "node scripts/docker-e2e-timings.mjs",
"test:docker:update-channel-switch": "bash scripts/e2e/update-channel-switch-docker.sh",
"test:docker:update-migration": "env OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1 OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC=${OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:-openclaw@2026.4.23} OPENCLAW_UPGRADE_SURVIVOR_SCENARIO=${OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:-plugin-deps-cleanup} bash scripts/e2e/upgrade-survivor-docker.sh",
"test:docker:upgrade-survivor": "bash scripts/e2e/upgrade-survivor-docker.sh",
"test:e2e": "node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts",
"test:e2e:openshell": "OPENCLAW_E2E_OPENSHELL=1 node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts extensions/openshell/src/backend.e2e.test.ts",

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);
}

View File

@@ -352,11 +352,39 @@ describe("scripts/lib/docker-e2e-plan", () => {
"published-upgrade-survivor-2026.4.29",
"published-upgrade-survivor-2026.4.29-feishu-channel",
"published-upgrade-survivor-2026.4.29-bootstrap-persona",
"published-upgrade-survivor-2026.4.29-plugin-deps-cleanup",
"published-upgrade-survivor-2026.4.29-tilde-log-path",
"published-upgrade-survivor-2026.4.29-versioned-runtime-deps",
]);
});
it("expands update migration across baselines and cleanup scenarios", () => {
const plan = planFor({
selectedLaneNames: ["update-migration"],
upgradeSurvivorBaselines: "2026.4.29 2026.4.23",
upgradeSurvivorScenarios: "plugin-deps-cleanup",
});
expect(plan.lanes.map((lane) => lane.name)).toEqual([
"update-migration-2026.4.29-plugin-deps-cleanup",
"update-migration-2026.4.23-plugin-deps-cleanup",
]);
expect(plan.lanes).toEqual(
expect.arrayContaining([
expect.objectContaining({
command: expect.stringContaining("pnpm test:docker:update-migration"),
imageKind: "bare",
stateScenario: "upgrade-survivor",
}),
expect.objectContaining({
command: expect.stringContaining(
"OPENCLAW_UPGRADE_SURVIVOR_SCENARIO='plugin-deps-cleanup'",
),
}),
]),
);
});
it("plans a live-only selected lane without package e2e images", () => {
const plan = planFor({ selectedLaneNames: ["live-models"] });

View File

@@ -9,6 +9,7 @@ const PACKAGE_JSON = "package.json";
const RELEASE_CHECKS_WORKFLOW = ".github/workflows/openclaw-release-checks.yml";
const FULL_RELEASE_VALIDATION_WORKFLOW = ".github/workflows/full-release-validation.yml";
const QA_LIVE_TRANSPORTS_WORKFLOW = ".github/workflows/qa-live-transports-convex.yml";
const UPDATE_MIGRATION_WORKFLOW = ".github/workflows/update-migration.yml";
const UPGRADE_SURVIVOR_RUN_SCRIPT = "scripts/e2e/lib/upgrade-survivor/run.sh";
type WorkflowStep = {
@@ -94,6 +95,7 @@ describe("package acceptance workflow", () => {
expect(workflow).toContain("--history-count 6");
expect(workflow).toContain("--include-version 2026.4.23");
expect(workflow).toContain("--pre-date 2026-03-15T00:00:00Z");
expect(workflow).toContain('"all-since-"');
expect(workflow).toContain("npm-onboard-channel-agent gateway-network config-reload");
expect(workflow).toContain("npm-onboard-channel-agent doctor-switch");
expect(workflow).toContain("update-channel-switch upgrade-survivor");
@@ -133,6 +135,22 @@ describe("package acceptance workflow", () => {
expect(workflow).toContain("Published upgrade survivor baselines:");
expect(workflow).toContain("Published upgrade survivor scenarios:");
});
it("keeps exhaustive update migration as a separate manual package gate", () => {
const workflow = readFileSync(UPDATE_MIGRATION_WORKFLOW, "utf8");
const packageWorkflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8");
expect(workflow).toContain("name: Update Migration");
expect(workflow).toContain("uses: ./.github/workflows/package-acceptance.yml");
expect(workflow).toContain("source: ref");
expect(workflow).toContain("suite_profile: custom");
expect(workflow).toContain("docker_lanes: update-migration");
expect(workflow).toContain("default: all-since-2026.4.23");
expect(workflow).toContain("default: plugin-deps-cleanup");
expect(workflow).toContain("telegram_mode: none");
expect(workflow).toContain("secrets: inherit");
expect(packageWorkflow).toContain("published-upgrade-survivor/update-migration");
});
});
describe("package artifact reuse", () => {

View File

@@ -75,6 +75,46 @@ describe("scripts/resolve-upgrade-survivor-baselines", () => {
});
});
it("resolves all-since baselines to every stable published release at or after the requested version", () => {
const releases = (
[
["v2026.5.2", "2026-05-03T00:00:00Z"],
["v2026.4.30", "2026-05-01T00:00:00Z"],
["v2026.4.29", "2026-04-30T00:00:00Z"],
["v2026.4.23", "2026-04-23T00:00:00Z"],
["v2026.4.22", "2026-04-22T00:00:00Z"],
["v2026.4.31-beta.1", "2026-05-02T00:00:00Z", true],
] as const
).map(([tagName, publishedAt, isPrerelease = false]) => ({
isPrerelease,
publishedAt,
tagName,
}));
withReleaseFixture(releases, (releasesFile) => {
withJsonFixture(
"versions.json",
["2026.5.2", "2026.4.30", "2026.4.29", "2026.4.23", "2026.4.22"],
(versionsFile) => {
expect(
resolveBaselines(
new Map([
["requested", "all-since-2026.4.23"],
["releases-json", releasesFile],
["npm-versions-json", versionsFile],
]),
),
).toEqual([
"openclaw@2026.5.2",
"openclaw@2026.4.30",
"openclaw@2026.4.29",
"openclaw@2026.4.23",
]);
},
);
});
});
it("maps release-history anchors to npm-published package versions when GitHub tags have republish suffixes", () => {
const releases = (
[