From 682e05532d8588c9c2d5828918de03918ec17796 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 00:34:28 +0100 Subject: [PATCH] test: add update migration package gate --- .../openclaw-live-and-e2e-checks-reusable.yml | 12 +- .github/workflows/package-acceptance.yml | 10 +- .github/workflows/update-migration.yml | 46 +++++++ docs/ci.md | 2 +- docs/help/testing-updates-plugins.md | 28 +++- docs/reference/RELEASING.md | 2 + docs/reference/test.md | 1 + package.json | 1 + .../e2e/lib/upgrade-survivor/assertions.mjs | 1 + scripts/e2e/lib/upgrade-survivor/run.sh | 123 ++++++++++++++++++ scripts/lib/docker-e2e-plan.mjs | 3 +- scripts/lib/docker-e2e-scenarios.mjs | 5 + .../resolve-upgrade-survivor-baselines.mjs | 43 ++++++ test/scripts/docker-e2e-plan.test.ts | 28 ++++ .../package-acceptance-workflow.test.ts | 18 +++ .../upgrade-survivor-baselines.test.ts | 40 ++++++ 16 files changed, 349 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/update-migration.yml diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index 1700ce173fc..d17bd927c38 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -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 diff --git a/.github/workflows/package-acceptance.yml b/.github/workflows/package-acceptance.yml index 53b231b9293..c8b920385af 100644 --- a/.github/workflows/package-acceptance.yml +++ b/.github/workflows/package-acceptance.yml @@ -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")" diff --git a/.github/workflows/update-migration.yml b/.github/workflows/update-migration.yml new file mode 100644 index 00000000000..cf671c49e2a --- /dev/null +++ b/.github/workflows/update-migration.yml @@ -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 diff --git a/docs/ci.md b/docs/ci.md index 915df398513..ac5465fa943 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -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 diff --git a/docs/help/testing-updates-plugins.md b/docs/help/testing-updates-plugins.md index dcb87239be7..33fff82108b 100644 --- a/docs/help/testing-updates-plugins.md +++ b/docs/help/testing-updates-plugins.md @@ -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 diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index c461fe7a742..0d77c53c8ba 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -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 diff --git a/docs/reference/test.md b/docs/reference/test.md index ee93fa87785..08887989b85 100644 --- a/docs/reference/test.md +++ b/docs/reference/test.md @@ -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 diff --git a/package.json b/package.json index 3c8381873df..6433719131d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/e2e/lib/upgrade-survivor/assertions.mjs b/scripts/e2e/lib/upgrade-survivor/assertions.mjs index 208ae295699..e5eaf18e39e 100644 --- a/scripts/e2e/lib/upgrade-survivor/assertions.mjs +++ b/scripts/e2e/lib/upgrade-survivor/assertions.mjs @@ -6,6 +6,7 @@ const SCENARIOS = new Set([ "base", "feishu-channel", "bootstrap-persona", + "plugin-deps-cleanup", "tilde-log-path", "versioned-runtime-deps", ]); diff --git a/scripts/e2e/lib/upgrade-survivor/run.sh b/scripts/e2e/lib/upgrade-survivor/run.sh index 18c5305138d..4594296508b 100644 --- a/scripts/e2e/lib/upgrade-survivor/run.sh +++ b/scripts/e2e/lib/upgrade-survivor/run.sh @@ -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 diff --git a/scripts/lib/docker-e2e-plan.mjs b/scripts/lib/docker-e2e-plan.mjs index 596f75ee911..f093380d7b2 100644 --- a/scripts/lib/docker-e2e-plan.mjs +++ b/scripts/lib/docker-e2e-plan.mjs @@ -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]; diff --git a/scripts/lib/docker-e2e-scenarios.mjs b/scripts/lib/docker-e2e-scenarios.mjs index 531aabf7b97..a371df6c8a7 100644 --- a/scripts/lib/docker-e2e-scenarios.mjs +++ b/scripts/lib/docker-e2e-scenarios.mjs @@ -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", diff --git a/scripts/resolve-upgrade-survivor-baselines.mjs b/scripts/resolve-upgrade-survivor-baselines.mjs index af3e958d58b..c4c10ade881 100644 --- a/scripts/resolve-upgrade-survivor-baselines.mjs +++ b/scripts/resolve-upgrade-survivor-baselines.mjs @@ -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); } diff --git a/test/scripts/docker-e2e-plan.test.ts b/test/scripts/docker-e2e-plan.test.ts index 0b2884a5f94..983746f53e4 100644 --- a/test/scripts/docker-e2e-plan.test.ts +++ b/test/scripts/docker-e2e-plan.test.ts @@ -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"] }); diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 30635762696..d38e4faa1d8 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -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", () => { diff --git a/test/scripts/upgrade-survivor-baselines.test.ts b/test/scripts/upgrade-survivor-baselines.test.ts index 54035863013..3b05720d327 100644 --- a/test/scripts/upgrade-survivor-baselines.test.ts +++ b/test/scripts/upgrade-survivor-baselines.test.ts @@ -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 = ( [