ci: shard release validation reruns

This commit is contained in:
Peter Steinberger
2026-04-27 23:38:09 +01:00
parent d2320e4d4b
commit 39e3d8d31d
13 changed files with 295 additions and 58 deletions
+21 -2
View File
@@ -141,6 +141,11 @@ parent gate. If a child workflow failed but was later rerun successfully, rerun
only the failed parent verifier job; do not dispatch a new full umbrella unless
the release evidence is stale.
For bounded recovery after a focused fix, pass `-f rerun_group=<group>`.
Supported umbrella groups are `all`, `ci`, `release-checks`, `install-smoke`,
`cross-os`, `live-e2e`, `package`, `qa`, `qa-parity`, `qa-live`, and
`npm-telegram`. Use the narrowest group that covers the failed box.
### Release Evidence
After release-candidate validation or before a release decision, record the
@@ -192,9 +197,13 @@ gh workflow run openclaw-release-checks.yml \
--ref main \
-f ref=<branch-or-sha> \
-f provider=openai \
-f mode=both
-f mode=both \
-f rerun_group=all
```
Release-check rerun groups are `all`, `install-smoke`, `cross-os`, `live-e2e`,
`package`, `qa`, `qa-parity`, and `qa-live`.
### QA Lab Matrix Profiles
`pnpm openclaw qa matrix` defaults to `--profile all`. Do not assume the CLI
@@ -236,7 +245,9 @@ gh workflow run openclaw-live-and-e2e-checks-reusable.yml \
Useful knobs:
- `docker_lanes='<lane[,lane]>'`: run selected Docker scheduler lanes against
prepared artifacts instead of the release chunk matrix.
prepared artifacts instead of the release chunk matrix. Multiple selected
lanes fan out as parallel targeted Docker jobs after one shared package/image
preparation step.
- `include_live_suites=false`: skip live/provider suites when testing Docker
scheduler or release packaging only.
- `live_models_only=true`: run only Docker live model coverage.
@@ -244,6 +255,14 @@ Useful knobs:
targeted Docker live model job instead of the full provider matrix.
- blank `live_model_providers`: run the full live-model provider matrix.
Release-path Docker chunks are currently `core`, `package-update-openai`,
`package-update-anthropic`, `package-update-core`, `plugins-runtime-core`,
`plugins-runtime-install-a`, `plugins-runtime-install-b`,
`bundled-channels-core`, `bundled-channels-update-a`,
`bundled-channels-update-b`, and `bundled-channels-contracts`. The aggregate
`bundled-channels` chunk remains valid for manual one-shot reruns, but release
checks use the split chunks.
When live suites are enabled, the workflow shards broad native `pnpm test:live`
coverage through `scripts/test-live-shard.mjs` instead of one serial `live-all`
job:
+59 -6
View File
@@ -26,6 +26,23 @@ on:
- fresh
- upgrade
- both
rerun_group:
description: Validation group to run
required: false
default: all
type: choice
options:
- all
- ci
- release-checks
- install-smoke
- cross-os
- live-e2e
- package
- qa
- qa-parity
- qa-live
- npm-telegram
npm_telegram_package_spec:
description: Optional published package spec for the post-publish Telegram E2E lane
required: false
@@ -89,6 +106,7 @@ jobs:
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }}
EVIDENCE_PACKAGE_SPEC: ${{ inputs.evidence_package_spec }}
RERUN_GROUP: ${{ inputs.rerun_group }}
run: |
{
echo "## Full release validation"
@@ -96,8 +114,17 @@ jobs:
echo "- Target ref: \`${TARGET_REF}\`"
echo "- Target SHA: \`${TARGET_SHA}\`"
echo "- Child workflow ref: \`${CHILD_WORKFLOW_REF}\`"
echo "- Normal CI: \`CI\` with \`target_ref=${TARGET_SHA}\`"
echo "- Release/live/Docker/package/QA: \`OpenClaw Release Checks\`"
echo "- Rerun group: \`${RERUN_GROUP}\`"
if [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "ci" ]]; then
echo "- Normal CI: \`CI\` with \`target_ref=${TARGET_SHA}\`"
else
echo "- Normal CI: skipped by rerun group"
fi
if [[ "$RERUN_GROUP" != "ci" && "$RERUN_GROUP" != "npm-telegram" ]]; then
echo "- Release/live/Docker/package/QA: \`OpenClaw Release Checks\`"
else
echo "- Release/live/Docker/package/QA: skipped by rerun group"
fi
if [[ -n "${NPM_TELEGRAM_PACKAGE_SPEC// }" ]]; then
echo "- Post-publish Telegram E2E: \`${NPM_TELEGRAM_PACKAGE_SPEC}\`"
else
@@ -111,6 +138,7 @@ jobs:
normal_ci:
name: Run normal full CI
needs: [resolve_target]
if: contains(fromJSON('["all","ci"]'), inputs.rerun_group)
runs-on: ubuntu-24.04
timeout-minutes: 240
outputs:
@@ -194,6 +222,7 @@ jobs:
release_checks:
name: Run release/live/Docker/QA validation
needs: [resolve_target]
if: contains(fromJSON('["all","release-checks","install-smoke","cross-os","live-e2e","package","qa","qa-parity","qa-live"]'), inputs.rerun_group)
runs-on: ubuntu-24.04
timeout-minutes: 720
outputs:
@@ -210,6 +239,7 @@ jobs:
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
PROVIDER: ${{ inputs.provider }}
MODE: ${{ inputs.mode }}
RERUN_GROUP: ${{ inputs.rerun_group }}
run: |
set -euo pipefail
@@ -274,17 +304,24 @@ jobs:
echo "- Target SHA: \`${TARGET_SHA}\`"
echo "- Provider: \`${PROVIDER}\`"
echo "- Cross-OS mode: \`${MODE}\`"
echo "- Rerun group: \`${RERUN_GROUP}\`"
} >> "$GITHUB_STEP_SUMMARY"
child_rerun_group="$RERUN_GROUP"
if [[ "$child_rerun_group" == "release-checks" ]]; then
child_rerun_group=all
fi
dispatch_and_wait openclaw-release-checks.yml \
-f ref="$TARGET_SHA" \
-f provider="$PROVIDER" \
-f mode="$MODE"
-f mode="$MODE" \
-f rerun_group="$child_rerun_group"
npm_telegram:
name: Run post-publish Telegram E2E
needs: [resolve_target]
if: inputs.npm_telegram_package_spec != ''
if: inputs.npm_telegram_package_spec != '' && contains(fromJSON('["all","npm-telegram"]'), inputs.rerun_group)
runs-on: ubuntu-24.04
timeout-minutes: 120
outputs:
@@ -363,8 +400,13 @@ jobs:
TARGET_REF: ${{ inputs.ref }}
PACKAGE_SPEC: ${{ inputs.evidence_package_spec || inputs.npm_telegram_package_spec }}
GITHUB_RUN_ID_VALUE: ${{ github.run_id }}
RELEASE_CHECKS_RESULT: ${{ needs.release_checks.result }}
run: |
set -euo pipefail
if [[ "$RELEASE_CHECKS_RESULT" == "skipped" ]]; then
echo "Release checks were skipped by rerun group; skipping automatic private evidence update."
exit 0
fi
if [[ -z "${RELEASE_PRIVATE_DISPATCH_TOKEN// }" ]]; then
echo "OPENCLAW_RELEASES_PRIVATE_DISPATCH_TOKEN is not configured; skipping automatic private evidence update."
exit 0
@@ -414,6 +456,8 @@ jobs:
NORMAL_CI_RUN_ID: ${{ needs.normal_ci.outputs.run_id }}
RELEASE_CHECKS_RUN_ID: ${{ needs.release_checks.outputs.run_id }}
NPM_TELEGRAM_RUN_ID: ${{ needs.npm_telegram.outputs.run_id }}
NORMAL_CI_RESULT: ${{ needs.normal_ci.result }}
RELEASE_CHECKS_RESULT: ${{ needs.release_checks.result }}
NPM_TELEGRAM_RESULT: ${{ needs.npm_telegram.result }}
run: |
set -euo pipefail
@@ -448,8 +492,17 @@ jobs:
failed=0
check_child "normal_ci" "$NORMAL_CI_RUN_ID" 1 || failed=1
check_child "release_checks" "$RELEASE_CHECKS_RUN_ID" 1 || failed=1
if [[ "$NORMAL_CI_RESULT" == "skipped" && -z "${NORMAL_CI_RUN_ID// }" ]]; then
check_child "normal_ci" "" 0 || failed=1
else
check_child "normal_ci" "$NORMAL_CI_RUN_ID" 1 || failed=1
fi
if [[ "$RELEASE_CHECKS_RESULT" == "skipped" && -z "${RELEASE_CHECKS_RUN_ID// }" ]]; then
check_child "release_checks" "" 0 || failed=1
else
check_child "release_checks" "$RELEASE_CHECKS_RUN_ID" 1 || failed=1
fi
if [[ "$NPM_TELEGRAM_RESULT" == "skipped" && -z "${NPM_TELEGRAM_RUN_ID// }" ]]; then
check_child "npm_telegram" "" 0 || failed=1
@@ -450,9 +450,18 @@ jobs:
- chunk_id: plugins-runtime-install-b
label: plugins/runtime install B
timeout_minutes: 180
- chunk_id: bundled-channels
label: bundled channels
timeout_minutes: 180
- chunk_id: bundled-channels-core
label: bundled channels core
timeout_minutes: 90
- chunk_id: bundled-channels-update-a
label: bundled channels update A
timeout_minutes: 90
- chunk_id: bundled-channels-update-b
label: bundled channels update B
timeout_minutes: 90
- chunk_id: bundled-channels-contracts
label: bundled channels contracts
timeout_minutes: 90
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
@@ -629,12 +638,43 @@ jobs:
path: .artifacts/docker-tests/
if-no-files-found: ignore
validate_docker_lanes:
needs: [validate_selected_ref, prepare_docker_e2e_image]
plan_docker_lane_groups:
needs: validate_selected_ref
if: inputs.docker_lanes != ''
name: Docker E2E targeted lanes
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
groups_json: ${{ steps.plan.outputs.groups_json }}
steps:
- name: Plan targeted Docker lane groups
id: plan
shell: bash
env:
LANES: ${{ inputs.docker_lanes }}
run: |
set -euo pipefail
groups_json="$(
LANES="$LANES" 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 sanitize = (lane) => lane.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "targeted";
process.stdout.write(JSON.stringify(lanes.map((lane) => ({ label: sanitize(lane), docker_lanes: lane }))));
NODE
)"
echo "groups_json=${groups_json}" >> "$GITHUB_OUTPUT"
validate_docker_lanes:
needs: [validate_selected_ref, prepare_docker_e2e_image, plan_docker_lane_groups]
if: inputs.docker_lanes != ''
name: Docker E2E targeted lanes (${{ matrix.group.label }})
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 180
strategy:
fail-fast: false
matrix:
group: ${{ fromJson(needs.plan_docker_lane_groups.outputs.groups_json) }}
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
@@ -688,7 +728,7 @@ jobs:
OPENCLAW_CURRENT_PACKAGE_TGZ: .artifacts/docker-e2e-package/openclaw-current.tgz
OPENCLAW_SKIP_DOCKER_BUILD: "1"
INCLUDE_OPENWEBUI: ${{ inputs.include_openwebui }}
DOCKER_E2E_LANES: ${{ inputs.docker_lanes }}
DOCKER_E2E_LANES: ${{ matrix.group.docker_lanes }}
steps:
- name: Checkout selected ref
uses: actions/checkout@v6
@@ -733,6 +773,8 @@ jobs:
plan_path=".artifacts/docker-tests/targeted-plan.json"
node scripts/test-docker-all.mjs --plan-json > "$plan_path"
node 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/-$//')"
echo "artifact_suffix=${suffix:-targeted}" >> "$GITHUB_OUTPUT"
echo "plan_json=$plan_path" >> "$GITHUB_OUTPUT"
- name: Download OpenClaw Docker E2E package
@@ -782,8 +824,8 @@ jobs:
export OPENCLAW_DOCKER_ALL_PREFLIGHT=0
export OPENCLAW_DOCKER_ALL_FAIL_FAST=0
export OPENCLAW_DOCKER_ALL_INCLUDE_OPENWEBUI="${INCLUDE_OPENWEBUI}"
export OPENCLAW_DOCKER_ALL_LOG_DIR=".artifacts/docker-tests/targeted"
export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/targeted-timings.json"
export OPENCLAW_DOCKER_ALL_LOG_DIR=".artifacts/docker-tests/targeted-${{ steps.plan.outputs.artifact_suffix }}"
export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/targeted-${{ steps.plan.outputs.artifact_suffix }}-timings.json"
export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)"
if [[ "${{ steps.plan.outputs.needs_live_image }}" == "1" ]]; then
pnpm test:docker:live-build
@@ -797,7 +839,7 @@ jobs:
shell: bash
run: |
set -euo pipefail
summary=".artifacts/docker-tests/targeted/summary.json"
summary=".artifacts/docker-tests/targeted-${{ steps.plan.outputs.artifact_suffix }}/summary.json"
if [[ ! -f "$summary" ]]; then
echo "Docker targeted summary missing: \`$summary\`" >> "$GITHUB_STEP_SUMMARY"
exit 0
@@ -808,7 +850,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v7
with:
name: docker-e2e-targeted
name: docker-e2e-${{ steps.plan.outputs.artifact_suffix }}
path: .artifacts/docker-tests/
if-no-files-found: ignore
@@ -25,6 +25,20 @@ on:
- fresh
- upgrade
- both
rerun_group:
description: Release check group to run
required: false
default: all
type: choice
options:
- all
- install-smoke
- cross-os
- live-e2e
- package
- qa
- qa-parity
- qa-live
concurrency:
group: openclaw-release-checks-${{ inputs.ref }}
@@ -47,6 +61,7 @@ jobs:
sha: ${{ steps.ref.outputs.sha }}
provider: ${{ steps.inputs.outputs.provider }}
mode: ${{ steps.inputs.outputs.mode }}
rerun_group: ${{ steps.inputs.outputs.rerun_group }}
steps:
- name: Require main or release workflow ref for release checks
env:
@@ -105,12 +120,14 @@ jobs:
RELEASE_REF_INPUT: ${{ inputs.ref }}
RELEASE_PROVIDER_INPUT: ${{ inputs.provider }}
RELEASE_MODE_INPUT: ${{ inputs.mode }}
RELEASE_RERUN_GROUP_INPUT: ${{ inputs.rerun_group }}
run: |
set -euo pipefail
{
printf 'ref=%s\n' "$RELEASE_REF_INPUT"
printf 'provider=%s\n' "$RELEASE_PROVIDER_INPUT"
printf 'mode=%s\n' "$RELEASE_MODE_INPUT"
printf 'rerun_group=%s\n' "$RELEASE_RERUN_GROUP_INPUT"
} >> "$GITHUB_OUTPUT"
- name: Summarize validated ref
@@ -119,6 +136,7 @@ jobs:
RELEASE_SHA: ${{ steps.ref.outputs.sha }}
RELEASE_PROVIDER: ${{ inputs.provider }}
RELEASE_MODE: ${{ inputs.mode }}
RELEASE_RERUN_GROUP: ${{ inputs.rerun_group }}
run: |
{
echo "## Release checks"
@@ -127,11 +145,13 @@ jobs:
echo "- Validated SHA: \`${RELEASE_SHA}\`"
echo "- Cross-OS provider: \`${RELEASE_PROVIDER}\`"
echo "- Cross-OS mode: \`${RELEASE_MODE}\`"
echo "- Rerun group: \`${RELEASE_RERUN_GROUP}\`"
echo "- This run will execute cross-OS release validation, install smoke, QA Lab parity, Matrix, and Telegram lanes, and the non-Parallels Docker/live/openwebui coverage from the CI migration plan."
} >> "$GITHUB_STEP_SUMMARY"
install_smoke_release_checks:
needs: [resolve_target]
if: contains(fromJSON('["all","install-smoke"]'), needs.resolve_target.outputs.rerun_group)
permissions:
contents: read
uses: ./.github/workflows/install-smoke.yml
@@ -141,6 +161,7 @@ jobs:
cross_os_release_checks:
needs: [resolve_target]
if: contains(fromJSON('["all","cross-os"]'), needs.resolve_target.outputs.rerun_group)
permissions: read-all
uses: ./.github/workflows/openclaw-cross-os-release-checks-reusable.yml
with:
@@ -157,6 +178,7 @@ jobs:
live_and_e2e_release_checks:
needs: [resolve_target]
if: contains(fromJSON('["all","live-e2e"]'), needs.resolve_target.outputs.rerun_group)
permissions:
actions: read
contents: read
@@ -218,6 +240,7 @@ jobs:
package_acceptance_release_checks:
name: Run package acceptance
needs: [resolve_target]
if: contains(fromJSON('["all","package"]'), needs.resolve_target.outputs.rerun_group)
permissions:
actions: read
contents: read
@@ -282,6 +305,7 @@ jobs:
qa_lab_parity_release_checks:
name: Run QA Lab parity gate
needs: [resolve_target]
if: contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group)
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 30
permissions:
@@ -356,6 +380,7 @@ jobs:
qa_live_matrix_release_checks:
name: Run QA Lab live Matrix lane
needs: [resolve_target]
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group)
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
permissions:
@@ -434,6 +459,7 @@ jobs:
qa_live_telegram_release_checks:
name: Run QA Lab live Telegram lane
needs: [resolve_target]
if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group)
runs-on: blacksmith-32vcpu-ubuntu-2404
timeout-minutes: 60
permissions:
+13 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -607,7 +607,7 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or
Set `OPENCLAW_PLUGINS_E2E_CLAWHUB=0` to skip the live ClawHub block, or override the default package with `OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC` and `OPENCLAW_PLUGINS_E2E_CLAWHUB_ID`.
- Plugin update unchanged smoke: `pnpm test:docker:plugin-update` (script: `scripts/e2e/plugin-update-unchanged-docker.sh`)
- Config reload metadata smoke: `pnpm test:docker:config-reload` (script: `scripts/e2e/config-reload-source-docker.sh`)
- Bundled plugin runtime deps: `pnpm test:docker:bundled-channel-deps` builds a small Docker runner image by default, builds and packs OpenClaw once on the host, then mounts that tarball into each Linux install scenario. Reuse the image with `OPENCLAW_SKIP_DOCKER_BUILD=1`, skip the host rebuild after a fresh local build with `OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD=0`, or point at an existing tarball with `OPENCLAW_CURRENT_PACKAGE_TGZ=/path/to/openclaw-*.tgz`. The full Docker aggregate and release-path `bundled-channels` chunk pre-pack this tarball once, then shard bundled channel checks into independent lanes, including separate update lanes for Telegram, Discord, Slack, Feishu, memory-lancedb, and ACPX. The release workflow also splits provider installer chunks and bundled plugin install/uninstall chunks; legacy `package-update`, `plugins-runtime`, and `plugins-integrations` chunks remain aggregate aliases for manual reruns. Use `OPENCLAW_BUNDLED_CHANNELS=telegram,slack` to narrow the channel matrix when running the bundled lane directly, or `OPENCLAW_BUNDLED_CHANNEL_UPDATE_TARGETS=telegram,acpx` to narrow the update scenario. The lane also verifies that `channels.<id>.enabled=false` and `plugins.entries.<id>.enabled=false` suppress doctor/runtime-dependency repair.
- Bundled plugin runtime deps: `pnpm test:docker:bundled-channel-deps` builds a small Docker runner image by default, builds and packs OpenClaw once on the host, then mounts that tarball into each Linux install scenario. Reuse the image with `OPENCLAW_SKIP_DOCKER_BUILD=1`, skip the host rebuild after a fresh local build with `OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD=0`, or point at an existing tarball with `OPENCLAW_CURRENT_PACKAGE_TGZ=/path/to/openclaw-*.tgz`. The full Docker aggregate and release-path bundled-channel chunks pre-pack this tarball once, then shard bundled channel checks into independent lanes, including separate update lanes for Telegram, Discord, Slack, Feishu, memory-lancedb, and ACPX. Release chunks split channel smokes, update targets, and setup/runtime contracts into `bundled-channels-core`, `bundled-channels-update-a`, `bundled-channels-update-b`, and `bundled-channels-contracts`; the aggregate `bundled-channels` chunk remains available for manual reruns. The release workflow also splits provider installer chunks and bundled plugin install/uninstall chunks; legacy `package-update`, `plugins-runtime`, and `plugins-integrations` chunks remain aggregate aliases for manual reruns. Use `OPENCLAW_BUNDLED_CHANNELS=telegram,slack` to narrow the channel matrix when running the bundled lane directly, or `OPENCLAW_BUNDLED_CHANNEL_UPDATE_TARGETS=telegram,acpx` to narrow the update scenario. The lane also verifies that `channels.<id>.enabled=false` and `plugins.entries.<id>.enabled=false` suppress doctor/runtime-dependency repair.
- Narrow bundled plugin runtime deps while iterating by disabling unrelated scenarios, for example:
`OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=0 pnpm test:docker:bundled-channel-deps`.
+11 -4
View File
@@ -290,6 +290,12 @@ stale. The umbrella's final verifier re-checks the recorded child workflow run
ids, so after a child workflow is rerun successfully, rerun only the failed
`Verify full validation` parent job.
For bounded recovery, pass `rerun_group` to the umbrella. `all` is the real
release-candidate run, `ci` runs only the normal CI child, `release-checks` runs
every release box, and the narrower release groups are `install-smoke`,
`cross-os`, `live-e2e`, `package`, `qa`, `qa-parity`, `qa-live`, and
`npm-telegram` when the standalone package Telegram lane is supplied.
### Vitest
The Vitest box is the manual `CI` child workflow. Manual CI intentionally
@@ -327,11 +333,12 @@ Release Docker coverage includes:
- repository E2E lanes
- release-path Docker chunks: `core`, `package-update-openai`,
`package-update-anthropic`, `package-update-core`, `plugins-runtime-core`,
`plugins-runtime-install-a`, `plugins-runtime-install-b`, and
`bundled-channels`
`plugins-runtime-install-a`, `plugins-runtime-install-b`,
`bundled-channels-core`, `bundled-channels-update-a`,
`bundled-channels-update-b`, and `bundled-channels-contracts`
- OpenWebUI coverage inside the `plugins-runtime-core` chunk when requested
- split bundled-channel dependency lanes in their own `bundled-channels` chunk
instead of the serial all-in-one bundled-channel lane
- split bundled-channel dependency lanes across channel-smoke, update-target,
and setup/runtime contract chunks instead of one large bundled-channel job
- split bundled plugin install/uninstall lanes
`bundled-plugin-install-uninstall-0` through
`bundled-plugin-install-uninstall-7`
+38 -10
View File
@@ -107,20 +107,30 @@ function bundledChannelScenarioLane(name, env, options = {}) {
);
}
const bundledScenarioLanes = [
...["telegram", "discord", "slack", "feishu", "memory-lancedb"].map((channel) =>
const bundledChannelSmokeLanes = ["telegram", "discord", "slack", "feishu", "memory-lancedb"].map(
(channel) =>
npmLane(
`bundled-channel-${channel}`,
`OPENCLAW_BUNDLED_CHANNELS=${channel} ${bundledChannelLaneCommand}`,
),
);
const bundledChannelUpdateLanes = [
"telegram",
"discord",
"slack",
"feishu",
"memory-lancedb",
"acpx",
].map((target) =>
bundledChannelScenarioLane(
`bundled-channel-update-${target}`,
`OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=1 OPENCLAW_BUNDLED_CHANNEL_UPDATE_TARGETS=${target} OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_DISABLED_CONFIG_SCENARIO=0`,
{ retryPatterns: LIVE_RETRY_PATTERNS, retries: 1, timeoutMs: BUNDLED_UPDATE_TIMEOUT_MS },
),
...["telegram", "discord", "slack", "feishu", "memory-lancedb", "acpx"].map((target) =>
bundledChannelScenarioLane(
`bundled-channel-update-${target}`,
`OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=1 OPENCLAW_BUNDLED_CHANNEL_UPDATE_TARGETS=${target} OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_DISABLED_CONFIG_SCENARIO=0`,
{ timeoutMs: BUNDLED_UPDATE_TIMEOUT_MS },
),
),
);
const bundledChannelContractLanes = [
bundledChannelScenarioLane(
"bundled-channel-root-owned",
"OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=1 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_DISABLED_CONFIG_SCENARIO=0",
@@ -139,6 +149,12 @@ const bundledScenarioLanes = [
),
];
const bundledScenarioLanes = [
...bundledChannelSmokeLanes,
...bundledChannelUpdateLanes,
...bundledChannelContractLanes,
];
const bundledPluginInstallUninstallLanes = Array.from(
{ length: BUNDLED_PLUGIN_INSTALL_UNINSTALL_SHARDS },
(_, index) =>
@@ -465,7 +481,18 @@ const primaryReleasePathChunks = {
"plugins-runtime-core": releasePathPluginRuntimeCoreLanes,
"plugins-runtime-install-a": bundledPluginInstallUninstallLanes.slice(0, 4),
"plugins-runtime-install-b": bundledPluginInstallUninstallLanes.slice(4),
"bundled-channels": releasePathBundledChannelLanes,
"bundled-channels-core": [releasePathBundledChannelLanes[0], ...bundledChannelSmokeLanes],
"bundled-channels-update-a": [
bundledChannelUpdateLanes[0],
bundledChannelUpdateLanes[1],
bundledChannelUpdateLanes[4],
],
"bundled-channels-update-b": [
bundledChannelUpdateLanes[2],
bundledChannelUpdateLanes[3],
bundledChannelUpdateLanes[5],
],
"bundled-channels-contracts": bundledChannelContractLanes,
openwebui: [],
};
@@ -477,6 +504,7 @@ const legacyReleasePathChunks = {
],
"plugins-runtime": releasePathPluginRuntimeLanes,
"plugins-integrations": [...releasePathPluginRuntimeLanes, ...releasePathBundledChannelLanes],
"bundled-channels": releasePathBundledChannelLanes,
};
function openWebUILane() {
+1 -9
View File
@@ -69,7 +69,6 @@ const BUNDLED_RUNTIME_DEPS_OWNERLESS_LOCK_STALE_MS = 30_000;
const BUNDLED_RUNTIME_DEPS_INSTALL_PROGRESS_INTERVAL_MS = 5_000;
const BUNDLED_RUNTIME_MIRROR_MATERIALIZED_EXTENSIONS = new Set([".cjs", ".js", ".mjs"]);
const BUNDLED_EXTENSION_DIST_DIR = "extensions";
const BUNDLED_RUNTIME_MIRROR_PLUGIN_REGION_RE = /(?:^|\n)\/\/#region extensions\/[^/\s]+(?:\/|$)/u;
const MIRRORED_CORE_RUNTIME_DEP_NAMES = ["tslog"] as const;
const MIRRORED_PACKAGE_RUNTIME_DEP_PLUGIN_ID = "openclaw-core";
@@ -82,14 +81,7 @@ export type BundledRuntimeDepsNpmRunner = {
};
export function shouldMaterializeBundledRuntimeMirrorDistFile(sourcePath: string): boolean {
if (!BUNDLED_RUNTIME_MIRROR_MATERIALIZED_EXTENSIONS.has(path.extname(sourcePath))) {
return false;
}
try {
return BUNDLED_RUNTIME_MIRROR_PLUGIN_REGION_RE.test(fs.readFileSync(sourcePath, "utf8"));
} catch {
return false;
}
return BUNDLED_RUNTIME_MIRROR_MATERIALIZED_EXTENSIONS.has(path.extname(sourcePath));
}
export function materializeBundledRuntimeMirrorDistFile(
+9 -1
View File
@@ -20,7 +20,7 @@ afterEach(() => {
});
describe("prepareBundledPluginRuntimeRoot", () => {
it("materializes plugin-owned root chunks in external mirrors", () => {
it("materializes root JavaScript chunks in external mirrors", () => {
const packageRoot = makeTempRoot();
const stageDir = makeTempRoot();
const pluginRoot = path.join(packageRoot, "dist", "extensions", "browser");
@@ -42,6 +42,11 @@ describe("prepareBundledPluginRuntimeRoot", () => {
].join("\n"),
"utf8",
);
fs.writeFileSync(
path.join(packageRoot, "dist", "shared-runtime.js"),
"export const shared = 'mirrored-without-region';\n",
"utf8",
);
fs.writeFileSync(
path.join(pluginRoot, "index.js"),
`import { marker } from "../../pw-ai.js"; export default { id: "browser", marker };\n`,
@@ -106,6 +111,9 @@ describe("prepareBundledPluginRuntimeRoot", () => {
expect(fs.existsSync(staleMirrorChunk)).toBe(true);
expect(fs.lstatSync(staleMirrorChunk).isSymbolicLink()).toBe(false);
expect(fs.readFileSync(staleMirrorChunk, "utf8")).toContain("playwright-core");
expect(fs.lstatSync(path.join(installRoot, "dist", "shared-runtime.js")).isSymbolicLink()).toBe(
false,
);
});
it("does not copy staged runtime mirror dist files onto themselves", () => {
+9 -1
View File
@@ -1714,7 +1714,7 @@ module.exports = {
expect(registry?.plugins.find((entry) => entry.id === "alpha")?.status).toBe("loaded");
});
it("materializes plugin-owned root chunks in external runtime mirrors", () => {
it("materializes root JavaScript chunks in external runtime mirrors", () => {
const packageRoot = makeTempDir();
const stageDir = makeTempDir();
const bundledDir = path.join(packageRoot, "dist", "extensions");
@@ -1736,6 +1736,11 @@ module.exports = {
].join("\n"),
"utf-8",
);
fs.writeFileSync(
path.join(packageRoot, "dist", "shared-runtime.js"),
"export const shared = 'mirrored-without-region';\n",
"utf-8",
);
fs.writeFileSync(
path.join(pluginRoot, "index.js"),
[
@@ -1829,6 +1834,9 @@ module.exports = {
expect(reloadedRegistry.plugins.find((entry) => entry.id === "browser")?.status).toBe("loaded");
expect(fs.existsSync(stagedMirrorChunk)).toBe(true);
expect(
fs.lstatSync(path.join(actualInstallRoot, "dist", "shared-runtime.js")).isSymbolicLink(),
).toBe(false);
});
it("loads bundled plugins with plugin-sdk imports from an external stage dir", () => {
+43 -11
View File
@@ -104,10 +104,25 @@ describe("scripts/lib/docker-e2e-plan", () => {
profile: RELEASE_PATH_PROFILE,
releaseChunk: "plugins-runtime-install-b",
});
const bundledChannels = planFor({
const bundledChannelsCore = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "bundled-channels",
releaseChunk: "bundled-channels-core",
});
const bundledChannelsUpdateA = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "bundled-channels-update-a",
});
const bundledChannelsUpdateB = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "bundled-channels-update-b",
});
const bundledChannelsContracts = planFor({
includeOpenWebUI: true,
profile: RELEASE_PATH_PROFILE,
releaseChunk: "bundled-channels-contracts",
});
expect(packageInstallOpenAi.lanes.map((lane) => lane.name)).toEqual(["install-e2e-openai"]);
@@ -142,15 +157,32 @@ describe("scripts/lib/docker-e2e-plan", () => {
"bundled-plugin-install-uninstall-6",
"bundled-plugin-install-uninstall-7",
]);
expect(bundledChannels.lanes.map((lane) => lane.name)).toEqual(
expect.arrayContaining([
"plugin-update",
"bundled-channel-telegram",
"bundled-channel-update-acpx",
]),
);
expect(bundledChannels.lanes.map((lane) => lane.name)).not.toContain("plugins");
expect(bundledChannels.lanes.map((lane) => lane.name)).not.toContain("openwebui");
expect(bundledChannelsCore.lanes.map((lane) => lane.name)).toEqual([
"plugin-update",
"bundled-channel-telegram",
"bundled-channel-discord",
"bundled-channel-slack",
"bundled-channel-feishu",
"bundled-channel-memory-lancedb",
]);
expect(bundledChannelsUpdateA.lanes.map((lane) => lane.name)).toEqual([
"bundled-channel-update-telegram",
"bundled-channel-update-discord",
"bundled-channel-update-memory-lancedb",
]);
expect(bundledChannelsUpdateB.lanes.map((lane) => lane.name)).toEqual([
"bundled-channel-update-slack",
"bundled-channel-update-feishu",
"bundled-channel-update-acpx",
]);
expect(bundledChannelsContracts.lanes.map((lane) => lane.name)).toEqual([
"bundled-channel-root-owned",
"bundled-channel-setup-entry",
"bundled-channel-load-failure",
"bundled-channel-disabled-config",
]);
expect(bundledChannelsCore.lanes.map((lane) => lane.name)).not.toContain("plugins");
expect(bundledChannelsUpdateA.lanes.map((lane) => lane.name)).not.toContain("openwebui");
});
it("keeps legacy release chunk names as aggregate aliases", () => {
@@ -79,6 +79,10 @@ describe("package artifact reuse", () => {
expect(workflow).not.toContain("uses: ./.github/actions/docker-e2e-plan");
expect(workflow).toContain("node scripts/test-docker-all.mjs --plan-json");
expect(workflow).toContain("node scripts/docker-e2e.mjs github-outputs");
expect(workflow).toContain("plan_docker_lane_groups:");
expect(workflow).toContain("Docker E2E targeted lanes (${{ matrix.group.label }})");
expect(workflow).toContain("DOCKER_E2E_LANES: ${{ matrix.group.docker_lanes }}");
expect(workflow).toContain("name: docker-e2e-${{ steps.plan.outputs.artifact_suffix }}");
});
it("uses Blacksmith Docker build caching for prepared E2E images", () => {
@@ -126,7 +130,7 @@ describe("package artifact reuse", () => {
expect(workflow).toContain("package_acceptance_release_checks:");
expect(workflow).toContain(
"live_and_e2e_release_checks:\n needs: [resolve_target]\n permissions:\n actions: read",
'live_and_e2e_release_checks:\n needs: [resolve_target]\n if: contains(fromJSON(\'["all","live-e2e"]\'), needs.resolve_target.outputs.rerun_group)',
);
expect(workflow).toContain("uses: ./.github/workflows/package-acceptance.yml");
expect(workflow).toContain("package_ref: ${{ needs.resolve_target.outputs.ref }}");
@@ -141,6 +145,9 @@ describe("package artifact reuse", () => {
expect(workflow).toContain(
"OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}",
);
expect(workflow).toContain("rerun_group:");
expect(workflow).toContain("- live-e2e");
expect(workflow).toContain("- qa-live");
});
it("detects Matrix fail-fast support for older release refs", () => {
@@ -177,6 +184,9 @@ describe("package artifact reuse", () => {
'gh workflow run npm-telegram-beta-e2e.yml --ref "$CHILD_WORKFLOW_REF" "${args[@]}"',
);
expect(workflow).toContain('-f harness_ref="$TARGET_SHA"');
expect(workflow).toContain("child_rerun_group=all");
expect(workflow).toContain('-f rerun_group="$child_rerun_group"');
expect(workflow).toContain("NORMAL_CI_RESULT: ${{ needs.normal_ci.result }}");
expect(workflow).not.toContain("workflow_ref:");
expect(workflow).not.toContain("inputs.workflow_ref");
});