From a811e164e3af0b1d5c50c35b8628c6d73d1d6f57 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 09:02:00 +0100 Subject: [PATCH] ci: speed up full release validation --- .agents/skills/openclaw-testing/SKILL.md | 48 +++-- .github/workflows/full-release-validation.yml | 17 +- ...nclaw-cross-os-release-checks-reusable.yml | 102 ++++++++++- .../openclaw-live-and-e2e-checks-reusable.yml | 19 +- .github/workflows/openclaw-release-checks.yml | 93 ++++++++-- docs/ci.md | 4 +- scripts/github/resolve-openclaw-ref.sh | 166 ++++++++++++++++++ scripts/lib/docker-e2e-scenarios.mjs | 23 ++- scripts/openclaw-cross-os-release-checks.ts | 2 +- test/scripts/docker-e2e-plan.test.ts | 41 +++-- 10 files changed, 456 insertions(+), 59 deletions(-) create mode 100755 scripts/github/resolve-openclaw-ref.sh diff --git a/.agents/skills/openclaw-testing/SKILL.md b/.agents/skills/openclaw-testing/SKILL.md index c37401c5b85..7f447d49df6 100644 --- a/.agents/skills/openclaw-testing/SKILL.md +++ b/.agents/skills/openclaw-testing/SKILL.md @@ -134,8 +134,10 @@ workflow ref input; choose the trusted harness by choosing the workflow run ref. Use `release_profile=minimum|stable|full` to control live/provider breadth: `minimum` keeps the fastest OpenAI/core release-critical set, `stable` adds the stable provider/backend set, and `full` adds the broad advisory provider/media -matrix. The parent verifier job appends slowest-job tables for child runs; rerun -only that verifier after a child rerun turns green. +matrix. Do not make `full` faster by silently dropping suites; optimize setup, +artifact reuse, and sharding instead. The parent verifier job appends +slowest-job tables for child runs; rerun only that verifier after a child rerun +turns green. If a full run is already active on a newer `origin/main`, prefer watching that run over dispatching a duplicate. If you accidentally dispatch a stale duplicate, @@ -211,8 +213,17 @@ gh workflow run openclaw-release-checks.yml \ Release-check rerun groups are `all`, `install-smoke`, `cross-os`, `live-e2e`, `package`, `qa`, `qa-parity`, and `qa-live`. `OpenClaw Release Checks` uses the trusted workflow ref to resolve the selected -ref once as `release-package-under-test` and passes that artifact into both -release-path Docker live/E2E checks and Package Acceptance. +ref once as `release-package-under-test` and passes that artifact into cross-OS +release checks, release-path Docker live/E2E checks, and Package Acceptance. +When `Full Release Validation` dispatches release checks, it passes the requested +branch/tag plus an `expected_sha` so branch/tag refs resolve through the fast +remote-ref path while the package and QA jobs still validate the exact SHA. + +The release Docker path intentionally shards the plugin/runtime tail. The +workflow uses `plugins-runtime-plugins`, `plugins-runtime-services`, and +`plugins-runtime-install-a` through `plugins-runtime-install-d`; aggregate +aliases such as `plugins-runtime-core`, `plugins-runtime`, and +`plugins-integrations` remain for manual reruns. The release QA parity box is internally split into candidate and baseline lane jobs, followed by a report job that downloads both artifacts and runs @@ -272,12 +283,15 @@ Useful knobs: - 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`, +`package-update-anthropic`, `package-update-core`, +`plugins-runtime-plugins`, `plugins-runtime-services`, `plugins-runtime-install-a`, `plugins-runtime-install-b`, +`plugins-runtime-install-c`, `plugins-runtime-install-d`, `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. +`bundled-channels`, `plugins-runtime-core`, `plugins-runtime`, and +`plugins-integrations` chunks remain 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` @@ -360,18 +374,22 @@ image. Release-path normal mode fans out into smaller Docker chunk jobs: - `package-update-openai` - `package-update-anthropic` - `package-update-core` -- `plugins-runtime-core` +- `plugins-runtime-plugins` +- `plugins-runtime-services` - `plugins-runtime-install-a` - `plugins-runtime-install-b` +- `plugins-runtime-install-c` +- `plugins-runtime-install-d` - `bundled-channels` -OpenWebUI is folded into `plugins-runtime-core` for full release-path coverage -and keeps a standalone `openwebui` chunk only for OpenWebUI-only dispatches. -The legacy `package-update`, `plugins-runtime`, and `plugins-integrations` -chunks still work as aggregate aliases for manual reruns, but the release -workflow uses the split chunks so provider installer checks, plugin runtime -checks, bundled plugin install/uninstall shards, and bundled-channel checks can -run on separate machines. The bundled-channel runtime-dependency coverage +OpenWebUI is folded into `plugins-runtime-services` for full release-path +coverage and keeps a standalone `openwebui` chunk only for OpenWebUI-only +dispatches. The legacy `package-update`, `plugins-runtime-core`, +`plugins-runtime`, and `plugins-integrations` chunks still work as aggregate +aliases for manual reruns, but the release workflow uses the split chunks so +provider installer checks, plugin runtime checks, bundled plugin +install/uninstall shards, and bundled-channel checks can run on separate +machines. The bundled-channel runtime-dependency coverage inside `bundled-channels` uses the split `bundled-channel-*` and `bundled-channel-update-*` lanes rather than the serial `bundled-channel-deps` lane, so failures produce cheap targeted diff --git a/.github/workflows/full-release-validation.yml b/.github/workflows/full-release-validation.yml index 24e9c7cf94a..caef63d4c9c 100644 --- a/.github/workflows/full-release-validation.yml +++ b/.github/workflows/full-release-validation.yml @@ -96,17 +96,23 @@ jobs: outputs: sha: ${{ steps.resolve.outputs.sha }} steps: - - name: Checkout target ref + - name: Checkout trusted workflow helper uses: actions/checkout@v6 with: - ref: ${{ inputs.ref }} - fetch-depth: 0 + ref: ${{ github.ref_name }} + path: workflow + fetch-depth: 1 persist-credentials: false submodules: false - name: Resolve target SHA id: resolve - run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + env: + TARGET_REF: ${{ inputs.ref }} + run: | + bash workflow/scripts/github/resolve-openclaw-ref.sh \ + --ref "$TARGET_REF" \ + --github-output "$GITHUB_OUTPUT" - name: Summarize target env: @@ -324,7 +330,8 @@ jobs: fi dispatch_and_wait openclaw-release-checks.yml \ - -f ref="$TARGET_SHA" \ + -f ref="$TARGET_REF" \ + -f expected_sha="$TARGET_SHA" \ -f provider="$PROVIDER" \ -f mode="$MODE" \ -f release_profile="$RELEASE_PROFILE" \ diff --git a/.github/workflows/openclaw-cross-os-release-checks-reusable.yml b/.github/workflows/openclaw-cross-os-release-checks-reusable.yml index 96aa4c432d9..d49962394bf 100644 --- a/.github/workflows/openclaw-cross-os-release-checks-reusable.yml +++ b/.github/workflows/openclaw-cross-os-release-checks-reusable.yml @@ -51,6 +51,31 @@ on: required: false default: "" type: string + candidate_artifact_name: + description: Optional current-run artifact name containing the candidate OpenClaw tarball + required: false + default: "" + type: string + candidate_artifact_run_id: + description: Optional workflow run id for candidate_artifact_name + required: false + default: "" + type: string + candidate_file_name: + description: Optional candidate tarball file name inside candidate_artifact_name + required: false + default: "" + type: string + candidate_version: + description: Optional candidate OpenClaw package version + required: false + default: "" + type: string + candidate_source_sha: + description: Optional source SHA used to build the candidate tarball + required: false + default: "" + type: string workflow_call: inputs: ref: @@ -90,6 +115,31 @@ on: required: false default: "" type: string + candidate_artifact_name: + description: Optional current-run artifact name containing the candidate OpenClaw tarball + required: false + default: "" + type: string + candidate_artifact_run_id: + description: Optional workflow run id for candidate_artifact_name + required: false + default: "" + type: string + candidate_file_name: + description: Optional candidate tarball file name inside candidate_artifact_name + required: false + default: "" + type: string + candidate_version: + description: Optional candidate OpenClaw package version + required: false + default: "" + type: string + candidate_source_sha: + description: Optional source SHA used to build the candidate tarball + required: false + default: "" + type: string secrets: OPENAI_API_KEY: required: false @@ -119,7 +169,7 @@ env: jobs: prepare: - runs-on: ubuntu-latest + runs-on: blacksmith-8vcpu-ubuntu-2404 outputs: baseline_file_name: ${{ steps.baseline_metadata.outputs.file_name }} baseline_spec: ${{ steps.baseline.outputs.value }} @@ -260,6 +310,7 @@ jobs: persist-credentials: false - name: Checkout public source ref + if: inputs.candidate_artifact_name == '' uses: actions/checkout@v6 with: repository: ${{ env.OPENCLAW_REPOSITORY }} @@ -280,9 +331,10 @@ jobs: with: node-version: ${{ env.NODE_VERSION }} cache: pnpm - cache-dependency-path: source/pnpm-lock.yaml + cache-dependency-path: ${{ inputs.candidate_artifact_name == '' && 'source/pnpm-lock.yaml' || 'workflow/pnpm-lock.yaml' }} - name: Build candidate artifact once + if: inputs.candidate_artifact_name == '' env: OUTPUT_DIR: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare run: | @@ -291,6 +343,52 @@ jobs: --source-dir source \ --output-dir "${OUTPUT_DIR}" + - name: Download provided candidate artifact + if: inputs.candidate_artifact_name != '' + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.candidate_artifact_name }} + run-id: ${{ inputs.candidate_artifact_run_id || github.run_id }} + github-token: ${{ github.token }} + path: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/package + + - name: Capture provided candidate artifact metadata + if: inputs.candidate_artifact_name != '' + env: + PACKAGE_DIR: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/package + INPUT_CANDIDATE_FILE_NAME: ${{ inputs.candidate_file_name }} + INPUT_CANDIDATE_VERSION: ${{ inputs.candidate_version }} + INPUT_CANDIDATE_SOURCE_SHA: ${{ inputs.candidate_source_sha }} + CANDIDATE_JSON: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/candidate.json + run: | + node <<'NODE' + const fs = require("node:fs"); + const path = require("node:path"); + + const packageDir = process.env.PACKAGE_DIR; + const requestedFileName = process.env.INPUT_CANDIDATE_FILE_NAME.trim(); + const files = fs.readdirSync(packageDir).filter((file) => file.endsWith(".tgz")); + const candidateFileName = requestedFileName || (files.length === 1 ? files[0] : ""); + if (!candidateFileName) { + throw new Error(`Expected exactly one candidate .tgz in ${packageDir}; found ${files.length}.`); + } + if (!fs.existsSync(path.join(packageDir, candidateFileName))) { + throw new Error(`Provided candidate artifact does not contain ${candidateFileName}.`); + } + const candidateVersion = process.env.INPUT_CANDIDATE_VERSION.trim(); + if (!candidateVersion) { + throw new Error("candidate_version is required when candidate_artifact_name is provided."); + } + const sourceSha = process.env.INPUT_CANDIDATE_SOURCE_SHA.trim(); + if (!/^[0-9a-f]{40}$/iu.test(sourceSha)) { + throw new Error("candidate_source_sha must be a full commit SHA when candidate_artifact_name is provided."); + } + fs.writeFileSync( + process.env.CANDIDATE_JSON, + `${JSON.stringify({ candidateFileName, candidateVersion, sourceSha }, null, 2)}\n`, + ); + NODE + - name: Resolve baseline package spec if: ${{ inputs.mode != 'fresh' }} id: baseline diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index b400419c0f7..ed126fac9fd 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -457,15 +457,24 @@ jobs: - chunk_id: package-update-core label: package/update core timeout_minutes: 120 - - chunk_id: plugins-runtime-core - label: plugins/runtime core - timeout_minutes: 180 + - chunk_id: plugins-runtime-plugins + label: plugins/runtime plugins + timeout_minutes: 120 + - chunk_id: plugins-runtime-services + label: plugins/runtime services + timeout_minutes: 120 - chunk_id: plugins-runtime-install-a label: plugins/runtime install A - timeout_minutes: 180 + timeout_minutes: 120 - chunk_id: plugins-runtime-install-b label: plugins/runtime install B - timeout_minutes: 180 + timeout_minutes: 120 + - chunk_id: plugins-runtime-install-c + label: plugins/runtime install C + timeout_minutes: 120 + - chunk_id: plugins-runtime-install-d + label: plugins/runtime install D + timeout_minutes: 120 - chunk_id: bundled-channels-core label: bundled channels core timeout_minutes: 90 diff --git a/.github/workflows/openclaw-release-checks.yml b/.github/workflows/openclaw-release-checks.yml index 3bb98571144..011b670c358 100644 --- a/.github/workflows/openclaw-release-checks.yml +++ b/.github/workflows/openclaw-release-checks.yml @@ -7,6 +7,11 @@ on: description: Branch, tag, or full commit SHA to validate required: true type: string + expected_sha: + description: Optional full SHA that ref must resolve to + required: false + default: "" + type: string provider: description: Provider lane for cross-OS onboarding and the end-to-end agent turn required: false @@ -86,24 +91,54 @@ jobs: - name: Validate ref input env: RELEASE_REF: ${{ inputs.ref }} + EXPECTED_SHA: ${{ inputs.expected_sha }} run: | set -euo pipefail if [[ -z "${RELEASE_REF// }" ]] || [[ "${RELEASE_REF}" == -* ]]; then echo "Expected a branch, tag, or full commit SHA; got: ${RELEASE_REF}" >&2 exit 1 fi + if [[ -n "${EXPECTED_SHA// }" ]] && [[ ! "${EXPECTED_SHA}" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "Expected expected_sha to be a full commit SHA; got: ${EXPECTED_SHA}" >&2 + exit 1 + fi - - name: Checkout selected ref + - name: Checkout trusted workflow helper + uses: actions/checkout@v6 + with: + ref: ${{ github.ref_name }} + path: workflow + fetch-depth: 1 + + - name: Fast-resolve selected ref + id: fast_ref + env: + RELEASE_REF: ${{ inputs.ref }} + EXPECTED_SHA: ${{ inputs.expected_sha }} + run: | + bash workflow/scripts/github/resolve-openclaw-ref.sh \ + --ref "$RELEASE_REF" \ + --expected-sha "$EXPECTED_SHA" \ + --fallback-ok \ + --github-output "$GITHUB_OUTPUT" + + - name: Checkout selected ref for reachability fallback + if: steps.fast_ref.outputs.fallback == 'true' uses: actions/checkout@v6 with: ref: ${{ inputs.ref }} + path: source fetch-depth: 0 - - name: Resolve checked-out SHA - id: ref + - name: Resolve checked-out fallback SHA + if: steps.fast_ref.outputs.fallback == 'true' + id: fallback_ref + working-directory: source run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - name: Validate selected ref belongs to this repository + if: steps.fast_ref.outputs.fallback == 'true' + working-directory: source env: RELEASE_REF: ${{ inputs.ref }} run: | @@ -124,6 +159,29 @@ jobs: echo "Secret-bearing release checks only run repository-owned branch/tag history, not arbitrary unreferenced commits." >&2 exit 1 + - name: Finalize resolved SHA + id: ref + env: + FAST_SHA: ${{ steps.fast_ref.outputs.sha }} + FALLBACK_SHA: ${{ steps.fallback_ref.outputs.sha }} + EXPECTED_SHA: ${{ inputs.expected_sha }} + USED_FALLBACK: ${{ steps.fast_ref.outputs.fallback }} + run: | + set -euo pipefail + selected_sha="$FAST_SHA" + if [[ "$USED_FALLBACK" == "true" ]]; then + selected_sha="$FALLBACK_SHA" + fi + if [[ -z "$selected_sha" ]]; then + echo "Failed to resolve selected ref SHA." >&2 + exit 1 + fi + if [[ -n "${EXPECTED_SHA// }" ]] && [[ "${selected_sha,,}" != "${EXPECTED_SHA,,}" ]]; then + echo "Ref resolved to ${selected_sha}, expected ${EXPECTED_SHA}." >&2 + exit 1 + fi + echo "sha=${selected_sha,,}" >> "$GITHUB_OUTPUT" + - name: Capture selected inputs id: inputs env: @@ -146,6 +204,7 @@ jobs: env: RELEASE_REF: ${{ inputs.ref }} RELEASE_SHA: ${{ steps.ref.outputs.sha }} + RELEASE_REF_FAST_PATH: ${{ steps.fast_ref.outputs.fast }} RELEASE_PROVIDER: ${{ inputs.provider }} RELEASE_MODE: ${{ inputs.mode }} RELEASE_PROFILE: ${{ inputs.release_profile }} @@ -156,6 +215,7 @@ jobs: echo echo "- Requested ref: \`${RELEASE_REF}\`" echo "- Validated SHA: \`${RELEASE_SHA}\`" + echo "- Ref resolution fast path: \`${RELEASE_REF_FAST_PATH}\`" echo "- Cross-OS provider: \`${RELEASE_PROVIDER}\`" echo "- Cross-OS mode: \`${RELEASE_MODE}\`" echo "- Release profile: \`${RELEASE_PROFILE}\`" @@ -166,7 +226,7 @@ jobs: prepare_release_package: name: Prepare release package artifact needs: [resolve_target] - if: contains(fromJSON('["all","live-e2e","package"]'), needs.resolve_target.outputs.rerun_group) + if: contains(fromJSON('["all","cross-os","live-e2e","package"]'), needs.resolve_target.outputs.rerun_group) runs-on: blacksmith-32vcpu-ubuntu-2404 timeout-minutes: 60 permissions: @@ -175,6 +235,7 @@ jobs: artifact_name: ${{ steps.artifact.outputs.name }} package_sha256: ${{ steps.package.outputs.sha256 }} package_version: ${{ steps.package.outputs.package_version }} + source_sha: ${{ steps.package.outputs.source_sha }} steps: - name: Checkout trusted workflow ref uses: actions/checkout@v6 @@ -198,7 +259,7 @@ jobs: id: package shell: bash env: - PACKAGE_REF: ${{ needs.resolve_target.outputs.ref }} + PACKAGE_REF: ${{ needs.resolve_target.outputs.sha }} run: | set -euo pipefail node scripts/resolve-openclaw-package-candidate.mjs \ @@ -210,6 +271,8 @@ jobs: --github-output "$GITHUB_OUTPUT" digest="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).sha256")" version="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).version")" + source_sha="$(node -p "JSON.parse(require('fs').readFileSync('.artifacts/docker-e2e-package/package-candidate.json', 'utf8')).packageSourceSha")" + echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT" { echo "## Release package artifact" echo @@ -217,6 +280,7 @@ jobs: echo "- Package ref: \`$PACKAGE_REF\`" echo "- SHA-256: \`$digest\`" echo "- Version: \`$version\`" + echo "- Source SHA: \`$source_sha\`" } >> "$GITHUB_STEP_SUMMARY" - name: Upload release package artifact @@ -234,11 +298,11 @@ jobs: contents: read uses: ./.github/workflows/install-smoke.yml with: - ref: ${{ needs.resolve_target.outputs.ref }} + ref: ${{ needs.resolve_target.outputs.sha }} run_bun_global_install_smoke: true cross_os_release_checks: - needs: [resolve_target] + needs: [resolve_target, prepare_release_package] 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 @@ -246,6 +310,11 @@ jobs: ref: ${{ needs.resolve_target.outputs.ref }} provider: ${{ needs.resolve_target.outputs.provider }} mode: ${{ needs.resolve_target.outputs.mode }} + candidate_artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }} + candidate_artifact_run_id: ${{ github.run_id }} + candidate_file_name: openclaw-current.tgz + candidate_version: ${{ needs.prepare_release_package.outputs.package_version }} + candidate_source_sha: ${{ needs.prepare_release_package.outputs.source_sha }} secrets: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} @@ -264,7 +333,7 @@ jobs: pull-requests: read uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml with: - ref: ${{ needs.resolve_target.outputs.ref }} + ref: ${{ needs.resolve_target.outputs.sha }} include_repo_e2e: true include_release_path_suites: true include_openwebui: ${{ needs.resolve_target.outputs.release_profile != 'minimum' }} @@ -419,7 +488,7 @@ jobs: - name: Checkout selected ref uses: actions/checkout@v6 with: - ref: ${{ needs.resolve_target.outputs.ref }} + ref: ${{ needs.resolve_target.outputs.sha }} fetch-depth: 1 - name: Setup Node environment @@ -487,7 +556,7 @@ jobs: - name: Checkout selected ref uses: actions/checkout@v6 with: - ref: ${{ needs.resolve_target.outputs.ref }} + ref: ${{ needs.resolve_target.outputs.sha }} fetch-depth: 1 - name: Setup Node environment @@ -543,7 +612,7 @@ jobs: - name: Checkout selected ref uses: actions/checkout@v6 with: - ref: ${{ needs.resolve_target.outputs.ref }} + ref: ${{ needs.resolve_target.outputs.sha }} fetch-depth: 1 - name: Setup Node environment @@ -622,7 +691,7 @@ jobs: - name: Checkout selected ref uses: actions/checkout@v6 with: - ref: ${{ needs.resolve_target.outputs.ref }} + ref: ${{ needs.resolve_target.outputs.sha }} fetch-depth: 1 - name: Setup Node environment diff --git a/docs/ci.md b/docs/ci.md index 42e8fabf9da..fadaaaba7f1 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -319,9 +319,9 @@ act as if every scoped area changed. CI workflow edits validate the Node CI graph plus workflow linting, but do not force Windows, Android, or macOS native builds by themselves; those platform lanes stay scoped to platform source changes. CI routing-only edits, selected cheap core-test fixture edits, and narrow plugin contract helper/test-routing edits use a fast Node-only manifest path: preflight, security, and a single `checks-fast-core` task. That path avoids build artifacts, Node 22 compatibility, channel contracts, full core shards, bundled-plugin shards, and additional guard matrices when the changed files are limited to the routing or helper surfaces that the fast task exercises directly. Windows Node checks are scoped to Windows-specific process/path wrappers, npm/pnpm/UI runner helpers, package manager config, and the CI workflow surfaces that execute that lane; unrelated source, plugin, install-smoke, and test-only changes stay on the Linux Node lanes so they do not reserve a 16-vCPU Windows worker for coverage that is already exercised by the normal test shards. -The separate `install-smoke` workflow reuses the same scope script through its own `preflight` job. It splits smoke coverage into `run_fast_install_smoke` and `run_full_install_smoke`. Pull requests run the fast path for Docker/package surfaces, bundled plugin package/manifest changes, and core plugin/channel/gateway/Plugin SDK surfaces that the Docker smoke jobs exercise. Source-only bundled plugin changes, test-only edits, and docs-only edits do not reserve Docker workers. The fast path builds the root Dockerfile image once, checks the CLI, runs the agents delete shared-workspace CLI smoke, runs the container gateway-network e2e, verifies a bundled extension build arg, and runs the bounded bundled-plugin Docker profile under a 240-second aggregate command timeout with each scenario's Docker run capped separately. The full path keeps QR package install and installer Docker/update coverage for nightly scheduled runs, manual dispatches, workflow-call release checks, and pull requests that truly touch installer/package/Docker surfaces. `main` pushes, including merge commits, do not force the full path; when changed-scope logic would request full coverage on a push, the workflow keeps the fast Docker smoke and leaves the full install smoke to nightly or release validation. The slow Bun global install image-provider smoke is separately gated by `run_bun_global_install_smoke`; it runs on the nightly schedule and from the release checks workflow, and manual `install-smoke` dispatches can opt into it, but pull requests and `main` pushes do not run it. QR and installer Docker tests keep their own install-focused Dockerfiles. Local `test:docker:all` prebuilds one shared live-test image, packs OpenClaw once as an npm tarball, and builds two shared `scripts/e2e/Dockerfile` images: a bare Node/Git runner for installer/update/plugin-dependency lanes and a functional image that installs the same tarball into `/app` for normal functionality lanes. Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mjs`, planner logic lives in `scripts/lib/docker-e2e-plan.mjs`, and the runner only executes the selected plan. The scheduler selects the image per lane with `OPENCLAW_DOCKER_E2E_BARE_IMAGE` and `OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE`, then runs lanes with `OPENCLAW_SKIP_DOCKER_BUILD=1`; tune the default main-pool slot count of 10 with `OPENCLAW_DOCKER_ALL_PARALLELISM` and the provider-sensitive tail-pool slot count of 10 with `OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM`. Heavy lane caps default to `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=10`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7` so npm install and multi-service lanes do not overcommit Docker while lighter lanes still fill available slots. A single lane heavier than the effective caps can still start from an empty pool, then runs alone until it releases capacity. Lane starts are staggered by 2 seconds by default to avoid local Docker daemon create storms; override with `OPENCLAW_DOCKER_ALL_START_STAGGER_MS=0` or another millisecond value. The local aggregate preflights Docker, removes stale OpenClaw E2E containers, emits active-lane status, persists lane timings for longest-first ordering, and supports `OPENCLAW_DOCKER_ALL_DRY_RUN=1` for scheduler inspection. It stops scheduling new pooled lanes after the first failure by default, and each lane has a 120-minute fallback timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`; selected live/tail lanes use tighter per-lane caps. `OPENCLAW_DOCKER_ALL_LANES=` runs exact scheduler lanes, including release-only lanes such as `install-e2e` and split bundled update lanes such as `bundled-channel-update-acpx`, while skipping the cleanup smoke so agents can reproduce one failed lane. The reusable live/E2E workflow asks `scripts/test-docker-all.mjs --plan-json` which package, image kind, live image, lane, and credential coverage is required, then `scripts/docker-e2e.mjs` converts that plan into GitHub outputs and summaries. It either packs OpenClaw through `scripts/package-openclaw-for-docker.mjs`, downloads a current-run package artifact, or downloads a package artifact from `package_artifact_run_id`; validates the tarball inventory; builds and pushes package-digest-tagged bare/functional GHCR Docker E2E images through Blacksmith's Docker layer cache when the plan needs package-installed lanes; and reuses provided `docker_e2e_bare_image`/`docker_e2e_functional_image` inputs or existing package-digest images instead of rebuilding. The `Package Acceptance` workflow is the high-level package gate: it resolves a candidate from npm, a trusted `package_ref`, an HTTPS tarball plus SHA-256, or a prior workflow artifact, then passes that single `package-under-test` artifact into the reusable Docker E2E workflow. It keeps `workflow_ref` separate from `package_ref` so current acceptance logic can validate older trusted commits without checking out old workflow code. Release checks run a custom Package Acceptance delta for the target ref: bundled-channel compat, offline plugin fixtures, and Telegram package QA against the resolved tarball. The release-path Docker suite runs smaller chunked jobs with `OPENCLAW_SKIP_DOCKER_BUILD=1` so each chunk pulls only the image kind it needs and executes multiple lanes through the same weighted scheduler (`OPENCLAW_DOCKER_ALL_PROFILE=release-path`, `OPENCLAW_DOCKER_ALL_CHUNK=core|package-update-openai|package-update-anthropic|package-update-core|plugins-runtime-core|plugins-runtime-install-a|plugins-runtime-install-b|bundled-channels`). OpenWebUI is folded into `plugins-runtime-core` when full release-path coverage requests it, and keeps a standalone `openwebui` chunk only for OpenWebUI-only dispatches. The legacy aggregate chunk names `package-update`, `plugins-runtime`, and `plugins-integrations` still work for manual reruns, but the release workflow uses the split chunks so installer E2E and bundled plugin install/uninstall sweeps do not dominate the critical path. The `install-e2e` lane alias remains the aggregate manual rerun alias for both provider installer lanes. The `bundled-channels` chunk runs split `bundled-channel-*` and `bundled-channel-update-*` lanes rather than the serial all-in-one `bundled-channel-deps` lane. Each chunk uploads `.artifacts/docker-tests/` with lane logs, timings, `summary.json`, `failures.json`, phase timings, scheduler plan JSON, slow-lane tables, and per-lane rerun commands. The workflow `docker_lanes` input runs selected lanes against the prepared images instead of the chunk jobs, which keeps failed-lane debugging bounded to one targeted Docker job and prepares, downloads, or reuses the package artifact for that run; if a selected lane is a live Docker lane, the targeted job builds the live-test image locally for that rerun. Generated per-lane GitHub rerun commands include `package_artifact_run_id`, `package_artifact_name`, and prepared image inputs when those values exist, so a failed lane can reuse the exact package and images from the failed run. Use `pnpm test:docker:rerun ` to download Docker artifacts from a GitHub run and print combined/per-lane targeted rerun commands; use `pnpm test:docker:timings ` for slow-lane and phase critical-path summaries. The scheduled live/E2E workflow runs the full release-path Docker suite daily. The bundled update matrix is split by update target so repeated npm update and doctor repair passes can shard with other bundled checks. +The separate `install-smoke` workflow reuses the same scope script through its own `preflight` job. It splits smoke coverage into `run_fast_install_smoke` and `run_full_install_smoke`. Pull requests run the fast path for Docker/package surfaces, bundled plugin package/manifest changes, and core plugin/channel/gateway/Plugin SDK surfaces that the Docker smoke jobs exercise. Source-only bundled plugin changes, test-only edits, and docs-only edits do not reserve Docker workers. The fast path builds the root Dockerfile image once, checks the CLI, runs the agents delete shared-workspace CLI smoke, runs the container gateway-network e2e, verifies a bundled extension build arg, and runs the bounded bundled-plugin Docker profile under a 240-second aggregate command timeout with each scenario's Docker run capped separately. The full path keeps QR package install and installer Docker/update coverage for nightly scheduled runs, manual dispatches, workflow-call release checks, and pull requests that truly touch installer/package/Docker surfaces. `main` pushes, including merge commits, do not force the full path; when changed-scope logic would request full coverage on a push, the workflow keeps the fast Docker smoke and leaves the full install smoke to nightly or release validation. The slow Bun global install image-provider smoke is separately gated by `run_bun_global_install_smoke`; it runs on the nightly schedule and from the release checks workflow, and manual `install-smoke` dispatches can opt into it, but pull requests and `main` pushes do not run it. QR and installer Docker tests keep their own install-focused Dockerfiles. Local `test:docker:all` prebuilds one shared live-test image, packs OpenClaw once as an npm tarball, and builds two shared `scripts/e2e/Dockerfile` images: a bare Node/Git runner for installer/update/plugin-dependency lanes and a functional image that installs the same tarball into `/app` for normal functionality lanes. Docker lane definitions live in `scripts/lib/docker-e2e-scenarios.mjs`, planner logic lives in `scripts/lib/docker-e2e-plan.mjs`, and the runner only executes the selected plan. The scheduler selects the image per lane with `OPENCLAW_DOCKER_E2E_BARE_IMAGE` and `OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE`, then runs lanes with `OPENCLAW_SKIP_DOCKER_BUILD=1`; tune the default main-pool slot count of 10 with `OPENCLAW_DOCKER_ALL_PARALLELISM` and the provider-sensitive tail-pool slot count of 10 with `OPENCLAW_DOCKER_ALL_TAIL_PARALLELISM`. Heavy lane caps default to `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=9`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=10`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7` so npm install and multi-service lanes do not overcommit Docker while lighter lanes still fill available slots. A single lane heavier than the effective caps can still start from an empty pool, then runs alone until it releases capacity. Lane starts are staggered by 2 seconds by default to avoid local Docker daemon create storms; override with `OPENCLAW_DOCKER_ALL_START_STAGGER_MS=0` or another millisecond value. The local aggregate preflights Docker, removes stale OpenClaw E2E containers, emits active-lane status, persists lane timings for longest-first ordering, and supports `OPENCLAW_DOCKER_ALL_DRY_RUN=1` for scheduler inspection. It stops scheduling new pooled lanes after the first failure by default, and each lane has a 120-minute fallback timeout overrideable with `OPENCLAW_DOCKER_ALL_LANE_TIMEOUT_MS`; selected live/tail lanes use tighter per-lane caps. `OPENCLAW_DOCKER_ALL_LANES=` runs exact scheduler lanes, including release-only lanes such as `install-e2e` and split bundled update lanes such as `bundled-channel-update-acpx`, while skipping the cleanup smoke so agents can reproduce one failed lane. The reusable live/E2E workflow asks `scripts/test-docker-all.mjs --plan-json` which package, image kind, live image, lane, and credential coverage is required, then `scripts/docker-e2e.mjs` converts that plan into GitHub outputs and summaries. It either packs OpenClaw through `scripts/package-openclaw-for-docker.mjs`, downloads a current-run package artifact, or downloads a package artifact from `package_artifact_run_id`; validates the tarball inventory; builds and pushes package-digest-tagged bare/functional GHCR Docker E2E images through Blacksmith's Docker layer cache when the plan needs package-installed lanes; and reuses provided `docker_e2e_bare_image`/`docker_e2e_functional_image` inputs or existing package-digest images instead of rebuilding. The `Package Acceptance` workflow is the high-level package gate: it resolves a candidate from npm, a trusted `package_ref`, an HTTPS tarball plus SHA-256, or a prior workflow artifact, then passes that single `package-under-test` artifact into the reusable Docker E2E workflow. It keeps `workflow_ref` separate from `package_ref` so current acceptance logic can validate older trusted commits without checking out old workflow code. Release checks run a custom Package Acceptance delta for the target ref: bundled-channel compat, offline plugin fixtures, and Telegram package QA against the resolved tarball. The release-path Docker suite runs smaller chunked jobs with `OPENCLAW_SKIP_DOCKER_BUILD=1` so each chunk pulls only the image kind it needs and executes multiple lanes through the same weighted scheduler (`OPENCLAW_DOCKER_ALL_PROFILE=release-path`, `OPENCLAW_DOCKER_ALL_CHUNK=core|package-update-openai|package-update-anthropic|package-update-core|plugins-runtime-plugins|plugins-runtime-services|plugins-runtime-install-a|plugins-runtime-install-b|plugins-runtime-install-c|plugins-runtime-install-d|bundled-channels`). OpenWebUI is folded into `plugins-runtime-services` when full release-path coverage requests it, and keeps a standalone `openwebui` chunk only for OpenWebUI-only dispatches. The legacy aggregate chunk names `package-update`, `plugins-runtime-core`, `plugins-runtime`, and `plugins-integrations` still work for manual reruns, but the release workflow uses the split chunks so installer E2E and bundled plugin install/uninstall sweeps do not dominate the critical path. The `install-e2e` lane alias remains the aggregate manual rerun alias for both provider installer lanes. The `bundled-channels` chunk runs split `bundled-channel-*` and `bundled-channel-update-*` lanes rather than the serial all-in-one `bundled-channel-deps` lane. Each chunk uploads `.artifacts/docker-tests/` with lane logs, timings, `summary.json`, `failures.json`, phase timings, scheduler plan JSON, slow-lane tables, and per-lane rerun commands. The workflow `docker_lanes` input runs selected lanes against the prepared images instead of the chunk jobs, which keeps failed-lane debugging bounded to one targeted Docker job and prepares, downloads, or reuses the package artifact for that run; if a selected lane is a live Docker lane, the targeted job builds the live-test image locally for that rerun. Generated per-lane GitHub rerun commands include `package_artifact_run_id`, `package_artifact_name`, and prepared image inputs when those values exist, so a failed lane can reuse the exact package and images from the failed run. Use `pnpm test:docker:rerun ` to download Docker artifacts from a GitHub run and print combined/per-lane targeted rerun commands; use `pnpm test:docker:timings ` for slow-lane and phase critical-path summaries. The scheduled live/E2E workflow runs the full release-path Docker suite daily. The bundled update matrix is split by update target so repeated npm update and doctor repair passes can shard with other bundled checks. -Current release Docker chunks are `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 available for manual one-shot reruns, but the release workflow uses the split chunks so channel smokes, update targets, and setup/runtime contract checks can run in parallel. Targeted `docker_lanes` dispatches also split multiple selected lanes into parallel jobs after one shared package/image preparation step, and bundled-channel update lanes retry once for transient npm network failures. +Current release Docker chunks are `core`, `package-update-openai`, `package-update-anthropic`, `package-update-core`, `plugins-runtime-plugins`, `plugins-runtime-services`, `plugins-runtime-install-a`, `plugins-runtime-install-b`, `plugins-runtime-install-c`, `plugins-runtime-install-d`, `bundled-channels-core`, `bundled-channels-update-a`, `bundled-channels-update-b`, and `bundled-channels-contracts`. The aggregate `bundled-channels` chunk remains available for manual one-shot reruns, and `plugins-runtime-core`, `plugins-runtime`, and `plugins-integrations` remain aggregate plugin/runtime aliases, but the release workflow uses the split chunks so channel smokes, update targets, plugin runtime checks, and bundled plugin install/uninstall sweeps can run in parallel. Targeted `docker_lanes` dispatches also split multiple selected lanes into parallel jobs after one shared package/image preparation step, and bundled-channel update lanes retry once for transient npm network failures. Local changed-lane logic lives in `scripts/changed-lanes.mjs` and is executed by `scripts/check-changed.mjs`. That local check gate is stricter about architecture boundaries than the broad CI platform scope: core production changes run core prod and core test typecheck plus core lint/guards, core test-only changes run only core test typecheck plus core lint, extension production changes run extension prod and extension test typecheck plus extension lint, and extension test-only changes run extension test typecheck plus extension lint. Public Plugin SDK or plugin-contract changes expand to extension typecheck because extensions depend on those core contracts, but Vitest extension sweeps are explicit test work. Release metadata-only version bumps run targeted version/config/root-dependency checks. Unknown root/config changes fail safe to all check lanes. diff --git a/scripts/github/resolve-openclaw-ref.sh b/scripts/github/resolve-openclaw-ref.sh new file mode 100755 index 00000000000..e197bfb5f80 --- /dev/null +++ b/scripts/github/resolve-openclaw-ref.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +set -euo pipefail + +REMOTE_URL="${OPENCLAW_REF_REMOTE:-https://github.com/openclaw/openclaw.git}" +REF="" +EXPECTED_SHA="" +FALLBACK_OK=0 +GITHUB_OUTPUT_FILE="${GITHUB_OUTPUT:-}" + +usage() { + cat >&2 <<'EOF' +Usage: resolve-openclaw-ref.sh --ref [--expected-sha ] [--fallback-ok] [--github-output ] + +Fast-resolves OpenClaw branch and tag refs with git ls-remote. Full commit SHAs +are returned as fallback refs so callers can decide whether to run deeper +reachability validation. +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --ref) + REF="${2:-}" + shift 2 + ;; + --expected-sha) + EXPECTED_SHA="${2:-}" + shift 2 + ;; + --fallback-ok) + FALLBACK_OK=1 + shift + ;; + --github-output) + GITHUB_OUTPUT_FILE="${2:-}" + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage + exit 2 + ;; + esac +done + +trim() { + local value="$1" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + printf '%s' "$value" +} + +write_output() { + local key="$1" + local value="$2" + if [[ -n "$GITHUB_OUTPUT_FILE" ]]; then + printf '%s=%s\n' "$key" "$value" >> "$GITHUB_OUTPUT_FILE" + else + printf '%s=%s\n' "$key" "$value" + fi +} + +resolve_unique_remote_ref() { + local refspec + local -a matches=() + for refspec in "$@"; do + [[ -n "$refspec" ]] || continue + mapfile -t matches < <( + git ls-remote "$REMOTE_URL" "$refspec" | awk '{print $1}' | awk '!seen[$0]++' + ) + if [[ "${#matches[@]}" -eq 0 ]]; then + continue + fi + if [[ "${#matches[@]}" -ne 1 ]]; then + return 2 + fi + printf '%s\n' "${matches[0]}" + return 0 + done + return 1 +} + +REF="$(trim "$REF")" +EXPECTED_SHA="$(trim "$EXPECTED_SHA")" +if [[ -z "$REF" ]] || [[ "$REF" == -* ]]; then + echo "Expected a branch, tag, or full commit SHA; got: ${REF}" >&2 + exit 1 +fi +if [[ -n "$EXPECTED_SHA" ]] && [[ ! "$EXPECTED_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then + echo "Expected --expected-sha to be a full commit SHA; got: ${EXPECTED_SHA}" >&2 + exit 1 +fi + +if [[ "$REF" =~ ^[0-9a-fA-F]{40}$ ]]; then + if [[ -n "$EXPECTED_SHA" ]] && [[ "${REF,,}" != "${EXPECTED_SHA,,}" ]]; then + echo "Ref SHA ${REF} does not match expected SHA ${EXPECTED_SHA}." >&2 + exit 1 + fi + write_output sha "${REF,,}" + write_output ref_kind sha + write_output fast false + write_output fallback true + exit 0 +fi + +declare -a matches=() +if [[ "$REF" == refs/heads/* ]]; then + mapfile -t matches < <(resolve_unique_remote_ref "$REF" || true) +elif [[ "$REF" == refs/tags/* ]]; then + mapfile -t matches < <(resolve_unique_remote_ref "${REF}^{}" "$REF" || true) +elif [[ "$REF" == refs/* ]]; then + mapfile -t matches < <(resolve_unique_remote_ref "$REF" || true) +else + mapfile -t branch_matches < <(resolve_unique_remote_ref "refs/heads/${REF}" || true) + mapfile -t tag_matches < <(resolve_unique_remote_ref "refs/tags/${REF}^{}" "refs/tags/${REF}" || true) + match_count=$(( ${#branch_matches[@]} + ${#tag_matches[@]} )) + if [[ "$match_count" -eq 1 ]]; then + if [[ "${#branch_matches[@]}" -eq 1 ]]; then + matches=("${branch_matches[0]}") + ref_kind=branch + else + matches=("${tag_matches[0]}") + ref_kind=tag + fi + elif [[ "$match_count" -gt 1 ]]; then + echo "Ref resolved ambiguously as both branch and tag: ${REF}" >&2 + exit 1 + fi +fi + +if [[ "${#matches[@]}" -eq 1 ]]; then + resolved="${matches[0],,}" + if [[ -n "$EXPECTED_SHA" ]] && [[ "$resolved" != "${EXPECTED_SHA,,}" ]]; then + echo "Ref ${REF} resolved to ${resolved}, expected ${EXPECTED_SHA}." >&2 + exit 1 + fi + if [[ -z "${ref_kind:-}" ]]; then + if [[ "$REF" == refs/tags/* ]]; then + ref_kind=tag + elif [[ "$REF" == refs/heads/* ]]; then + ref_kind=branch + else + ref_kind=ref + fi + fi + write_output sha "$resolved" + write_output ref_kind "$ref_kind" + write_output fast true + write_output fallback false + exit 0 +fi + +if [[ "$FALLBACK_OK" -eq 1 ]]; then + write_output sha "$EXPECTED_SHA" + write_output ref_kind unknown + write_output fast false + write_output fallback true + exit 0 +fi + +echo "Failed to resolve OpenClaw ref: ${REF}" >&2 +exit 1 diff --git a/scripts/lib/docker-e2e-scenarios.mjs b/scripts/lib/docker-e2e-scenarios.mjs index e78149a5622..8debc47b078 100644 --- a/scripts/lib/docker-e2e-scenarios.mjs +++ b/scripts/lib/docker-e2e-scenarios.mjs @@ -418,11 +418,14 @@ const releasePathPluginRuntimeLanes = [ ), ]; -const releasePathPluginRuntimeCoreLanes = [ +const releasePathPluginRuntimePluginLanes = [ lane("plugins", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugins", { resources: ["npm", "service"], weight: 6, }), +]; + +const releasePathPluginRuntimeServiceLanes = [ serviceLane( "cron-mcp-cleanup", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:cron-mcp-cleanup", @@ -438,6 +441,11 @@ const releasePathPluginRuntimeCoreLanes = [ ), ]; +const releasePathPluginRuntimeCoreLanes = [ + ...releasePathPluginRuntimePluginLanes, + ...releasePathPluginRuntimeServiceLanes, +]; + const releasePathBundledChannelLanes = [ npmLane("plugin-update", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugin-update"), ...bundledScenarioLanes, @@ -508,9 +516,12 @@ const primaryReleasePathChunks = { "package-update-openai": releasePathPackageInstallOpenAiLanes, "package-update-anthropic": releasePathPackageInstallAnthropicLanes, "package-update-core": releasePathPackageUpdateCoreLanes, - "plugins-runtime-core": releasePathPluginRuntimeCoreLanes, - "plugins-runtime-install-a": bundledPluginInstallUninstallLanes.slice(0, 4), - "plugins-runtime-install-b": bundledPluginInstallUninstallLanes.slice(4), + "plugins-runtime-plugins": releasePathPluginRuntimePluginLanes, + "plugins-runtime-services": releasePathPluginRuntimeServiceLanes, + "plugins-runtime-install-a": bundledPluginInstallUninstallLanes.slice(0, 2), + "plugins-runtime-install-b": bundledPluginInstallUninstallLanes.slice(2, 4), + "plugins-runtime-install-c": bundledPluginInstallUninstallLanes.slice(4, 6), + "plugins-runtime-install-d": bundledPluginInstallUninstallLanes.slice(6), "bundled-channels-core": [releasePathBundledChannelLanes[0], ...bundledChannelSmokeLanes], "bundled-channels-update-a": [bundledChannelUpdateLanes[0], bundledChannelUpdateLanes[4]], "bundled-channels-update-discord": [bundledChannelUpdateLanes[1]], @@ -529,6 +540,7 @@ const legacyReleasePathChunks = { ...releasePathPackageInstallAnthropicLanes, ...releasePathPackageUpdateCoreLanes, ], + "plugins-runtime-core": releasePathPluginRuntimeCoreLanes, "plugins-runtime": releasePathPluginRuntimeLanes, "plugins-integrations": [...releasePathPluginRuntimeLanes, ...releasePathBundledChannelLanes], "bundled-channels": releasePathBundledChannelLanes, @@ -560,7 +572,8 @@ export function releasePathChunkLanes(chunk, options = {}) { return options.includeOpenWebUI ? [openWebUILane()] : []; } if ( - (chunk !== "plugins-runtime-core" && + (chunk !== "plugins-runtime-services" && + chunk !== "plugins-runtime-core" && chunk !== "plugins-runtime" && chunk !== "plugins-integrations") || !options.includeOpenWebUI diff --git a/scripts/openclaw-cross-os-release-checks.ts b/scripts/openclaw-cross-os-release-checks.ts index 2e806ca5339..f5d42a9b8d6 100644 --- a/scripts/openclaw-cross-os-release-checks.ts +++ b/scripts/openclaw-cross-os-release-checks.ts @@ -155,7 +155,7 @@ export function resolveRunnerMatrix(params) { { os_id: "ubuntu", display_name: "Linux", - runner: pick(params.ubuntuRunner, params.varUbuntuRunner, "ubuntu-latest"), + runner: pick(params.ubuntuRunner, params.varUbuntuRunner, "blacksmith-8vcpu-ubuntu-2404"), artifact_name: "linux", }, { diff --git a/test/scripts/docker-e2e-plan.test.ts b/test/scripts/docker-e2e-plan.test.ts index d0828211155..6cccb8aef0f 100644 --- a/test/scripts/docker-e2e-plan.test.ts +++ b/test/scripts/docker-e2e-plan.test.ts @@ -89,10 +89,15 @@ describe("scripts/lib/docker-e2e-plan", () => { profile: RELEASE_PATH_PROFILE, releaseChunk: "package-update-core", }); - const pluginsRuntimeCore = planFor({ + const pluginsRuntimePlugins = planFor({ includeOpenWebUI: true, profile: RELEASE_PATH_PROFILE, - releaseChunk: "plugins-runtime-core", + releaseChunk: "plugins-runtime-plugins", + }); + const pluginsRuntimeServices = planFor({ + includeOpenWebUI: true, + profile: RELEASE_PATH_PROFILE, + releaseChunk: "plugins-runtime-services", }); const pluginsRuntimeInstallA = planFor({ includeOpenWebUI: true, @@ -104,6 +109,16 @@ describe("scripts/lib/docker-e2e-plan", () => { profile: RELEASE_PATH_PROFILE, releaseChunk: "plugins-runtime-install-b", }); + const pluginsRuntimeInstallC = planFor({ + includeOpenWebUI: true, + profile: RELEASE_PATH_PROFILE, + releaseChunk: "plugins-runtime-install-c", + }); + const pluginsRuntimeInstallD = planFor({ + includeOpenWebUI: true, + profile: RELEASE_PATH_PROFILE, + releaseChunk: "plugins-runtime-install-d", + }); const bundledChannelsCore = planFor({ includeOpenWebUI: true, profile: RELEASE_PATH_PROFILE, @@ -139,26 +154,28 @@ describe("scripts/lib/docker-e2e-plan", () => { "doctor-switch", "update-channel-switch", ]); - expect(pluginsRuntimeCore.lanes.map((lane) => lane.name)).toEqual( - expect.arrayContaining([ - "plugins", - "cron-mcp-cleanup", - "openai-web-search-minimal", - "openwebui", - ]), - ); - expect(pluginsRuntimeCore.lanes.map((lane) => lane.name)).not.toContain( + expect(pluginsRuntimePlugins.lanes.map((lane) => lane.name)).toEqual(["plugins"]); + expect(pluginsRuntimeServices.lanes.map((lane) => lane.name)).toEqual([ + "cron-mcp-cleanup", + "openai-web-search-minimal", + "openwebui", + ]); + expect(pluginsRuntimePlugins.lanes.map((lane) => lane.name)).not.toContain( "bundled-plugin-install-uninstall-0", ); expect(pluginsRuntimeInstallA.lanes.map((lane) => lane.name)).toEqual([ "bundled-plugin-install-uninstall-0", "bundled-plugin-install-uninstall-1", + ]); + expect(pluginsRuntimeInstallB.lanes.map((lane) => lane.name)).toEqual([ "bundled-plugin-install-uninstall-2", "bundled-plugin-install-uninstall-3", ]); - expect(pluginsRuntimeInstallB.lanes.map((lane) => lane.name)).toEqual([ + expect(pluginsRuntimeInstallC.lanes.map((lane) => lane.name)).toEqual([ "bundled-plugin-install-uninstall-4", "bundled-plugin-install-uninstall-5", + ]); + expect(pluginsRuntimeInstallD.lanes.map((lane) => lane.name)).toEqual([ "bundled-plugin-install-uninstall-6", "bundled-plugin-install-uninstall-7", ]);