From 1c3b27718fe84ff4685bd2bbdce9db63f29610e7 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 4 May 2026 20:53:11 -0700 Subject: [PATCH] ci: shard package upgrade survivor baselines --- .../openclaw-live-and-e2e-checks-reusable.yml | 39 +++----- .github/workflows/openclaw-release-checks.yml | 2 +- .github/workflows/package-acceptance.yml | 6 +- docs/ci.md | 2 +- docs/help/testing-updates-plugins.md | 21 +++- docs/help/testing.md | 2 +- docs/reference/RELEASING.md | 5 +- docs/reference/test.md | 2 +- scripts/plan-targeted-docker-lane-groups.mjs | 97 +++++++++++++++++++ .../resolve-upgrade-survivor-baselines.mjs | 21 +++- .../package-acceptance-workflow.test.ts | 11 ++- .../targeted-docker-lane-groups.test.ts | 68 +++++++++++++ .../upgrade-survivor-baselines.test.ts | 43 ++++++++ 13 files changed, 276 insertions(+), 43 deletions(-) create mode 100644 scripts/plan-targeted-docker-lane-groups.mjs create mode 100644 test/scripts/targeted-docker-lane-groups.test.ts diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index 43000d0f680..c0043b74a93 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -861,36 +861,24 @@ jobs: runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 5 outputs: - groups_json: ${{ steps.plan.outputs.groups_json }} + groups_json: ${{ steps.groups.outputs.groups_json }} steps: - - name: Plan targeted Docker lane groups - id: plan + - name: Checkout trusted release harness + uses: actions/checkout@v6 + with: + ref: ${{ github.sha }} + fetch-depth: 1 + + - name: Build targeted Docker lane groups + id: groups shell: bash env: LANES: ${{ inputs.docker_lanes }} GROUP_SIZE: ${{ inputs.targeted_docker_lane_group_size }} + OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ inputs.published_upgrade_survivor_baselines }} run: | set -euo pipefail - groups_json="$( - LANES="$LANES" GROUP_SIZE="$GROUP_SIZE" node <<'NODE' - const lanes = [...new Set(String(process.env.LANES || "").split(/[,\s]+/u).map((lane) => lane.trim()).filter(Boolean))]; - if (lanes.length === 0) { - throw new Error("docker_lanes is required when planning targeted Docker lane groups."); - } - const rawGroupSize = Number.parseInt(process.env.GROUP_SIZE || "1", 10); - const groupSize = Number.isFinite(rawGroupSize) && rawGroupSize > 0 ? rawGroupSize : 1; - const sanitize = (lane) => lane.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "targeted"; - const groups = []; - for (let index = 0; index < lanes.length; index += groupSize) { - const groupLanes = lanes.slice(index, index + groupSize); - const first = sanitize(groupLanes[0]); - const last = sanitize(groupLanes[groupLanes.length - 1]); - const label = groupLanes.length === 1 ? first : `${first}--${last}`; - groups.push({ label, docker_lanes: groupLanes.join(" ") }); - } - process.stdout.write(JSON.stringify(groups)); - NODE - )" + groups_json="$(node scripts/plan-targeted-docker-lane-groups.mjs)" echo "groups_json=${groups_json}" >> "$GITHUB_OUTPUT" validate_docker_lanes: @@ -957,7 +945,7 @@ jobs: OPENCLAW_DOCKER_E2E_SELECTED_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }} OPENCLAW_CURRENT_PACKAGE_TGZ: .artifacts/docker-e2e-package/openclaw-current.tgz OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: ${{ inputs.published_upgrade_survivor_baseline }} - OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ inputs.published_upgrade_survivor_baselines }} + OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ matrix.group.published_upgrade_survivor_baselines || inputs.published_upgrade_survivor_baselines }} OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS: ${{ inputs.published_upgrade_survivor_scenarios }} OPENCLAW_SKIP_DOCKER_BUILD: "1" INCLUDE_OPENWEBUI: ${{ inputs.include_openwebui }} @@ -998,6 +986,7 @@ jobs: shell: bash env: LANES: ${{ matrix.group.docker_lanes }} + GROUP_LABEL: ${{ matrix.group.label }} INCLUDE_OPENWEBUI: ${{ inputs.include_openwebui }} INCLUDE_RELEASE_PATH_SUITES: ${{ inputs.include_release_path_suites }} run: | @@ -1017,7 +1006,7 @@ jobs: plan_path=".artifacts/docker-tests/targeted-plan.json" node .release-harness/scripts/test-docker-all.mjs --plan-json > "$plan_path" node .release-harness/scripts/docker-e2e.mjs github-outputs "$plan_path" >> "$GITHUB_OUTPUT" - suffix="$(printf '%s' "$LANES" | tr ',[:space:]' '-' | tr -cd 'A-Za-z0-9._-' | sed -E 's/-+/-/g; s/^-//; s/-$//')" + suffix="$(printf '%s' "${GROUP_LABEL:-$LANES}" | tr ',[:space:]' '-' | tr -cd 'A-Za-z0-9._-' | sed -E 's/-+/-/g; s/^-//; s/-$//')" echo "artifact_suffix=${suffix:-targeted}" >> "$GITHUB_OUTPUT" echo "plan_json=$plan_path" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/openclaw-release-checks.yml b/.github/workflows/openclaw-release-checks.yml index a97c52f50b3..92ca6d09334 100644 --- a/.github/workflows/openclaw-release-checks.yml +++ b/.github/workflows/openclaw-release-checks.yml @@ -559,7 +559,7 @@ jobs: package_sha256: ${{ needs.prepare_release_package.outputs.package_sha256 }} suite_profile: custom docker_lanes: doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor plugins-offline plugin-update - published_upgrade_survivor_baselines: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'all-since-2026.4.23' || '' }} + published_upgrade_survivor_baselines: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'last-stable-4 2026.4.23 2026.5.2 2026.4.15' || '' }} published_upgrade_survivor_scenarios: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'reported-issues' || '' }} telegram_mode: mock-openai telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-context-command,telegram-current-session-status-tool,telegram-mention-gating diff --git a/.github/workflows/package-acceptance.yml b/.github/workflows/package-acceptance.yml index 5ca4d5f28c9..215e623aafc 100644 --- a/.github/workflows/package-acceptance.yml +++ b/.github/workflows/package-acceptance.yml @@ -70,7 +70,7 @@ on: default: openclaw@latest type: string published_upgrade_survivor_baselines: - description: Optional baseline list for published-upgrade-survivor/update-migration; use all-since-2026.4.23, release-history, or exact versions + description: Optional baseline list for published-upgrade-survivor/update-migration; use last-stable-4, all-since-2026.4.23, release-history, or exact versions required: false default: "" type: string @@ -150,7 +150,7 @@ on: default: openclaw@latest type: string published_upgrade_survivor_baselines: - description: Optional baseline list for published-upgrade-survivor/update-migration; use all-since-2026.4.23, release-history, or exact versions + description: Optional baseline list for published-upgrade-survivor/update-migration; use last-stable-4, all-since-2026.4.23, release-history, or exact versions required: false default: "" type: string @@ -442,7 +442,7 @@ jobs: fi releases_json="" npm_versions_json="" - if [[ "$REQUESTED_BASELINES" == *"release-history"* || "$REQUESTED_BASELINES" == *"all-since-"* ]]; then + if [[ "$REQUESTED_BASELINES" == *"release-history"* || "$REQUESTED_BASELINES" == *"all-since-"* || "$REQUESTED_BASELINES" == *"last-stable-"* ]]; 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/docs/ci.md b/docs/ci.md index dbdffeea439..134da4a8146 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -265,7 +265,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'`, and `telegram_mode=mock-openai`. This keeps package migration, update, stale-plugin-dependency cleanup, configured-plugin install repair, offline plugin, plugin-update, and Telegram proof on the same resolved package tarball. Set `package_acceptance_package_spec` on Full Release Validation or OpenClaw Release Checks to run that same matrix against a shipped npm package instead of the SHA-built artifact. 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 the blocking release path. 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. Full Release Validation with `run_release_soak=true` or `release_profile=full` sets `published_upgrade_survivor_baselines=all-since-2026.4.23` and `published_upgrade_survivor_scenarios=reported-issues` to expand across every stable npm release from `2026.4.23` through `latest` and issue-shaped fixtures for Feishu config, preserved bootstrap/persona files, configured OpenClaw plugin installs, 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.4`, so the install and gateway proof stays on a GPT-5 test model while avoiding GPT-4.x defaults. +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'`, and `telegram_mode=mock-openai`. This keeps package migration, update, stale-plugin-dependency cleanup, configured-plugin install repair, offline plugin, plugin-update, and Telegram proof on the same resolved package tarball. Set `package_acceptance_package_spec` on Full Release Validation or OpenClaw Release Checks to run that same matrix against a shipped npm package instead of the SHA-built artifact. 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 the blocking release path. 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. Full Release Validation with `run_release_soak=true` or `release_profile=full` sets `published_upgrade_survivor_baselines='last-stable-4 2026.4.23 2026.5.2 2026.4.15'` and `published_upgrade_survivor_scenarios=reported-issues` to expand across the four latest stable npm releases plus pinned plugin-compatibility boundary releases and issue-shaped fixtures for Feishu config, preserved bootstrap/persona files, configured OpenClaw plugin installs, tilde log paths, and stale legacy plugin dependency roots. Multi-baseline published-upgrade survivor selections are sharded by baseline into separate targeted Docker runner jobs. 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.4`, so the install and gateway proof stays on a GPT-5 test model while avoiding GPT-4.x defaults. ### Legacy compatibility windows diff --git a/docs/help/testing-updates-plugins.md b/docs/help/testing-updates-plugins.md index 45ab2115d8e..dfa5b26fbeb 100644 --- a/docs/help/testing-updates-plugins.md +++ b/docs/help/testing-updates-plugins.md @@ -170,24 +170,35 @@ Release checks call Package Acceptance with the package/update/plugin set: doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor plugins-offline plugin-update ``` -They also pass: +When release soak is enabled, they also pass: ```text -published_upgrade_survivor_baselines=all-since-2026.4.23 +published_upgrade_survivor_baselines=last-stable-4 2026.4.23 2026.5.2 2026.4.15 published_upgrade_survivor_scenarios=reported-issues telegram_mode=mock-openai ``` 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. +QA on the same resolved artifact without making the default release package gate +walk every published release. -`all-since-2026.4.23` is the Full Release CI upgrade sample: every stable npm-published release from `2026.4.23` through `latest`. For exhaustive published +`last-stable-4` resolves to the four latest stable npm-published OpenClaw +releases. Release package acceptance pins `2026.4.23` as the first plugin-update +compatibility boundary, `2026.5.2` as a plugin-architecture churn boundary, and +`2026.4.15` as an older 2026.4.1x published-update baseline; the resolver +dedupes pins that are already in the latest four. For exhaustive published update migration coverage, use `all-since-2026.4.23` in the separate Update Migration workflow instead of Full Release CI. `release-history` remains available for manual wider sampling when you also want the legacy pre-date anchor. +When multiple published-upgrade survivor baselines are selected, the reusable +Docker workflow shards each baseline into its own targeted runner job. Each +baseline shard still runs the selected scenario set, but logs and artifacts stay +per-baseline and wall time is bounded by the slowest shard instead of one large +serial job. + Run a package profile manually when validating a candidate before release: ```bash @@ -197,7 +208,7 @@ gh workflow run package-acceptance.yml \ -f source=npm \ -f package_spec=openclaw@beta \ -f suite_profile=package \ - -f published_upgrade_survivor_baselines=all-since-2026.4.23 \ + -f published_upgrade_survivor_baselines="last-stable-4 2026.4.23 2026.5.2 2026.4.15" \ -f published_upgrade_survivor_scenarios=reported-issues \ -f telegram_mode=mock-openai ``` diff --git a/docs/help/testing.md b/docs/help/testing.md index d956ccc28fe..001abf4bd3b 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -643,7 +643,7 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or - Npm tarball onboarding/channel/agent smoke: `pnpm test:docker:npm-onboard-channel-agent` installs the packed OpenClaw tarball globally in Docker, configures OpenAI via env-ref onboarding plus Telegram by default, runs doctor, and runs one mocked OpenAI agent turn. Reuse a prebuilt tarball with `OPENCLAW_CURRENT_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host rebuild with `OPENCLAW_NPM_ONBOARD_HOST_BUILD=0`, or switch channel with `OPENCLAW_NPM_ONBOARD_CHANNEL=discord` or `OPENCLAW_NPM_ONBOARD_CHANNEL=slack`. - 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. - Upgrade survivor smoke: `pnpm test:docker:upgrade-survivor` installs the packed OpenClaw tarball over a dirty old-user fixture with agents, channel config, plugin allowlists, stale plugin dependency state, and existing workspace/session files. It runs package update plus non-interactive doctor without live provider or channel keys, then starts a loopback Gateway and checks config/state preservation plus startup/status budgets. -- Published upgrade survivor smoke: `pnpm test:docker:published-upgrade-survivor` installs `openclaw@latest` by default, seeds realistic existing-user files, configures that baseline with a baked command recipe, validates the resulting config, updates that published install to the candidate tarball, runs non-interactive doctor, writes `.artifacts/upgrade-survivor/summary.json`, then starts a loopback Gateway and checks configured intents, state preservation, startup, `/healthz`, `/readyz`, and RPC status budgets. Override one baseline with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC`, ask the aggregate scheduler to expand exact baselines with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS` such as `all-since-2026.4.23`, and expand issue-shaped fixtures with `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS` such as `reported-issues`; the reported-issues set includes `configured-plugin-installs` for automatic external OpenClaw plugin install repair. Package Acceptance exposes those as `published_upgrade_survivor_baseline`, `published_upgrade_survivor_baselines`, and `published_upgrade_survivor_scenarios`; Full Release Validation uses the default latest baseline in the blocking path and expands to all-since/reported-issues only for `run_release_soak=true` or `release_profile=full`. +- Published upgrade survivor smoke: `pnpm test:docker:published-upgrade-survivor` installs `openclaw@latest` by default, seeds realistic existing-user files, configures that baseline with a baked command recipe, validates the resulting config, updates that published install to the candidate tarball, runs non-interactive doctor, writes `.artifacts/upgrade-survivor/summary.json`, then starts a loopback Gateway and checks configured intents, state preservation, startup, `/healthz`, `/readyz`, and RPC status budgets. Override one baseline with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC`, ask the aggregate scheduler to expand exact baselines with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS` such as `last-stable-4 2026.4.23 2026.5.2 2026.4.15` or `all-since-2026.4.23`, and expand issue-shaped fixtures with `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS` such as `reported-issues`; the reported-issues set includes `configured-plugin-installs` for automatic external OpenClaw plugin install repair. Package Acceptance exposes those as `published_upgrade_survivor_baseline`, `published_upgrade_survivor_baselines`, and `published_upgrade_survivor_scenarios`; Full Release Validation uses the default latest baseline in the blocking path and expands the release-soak package gate to `last-stable-4 2026.4.23 2026.5.2 2026.4.15` plus `reported-issues`. - Session runtime context smoke: `pnpm test:docker:session-runtime-context` verifies hidden runtime context transcript persistence plus doctor repair of affected duplicated prompt-rewrite branches. - Bun global install smoke: `bash scripts/e2e/bun-global-install-smoke.sh` packs the current tree, installs it with `bun install -g` in an isolated home, and verifies `openclaw infer image providers --json` returns bundled image providers instead of hanging. Reuse a prebuilt tarball with `OPENCLAW_BUN_GLOBAL_SMOKE_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host build with `OPENCLAW_BUN_GLOBAL_SMOKE_HOST_BUILD=0`, or copy `dist/` from a built Docker image with `OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE=openclaw-dockerfile-smoke:local`. - Installer Docker smoke: `bash scripts/test-install-sh-docker.sh` shares one npm cache across its root, update, and direct-npm containers. Update smoke defaults to npm `latest` as the stable baseline before upgrading to the candidate tarball. Override with `OPENCLAW_INSTALL_SMOKE_UPDATE_BASELINE=2026.4.22` locally, or with the Install Smoke workflow's `update_baseline_version` input on GitHub. Non-root installer checks keep an isolated npm cache so root-owned cache entries do not mask user-local install behavior. Set `OPENCLAW_INSTALL_SMOKE_NPM_CACHE_DIR=/path/to/cache` to reuse the root/update/direct-npm cache across local reruns. diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index e38894361c8..ea5e0ee5852 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -322,7 +322,10 @@ Use `release_profile` to select live/provider breadth: Use `run_release_soak=true` with `stable` when the release-blocking lanes are green and you want the exhaustive live/E2E, Docker release-path, and -all-since-2026.4.23 upgrade-survivor sweep before promotion. `full` implies +bounded published upgrade-survivor sweep before promotion. That sweep covers +the latest four stable packages plus pinned `2026.4.23` and `2026.5.2` +baselines plus older `2026.4.15` coverage, with duplicate baselines removed and +each baseline sharded into its own Docker runner job. `full` implies `run_release_soak=true`. `OpenClaw Release Checks` uses the trusted workflow ref to resolve the target diff --git a/docs/reference/test.md b/docs/reference/test.md index f20d60f73e3..7d352cc6dd4 100644 --- a/docs/reference/test.md +++ b/docs/reference/test.md @@ -44,7 +44,7 @@ title: "Tests" - `pnpm test:docker:openwebui`: Starts Dockerized OpenClaw + Open WebUI, signs in through Open WebUI, checks `/api/models`, then runs a real proxied chat through `/api/chat/completions`. Requires a usable live model key (for example OpenAI in `~/.profile`), pulls an external Open WebUI image, and is not expected to be CI-stable like the normal unit/e2e suites. - `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` such as `all-since-2026.4.23`, or add scenario fixtures with `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS=reported-issues`; the reported-issues set includes `configured-plugin-installs` to verify configured external OpenClaw plugins install automatically during upgrade and `stale-source-plugin-shadow` to keep source-only plugin shadows from breaking startup. Package Acceptance exposes those as `published_upgrade_survivor_baseline`, `published_upgrade_survivor_baselines`, and `published_upgrade_survivor_scenarios`. +- `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` such as `last-stable-4 2026.4.23 2026.5.2 2026.4.15` or `all-since-2026.4.23`, or add scenario fixtures with `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS=reported-issues`; the reported-issues set includes `configured-plugin-installs` to verify configured external OpenClaw plugins install automatically during upgrade and `stale-source-plugin-shadow` to keep source-only plugin shadows from breaking startup. 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. - `pnpm test:docker:plugins`: Runs install/update smoke for local path, `file:`, npm registry packages with hoisted dependencies, git moving refs, ClawHub fixtures, marketplace updates, and Claude-bundle enable/inspect. diff --git a/scripts/plan-targeted-docker-lane-groups.mjs b/scripts/plan-targeted-docker-lane-groups.mjs new file mode 100644 index 00000000000..74149807a3c --- /dev/null +++ b/scripts/plan-targeted-docker-lane-groups.mjs @@ -0,0 +1,97 @@ +import { fileURLToPath } from "node:url"; + +const BASELINE_SHARDED_LANES = new Set(["published-upgrade-survivor", "update-migration"]); + +function splitTokens(raw) { + return [ + ...new Set( + String(raw ?? "") + .split(/[,\s]+/u) + .map((token) => token.trim()) + .filter(Boolean), + ), + ]; +} + +function parsePositiveInt(raw, fallback, label) { + const parsed = Number.parseInt(String(raw ?? ""), 10); + if (!Number.isFinite(parsed)) { + return fallback; + } + if (parsed < 1) { + throw new Error(`${label} must be a positive integer. Got: ${JSON.stringify(raw)}`); + } + return parsed; +} + +function sanitizeLabel(value) { + return ( + String(value) + .replace(/^openclaw@/u, "") + .replace(/[^A-Za-z0-9._-]+/g, "-") + .replace(/^-+|-+$/g, "") || "targeted" + ); +} + +export function planTargetedDockerLaneGroups({ + groupSize = 1, + lanes, + upgradeSurvivorBaselines = "", +} = {}) { + const selectedLanes = splitTokens(lanes); + if (selectedLanes.length === 0) { + throw new Error("docker_lanes is required when planning targeted Docker lane groups."); + } + + const parsedGroupSize = parsePositiveInt(groupSize, 1, "groupSize"); + const baselineSpecs = splitTokens(upgradeSurvivorBaselines); + const groups = []; + let pendingLanes = []; + + const flushPending = () => { + if (pendingLanes.length === 0) { + return; + } + const first = sanitizeLabel(pendingLanes[0]); + const last = sanitizeLabel(pendingLanes[pendingLanes.length - 1]); + const label = pendingLanes.length === 1 ? first : `${first}--${last}`; + groups.push({ docker_lanes: pendingLanes.join(" "), label }); + pendingLanes = []; + }; + + for (const lane of selectedLanes) { + if (BASELINE_SHARDED_LANES.has(lane) && baselineSpecs.length > 1) { + flushPending(); + for (const baselineSpec of baselineSpecs) { + groups.push({ + docker_lanes: lane, + label: `${sanitizeLabel(lane)}-${sanitizeLabel(baselineSpec)}`, + published_upgrade_survivor_baselines: baselineSpec, + }); + } + continue; + } + + pendingLanes.push(lane); + if (pendingLanes.length >= parsedGroupSize) { + flushPending(); + } + } + + flushPending(); + return groups; +} + +const isMain = process.argv[1] ? fileURLToPath(import.meta.url) === process.argv[1] : false; + +if (isMain) { + process.stdout.write( + JSON.stringify( + planTargetedDockerLaneGroups({ + groupSize: process.env.GROUP_SIZE, + lanes: process.env.LANES, + upgradeSurvivorBaselines: process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS, + }), + ), + ); +} diff --git a/scripts/resolve-upgrade-survivor-baselines.mjs b/scripts/resolve-upgrade-survivor-baselines.mjs index c4c10ade881..47ed92c616a 100644 --- a/scripts/resolve-upgrade-survivor-baselines.mjs +++ b/scripts/resolve-upgrade-survivor-baselines.mjs @@ -128,6 +128,19 @@ export function resolveReleaseHistory(args) { return dedupeSpecs(versions); } +export function resolveLastStable(args, count) { + const releasesJson = args.get("releases-json"); + if (!releasesJson) { + throw new Error("--releases-json is required when requested baselines include last-stable-*"); + } + if (!Number.isInteger(count) || count < 1) { + throw new Error(`invalid last-stable baseline count: ${count}`); + } + const publishedVersions = readPublishedVersions(args.get("npm-versions-json")); + const releases = readStableReleases(releasesJson, publishedVersions); + return dedupeSpecs(releases.slice(0, count).map((release) => release.version)); +} + export function resolveAllSince(args, minimumVersion) { const releasesJson = args.get("releases-json"); if (!releasesJson) { @@ -149,11 +162,13 @@ export function resolveBaselines(args) { if (requestedTokens.length === 0) { return dedupeSpecs([fallback]); } - const exactTokens = []; const resolved = []; for (const token of requestedTokens) { if (token === "release-history") { resolved.push(...resolveReleaseHistory(args)); + } else if (token.startsWith("last-stable-")) { + const count = Number.parseInt(token.slice("last-stable-".length), 10); + resolved.push(...resolveLastStable(args, count)); } else if (token.startsWith("all-since-")) { const minimumVersion = token.slice("all-since-".length); if (!parseStableVersion(minimumVersion)) { @@ -161,10 +176,10 @@ export function resolveBaselines(args) { } resolved.push(...resolveAllSince(args, minimumVersion)); } else { - exactTokens.push(token); + resolved.push(token); } } - return dedupeSpecs([...exactTokens, ...resolved]); + return dedupeSpecs(resolved); } const isMain = process.argv[1] ? fileURLToPath(import.meta.url) === process.argv[1] : false; diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 9e325e78a81..1d4126f3864 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -92,12 +92,14 @@ describe("package acceptance workflow", () => { expect(workflow).toContain("suite_profile:"); expect(workflow).toContain("published_upgrade_survivor_baseline:"); expect(workflow).toContain("published_upgrade_survivor_baselines:"); + expect(workflow).toContain("last-stable-4"); expect(workflow).toContain("all-since-2026.4.23"); expect(workflow).toContain("published_upgrade_survivor_scenarios:"); expect(workflow).toContain("scripts/resolve-upgrade-survivor-baselines.mjs"); 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('"last-stable-"'); 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"); @@ -199,7 +201,7 @@ describe("package artifact reuse", () => { "OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: ${{ inputs.published_upgrade_survivor_baseline }}", ); expect(workflow).toContain( - "OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ inputs.published_upgrade_survivor_baselines }}", + "OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ matrix.group.published_upgrade_survivor_baselines || inputs.published_upgrade_survivor_baselines }}", ); expect(workflow).toContain( "OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS: ${{ inputs.published_upgrade_survivor_scenarios }}", @@ -229,8 +231,13 @@ describe("package artifact reuse", () => { }); expect(workflow).toContain("plan_docker_lane_groups:"); expect(workflow).toContain("targeted_docker_lane_group_size:"); + expect(workflow).toContain("scripts/plan-targeted-docker-lane-groups.mjs"); + expect(workflow).toContain( + "OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ inputs.published_upgrade_survivor_baselines }}", + ); expect(workflow).toContain("Docker E2E targeted lanes (${{ matrix.group.label }})"); expect(workflow).toContain("LANES: ${{ matrix.group.docker_lanes }}"); + expect(workflow).toContain("GROUP_LABEL: ${{ matrix.group.label }}"); expect(workflow).toContain("DOCKER_E2E_LANES: ${{ matrix.group.docker_lanes }}"); expect(workflow).toContain("name: docker-e2e-${{ steps.plan.outputs.artifact_suffix }}"); expect(scheduler).toContain( @@ -530,7 +537,7 @@ describe("package artifact reuse", () => { "docker_lanes: doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor plugins-offline plugin-update", ); expect(workflow).toContain( - "published_upgrade_survivor_baselines: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'all-since-2026.4.23' || '' }}", + "published_upgrade_survivor_baselines: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'last-stable-4 2026.4.23 2026.5.2 2026.4.15' || '' }}", ); expect(workflow).toContain( "published_upgrade_survivor_scenarios: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'reported-issues' || '' }}", diff --git a/test/scripts/targeted-docker-lane-groups.test.ts b/test/scripts/targeted-docker-lane-groups.test.ts new file mode 100644 index 00000000000..74911d194ab --- /dev/null +++ b/test/scripts/targeted-docker-lane-groups.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from "vitest"; +import { planTargetedDockerLaneGroups } from "../../scripts/plan-targeted-docker-lane-groups.mjs"; + +describe("scripts/plan-targeted-docker-lane-groups", () => { + it("keeps normal targeted lanes grouped by the configured group size", () => { + expect( + planTargetedDockerLaneGroups({ + groupSize: 2, + lanes: "doctor-switch update-channel-switch plugin-update", + }), + ).toEqual([ + { + docker_lanes: "doctor-switch update-channel-switch", + label: "doctor-switch--update-channel-switch", + }, + { docker_lanes: "plugin-update", label: "plugin-update" }, + ]); + }); + + it("shards published upgrade survivor by baseline while preserving surrounding lanes", () => { + expect( + planTargetedDockerLaneGroups({ + groupSize: 2, + lanes: + "doctor-switch update-channel-switch published-upgrade-survivor plugins-offline plugin-update", + upgradeSurvivorBaselines: + "openclaw@2026.5.3-1 openclaw@2026.5.3 openclaw@2026.5.2 openclaw@2026.4.23", + }), + ).toEqual([ + { + docker_lanes: "doctor-switch update-channel-switch", + label: "doctor-switch--update-channel-switch", + }, + { + docker_lanes: "published-upgrade-survivor", + label: "published-upgrade-survivor-2026.5.3-1", + published_upgrade_survivor_baselines: "openclaw@2026.5.3-1", + }, + { + docker_lanes: "published-upgrade-survivor", + label: "published-upgrade-survivor-2026.5.3", + published_upgrade_survivor_baselines: "openclaw@2026.5.3", + }, + { + docker_lanes: "published-upgrade-survivor", + label: "published-upgrade-survivor-2026.5.2", + published_upgrade_survivor_baselines: "openclaw@2026.5.2", + }, + { + docker_lanes: "published-upgrade-survivor", + label: "published-upgrade-survivor-2026.4.23", + published_upgrade_survivor_baselines: "openclaw@2026.4.23", + }, + { docker_lanes: "plugins-offline plugin-update", label: "plugins-offline--plugin-update" }, + ]); + }); + + it("leaves a single baseline on the normal logical lane", () => { + expect( + planTargetedDockerLaneGroups({ + lanes: "published-upgrade-survivor", + upgradeSurvivorBaselines: "openclaw@2026.5.2", + }), + ).toEqual([ + { docker_lanes: "published-upgrade-survivor", label: "published-upgrade-survivor" }, + ]); + }); +}); diff --git a/test/scripts/upgrade-survivor-baselines.test.ts b/test/scripts/upgrade-survivor-baselines.test.ts index 3b05720d327..7f4771524a4 100644 --- a/test/scripts/upgrade-survivor-baselines.test.ts +++ b/test/scripts/upgrade-survivor-baselines.test.ts @@ -115,6 +115,49 @@ describe("scripts/resolve-upgrade-survivor-baselines", () => { }); }); + it("resolves last-stable baselines to the latest stable published package versions", () => { + const releases = ( + [ + ["v2026.5.4-beta.1", "2026-05-05T00:00:00Z", true], + ["v2026.5.3-1", "2026-05-04T00:00:00Z"], + ["v2026.5.3", "2026-05-03T00:00:00Z"], + ["v2026.5.2", "2026-05-02T00:00:00Z"], + ["v2026.4.29", "2026-04-30T00:00:00Z"], + ["v2026.4.27", "2026-04-28T00:00:00Z"], + ["v2026.4.15", "2026-04-16T00:00:00Z"], + ] as const + ).map(([tagName, publishedAt, isPrerelease = false]) => ({ + isPrerelease, + publishedAt, + tagName, + })); + + withReleaseFixture(releases, (releasesFile) => { + withJsonFixture( + "versions.json", + ["2026.5.3-1", "2026.5.3", "2026.5.2", "2026.4.29", "2026.4.27", "2026.4.15"], + (versionsFile) => { + expect( + resolveBaselines( + new Map([ + ["requested", "last-stable-4 2026.4.23 2026.5.2 2026.4.15"], + ["releases-json", releasesFile], + ["npm-versions-json", versionsFile], + ]), + ), + ).toEqual([ + "openclaw@2026.5.3-1", + "openclaw@2026.5.3", + "openclaw@2026.5.2", + "openclaw@2026.4.29", + "openclaw@2026.4.23", + "openclaw@2026.4.15", + ]); + }, + ); + }); + }); + it("maps release-history anchors to npm-published package versions when GitHub tags have republish suffixes", () => { const releases = ( [