ci: shard package upgrade survivor baselines

This commit is contained in:
Vincent Koc
2026-05-04 20:53:11 -07:00
parent a91c17c426
commit 1c3b27718f
13 changed files with 276 additions and 43 deletions

View File

@@ -861,36 +861,24 @@ jobs:
runs-on: blacksmith-4vcpu-ubuntu-2404 runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 5 timeout-minutes: 5
outputs: outputs:
groups_json: ${{ steps.plan.outputs.groups_json }} groups_json: ${{ steps.groups.outputs.groups_json }}
steps: steps:
- name: Plan targeted Docker lane groups - name: Checkout trusted release harness
id: plan uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 1
- name: Build targeted Docker lane groups
id: groups
shell: bash shell: bash
env: env:
LANES: ${{ inputs.docker_lanes }} LANES: ${{ inputs.docker_lanes }}
GROUP_SIZE: ${{ inputs.targeted_docker_lane_group_size }} GROUP_SIZE: ${{ inputs.targeted_docker_lane_group_size }}
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ inputs.published_upgrade_survivor_baselines }}
run: | run: |
set -euo pipefail set -euo pipefail
groups_json="$( groups_json="$(node scripts/plan-targeted-docker-lane-groups.mjs)"
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
)"
echo "groups_json=${groups_json}" >> "$GITHUB_OUTPUT" echo "groups_json=${groups_json}" >> "$GITHUB_OUTPUT"
validate_docker_lanes: validate_docker_lanes:
@@ -957,7 +945,7 @@ jobs:
OPENCLAW_DOCKER_E2E_SELECTED_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }} OPENCLAW_DOCKER_E2E_SELECTED_SHA: ${{ needs.validate_selected_ref.outputs.selected_sha }}
OPENCLAW_CURRENT_PACKAGE_TGZ: .artifacts/docker-e2e-package/openclaw-current.tgz 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_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_UPGRADE_SURVIVOR_SCENARIOS: ${{ inputs.published_upgrade_survivor_scenarios }}
OPENCLAW_SKIP_DOCKER_BUILD: "1" OPENCLAW_SKIP_DOCKER_BUILD: "1"
INCLUDE_OPENWEBUI: ${{ inputs.include_openwebui }} INCLUDE_OPENWEBUI: ${{ inputs.include_openwebui }}
@@ -998,6 +986,7 @@ jobs:
shell: bash shell: bash
env: env:
LANES: ${{ matrix.group.docker_lanes }} LANES: ${{ matrix.group.docker_lanes }}
GROUP_LABEL: ${{ matrix.group.label }}
INCLUDE_OPENWEBUI: ${{ inputs.include_openwebui }} INCLUDE_OPENWEBUI: ${{ inputs.include_openwebui }}
INCLUDE_RELEASE_PATH_SUITES: ${{ inputs.include_release_path_suites }} INCLUDE_RELEASE_PATH_SUITES: ${{ inputs.include_release_path_suites }}
run: | run: |
@@ -1017,7 +1006,7 @@ jobs:
plan_path=".artifacts/docker-tests/targeted-plan.json" plan_path=".artifacts/docker-tests/targeted-plan.json"
node .release-harness/scripts/test-docker-all.mjs --plan-json > "$plan_path" 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" 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 "artifact_suffix=${suffix:-targeted}" >> "$GITHUB_OUTPUT"
echo "plan_json=$plan_path" >> "$GITHUB_OUTPUT" echo "plan_json=$plan_path" >> "$GITHUB_OUTPUT"

View File

@@ -559,7 +559,7 @@ jobs:
package_sha256: ${{ needs.prepare_release_package.outputs.package_sha256 }} package_sha256: ${{ needs.prepare_release_package.outputs.package_sha256 }}
suite_profile: custom suite_profile: custom
docker_lanes: doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor plugins-offline plugin-update 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' || '' }} published_upgrade_survivor_scenarios: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'reported-issues' || '' }}
telegram_mode: mock-openai 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 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

View File

@@ -70,7 +70,7 @@ on:
default: openclaw@latest default: openclaw@latest
type: string type: string
published_upgrade_survivor_baselines: 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 required: false
default: "" default: ""
type: string type: string
@@ -150,7 +150,7 @@ on:
default: openclaw@latest default: openclaw@latest
type: string type: string
published_upgrade_survivor_baselines: 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 required: false
default: "" default: ""
type: string type: string
@@ -442,7 +442,7 @@ jobs:
fi fi
releases_json="" releases_json=""
npm_versions_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" releases_json=".artifacts/package-candidate-input/openclaw-releases.json"
npm_versions_json=".artifacts/package-candidate-input/openclaw-npm-versions.json" npm_versions_json=".artifacts/package-candidate-input/openclaw-npm-versions.json"
mkdir -p "$(dirname "$releases_json")" mkdir -p "$(dirname "$releases_json")"

View File

@@ -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, Docker lanes, Package Acceptance inputs, release defaults, and failure triage,
see [Testing updates and plugins](/help/testing-updates-plugins). 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 ### Legacy compatibility windows

View File

@@ -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 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 ```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 published_upgrade_survivor_scenarios=reported-issues
telegram_mode=mock-openai telegram_mode=mock-openai
``` ```
This keeps package migration, update channel switching, stale plugin dependency This keeps package migration, update channel switching, stale plugin dependency
cleanup, offline plugin coverage, plugin update behavior, and Telegram package 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 update migration coverage, use `all-since-2026.4.23` in the separate Update
Migration workflow instead of Full Release CI. `release-history` remains Migration workflow instead of Full Release CI. `release-history` remains
available for manual wider sampling when you also want the legacy pre-date available for manual wider sampling when you also want the legacy pre-date
anchor. 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: Run a package profile manually when validating a candidate before release:
```bash ```bash
@@ -197,7 +208,7 @@ gh workflow run package-acceptance.yml \
-f source=npm \ -f source=npm \
-f package_spec=openclaw@beta \ -f package_spec=openclaw@beta \
-f suite_profile=package \ -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 published_upgrade_survivor_scenarios=reported-issues \
-f telegram_mode=mock-openai -f telegram_mode=mock-openai
``` ```

View File

@@ -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`. - 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. - 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. - 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. - 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`. - 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. - 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.

View File

@@ -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 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 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`. `run_release_soak=true`.
`OpenClaw Release Checks` uses the trusted workflow ref to resolve the target `OpenClaw Release Checks` uses the trusted workflow ref to resolve the target

View File

@@ -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: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: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: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: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. - `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.

View File

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

View File

@@ -128,6 +128,19 @@ export function resolveReleaseHistory(args) {
return dedupeSpecs(versions); 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) { export function resolveAllSince(args, minimumVersion) {
const releasesJson = args.get("releases-json"); const releasesJson = args.get("releases-json");
if (!releasesJson) { if (!releasesJson) {
@@ -149,11 +162,13 @@ export function resolveBaselines(args) {
if (requestedTokens.length === 0) { if (requestedTokens.length === 0) {
return dedupeSpecs([fallback]); return dedupeSpecs([fallback]);
} }
const exactTokens = [];
const resolved = []; const resolved = [];
for (const token of requestedTokens) { for (const token of requestedTokens) {
if (token === "release-history") { if (token === "release-history") {
resolved.push(...resolveReleaseHistory(args)); 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-")) { } else if (token.startsWith("all-since-")) {
const minimumVersion = token.slice("all-since-".length); const minimumVersion = token.slice("all-since-".length);
if (!parseStableVersion(minimumVersion)) { if (!parseStableVersion(minimumVersion)) {
@@ -161,10 +176,10 @@ export function resolveBaselines(args) {
} }
resolved.push(...resolveAllSince(args, minimumVersion)); resolved.push(...resolveAllSince(args, minimumVersion));
} else { } 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; const isMain = process.argv[1] ? fileURLToPath(import.meta.url) === process.argv[1] : false;

View File

@@ -92,12 +92,14 @@ describe("package acceptance workflow", () => {
expect(workflow).toContain("suite_profile:"); expect(workflow).toContain("suite_profile:");
expect(workflow).toContain("published_upgrade_survivor_baseline:"); expect(workflow).toContain("published_upgrade_survivor_baseline:");
expect(workflow).toContain("published_upgrade_survivor_baselines:"); expect(workflow).toContain("published_upgrade_survivor_baselines:");
expect(workflow).toContain("last-stable-4");
expect(workflow).toContain("all-since-2026.4.23"); expect(workflow).toContain("all-since-2026.4.23");
expect(workflow).toContain("published_upgrade_survivor_scenarios:"); expect(workflow).toContain("published_upgrade_survivor_scenarios:");
expect(workflow).toContain("scripts/resolve-upgrade-survivor-baselines.mjs"); expect(workflow).toContain("scripts/resolve-upgrade-survivor-baselines.mjs");
expect(workflow).toContain("--history-count 6"); expect(workflow).toContain("--history-count 6");
expect(workflow).toContain("--include-version 2026.4.23"); expect(workflow).toContain("--include-version 2026.4.23");
expect(workflow).toContain("--pre-date 2026-03-15T00:00:00Z"); expect(workflow).toContain("--pre-date 2026-03-15T00:00:00Z");
expect(workflow).toContain('"last-stable-"');
expect(workflow).toContain('"all-since-"'); expect(workflow).toContain('"all-since-"');
expect(workflow).toContain("npm-onboard-channel-agent gateway-network config-reload"); expect(workflow).toContain("npm-onboard-channel-agent gateway-network config-reload");
expect(workflow).toContain("npm-onboard-channel-agent doctor-switch"); 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 }}", "OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: ${{ inputs.published_upgrade_survivor_baseline }}",
); );
expect(workflow).toContain( 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( expect(workflow).toContain(
"OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS: ${{ inputs.published_upgrade_survivor_scenarios }}", "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("plan_docker_lane_groups:");
expect(workflow).toContain("targeted_docker_lane_group_size:"); 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("Docker E2E targeted lanes (${{ matrix.group.label }})");
expect(workflow).toContain("LANES: ${{ matrix.group.docker_lanes }}"); 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("DOCKER_E2E_LANES: ${{ matrix.group.docker_lanes }}");
expect(workflow).toContain("name: docker-e2e-${{ steps.plan.outputs.artifact_suffix }}"); expect(workflow).toContain("name: docker-e2e-${{ steps.plan.outputs.artifact_suffix }}");
expect(scheduler).toContain( 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", "docker_lanes: doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor plugins-offline plugin-update",
); );
expect(workflow).toContain( 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( expect(workflow).toContain(
"published_upgrade_survivor_scenarios: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'reported-issues' || '' }}", "published_upgrade_survivor_scenarios: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'reported-issues' || '' }}",

View File

@@ -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" },
]);
});
});

View File

@@ -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", () => { it("maps release-history anchors to npm-published package versions when GitHub tags have republish suffixes", () => {
const releases = ( const releases = (
[ [