ci: speed up full release validation

This commit is contained in:
Peter Steinberger
2026-04-28 09:02:00 +01:00
parent c7af9c765c
commit a811e164e3
10 changed files with 456 additions and 59 deletions

View File

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

View File

@@ -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" \

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -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 <ref> [--expected-sha <sha>] [--fallback-ok] [--github-output <file>]
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

View File

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

View File

@@ -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",
},
{

View File

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