mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
ci: shard release validation reruns
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
File diff suppressed because one or more lines are too long
@@ -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`.
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user