mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:10:44 +00:00
951 lines
40 KiB
YAML
951 lines
40 KiB
YAML
name: Full Release Validation
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
ref:
|
|
description: Branch, tag, or full commit SHA to validate
|
|
required: true
|
|
default: main
|
|
type: string
|
|
provider:
|
|
description: Provider lane for cross-OS onboarding and the end-to-end agent turn
|
|
required: false
|
|
default: openai
|
|
type: choice
|
|
options:
|
|
- openai
|
|
- anthropic
|
|
- minimax
|
|
mode:
|
|
description: Which cross-OS release lanes to run
|
|
required: false
|
|
default: both
|
|
type: choice
|
|
options:
|
|
- fresh
|
|
- upgrade
|
|
- both
|
|
release_profile:
|
|
description: Release coverage profile for live/Docker/provider breadth
|
|
required: false
|
|
default: stable
|
|
type: choice
|
|
options:
|
|
- minimum
|
|
- stable
|
|
- full
|
|
run_release_soak:
|
|
description: Run exhaustive live/Docker and upgrade-survivor soak lanes; forced on for release_profile=full
|
|
required: false
|
|
default: false
|
|
type: boolean
|
|
rerun_group:
|
|
description: Validation group to run
|
|
required: false
|
|
default: all
|
|
type: choice
|
|
options:
|
|
- all
|
|
- ci
|
|
- plugin-prerelease
|
|
- release-checks
|
|
- install-smoke
|
|
- cross-os
|
|
- live-e2e
|
|
- package
|
|
- qa
|
|
- qa-parity
|
|
- qa-live
|
|
- npm-telegram
|
|
live_suite_filter:
|
|
description: Optional exact live/E2E suite id, or comma-separated QA live lanes such as qa-live-matrix,qa-live-telegram; blank runs all selected live suites
|
|
required: false
|
|
default: ""
|
|
type: string
|
|
cross_os_suite_filter:
|
|
description: Optional focused cross-OS suite filter, e.g. windows/packaged-upgrade or packaged-fresh
|
|
required: false
|
|
default: ""
|
|
type: string
|
|
npm_telegram_package_spec:
|
|
description: Optional published package spec for the package Telegram E2E lane
|
|
required: false
|
|
default: ""
|
|
type: string
|
|
evidence_package_spec:
|
|
description: Optional published package spec to prove in the private release evidence report
|
|
required: false
|
|
default: ""
|
|
type: string
|
|
package_acceptance_package_spec:
|
|
description: Optional published package spec for Package Acceptance; blank uses the SHA-built release artifact
|
|
required: false
|
|
default: ""
|
|
type: string
|
|
npm_telegram_provider_mode:
|
|
description: Provider mode for the package Telegram E2E lane
|
|
required: false
|
|
default: mock-openai
|
|
type: choice
|
|
options:
|
|
- mock-openai
|
|
- live-frontier
|
|
npm_telegram_scenario:
|
|
description: Optional comma-separated Telegram scenario ids for the package Telegram lane
|
|
required: false
|
|
default: ""
|
|
type: string
|
|
|
|
permissions:
|
|
actions: write
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: full-release-validation-${{ inputs.ref }}-${{ inputs.rerun_group }}
|
|
cancel-in-progress: ${{ inputs.ref == 'main' && inputs.rerun_group == 'all' }}
|
|
|
|
env:
|
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
GH_REPO: ${{ github.repository }}
|
|
NODE_VERSION: "24.x"
|
|
PNPM_VERSION: "10.32.1"
|
|
|
|
jobs:
|
|
resolve_target:
|
|
name: Resolve target ref
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 10
|
|
outputs:
|
|
sha: ${{ steps.resolve.outputs.sha }}
|
|
steps:
|
|
- name: Checkout trusted workflow helper
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ github.ref_name }}
|
|
path: workflow
|
|
fetch-depth: 1
|
|
persist-credentials: false
|
|
submodules: false
|
|
|
|
- name: Resolve target SHA
|
|
id: resolve
|
|
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:
|
|
TARGET_REF: ${{ inputs.ref }}
|
|
TARGET_SHA: ${{ steps.resolve.outputs.sha }}
|
|
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
|
|
NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }}
|
|
EVIDENCE_PACKAGE_SPEC: ${{ inputs.evidence_package_spec }}
|
|
PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }}
|
|
RELEASE_PROFILE: ${{ inputs.release_profile }}
|
|
RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'full' }}
|
|
RERUN_GROUP: ${{ inputs.rerun_group }}
|
|
LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}
|
|
CROSS_OS_SUITE_FILTER: ${{ inputs.cross_os_suite_filter }}
|
|
run: |
|
|
{
|
|
echo "## Full release validation"
|
|
echo
|
|
echo "- Target ref: \`${TARGET_REF}\`"
|
|
echo "- Target SHA: \`${TARGET_SHA}\`"
|
|
echo "- Child workflow ref: \`${CHILD_WORKFLOW_REF}\`"
|
|
echo "- Release soak lanes: \`${RUN_RELEASE_SOAK}\`"
|
|
echo "- Rerun group: \`${RERUN_GROUP}\`"
|
|
if [[ -n "${LIVE_SUITE_FILTER// }" ]]; then
|
|
echo "- Live suite filter: \`${LIVE_SUITE_FILTER}\`"
|
|
fi
|
|
if [[ -n "${CROSS_OS_SUITE_FILTER// }" ]]; then
|
|
echo "- Cross-OS suite filter: \`${CROSS_OS_SUITE_FILTER}\`"
|
|
fi
|
|
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" == "all" || "$RERUN_GROUP" == "plugin-prerelease" ]]; then
|
|
echo "- Plugin prerelease: \`Plugin Prerelease\` with \`target_ref=${TARGET_SHA}\`"
|
|
else
|
|
echo "- Plugin prerelease: skipped by rerun group"
|
|
fi
|
|
if [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "release-checks" || "$RERUN_GROUP" == "install-smoke" || "$RERUN_GROUP" == "cross-os" || "$RERUN_GROUP" == "live-e2e" || "$RERUN_GROUP" == "package" || "$RERUN_GROUP" == "qa" || "$RERUN_GROUP" == "qa-parity" || "$RERUN_GROUP" == "qa-live" ]]; 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 "- Published-package Telegram E2E: \`${NPM_TELEGRAM_PACKAGE_SPEC}\`"
|
|
elif [[ "$RERUN_GROUP" == "all" && "$RELEASE_PROFILE" == "full" ]]; then
|
|
echo "- Package Telegram E2E: parent \`release-package-under-test\` artifact"
|
|
else
|
|
echo "- Package Telegram E2E: skipped unless \`release_profile=full\` or \`npm_telegram_package_spec\` is provided"
|
|
fi
|
|
if [[ -n "${EVIDENCE_PACKAGE_SPEC// }" ]]; then
|
|
echo "- Private evidence package proof: \`${EVIDENCE_PACKAGE_SPEC}\`"
|
|
fi
|
|
if [[ -n "${PACKAGE_ACCEPTANCE_PACKAGE_SPEC// }" ]]; then
|
|
echo "- Package Acceptance package spec: \`${PACKAGE_ACCEPTANCE_PACKAGE_SPEC}\`"
|
|
else
|
|
echo "- Package Acceptance package spec: SHA-built release artifact"
|
|
fi
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
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:
|
|
run_id: ${{ steps.dispatch.outputs.run_id }}
|
|
url: ${{ steps.dispatch.outputs.url }}
|
|
conclusion: ${{ steps.dispatch.outputs.conclusion }}
|
|
steps:
|
|
- name: Dispatch and monitor CI
|
|
id: dispatch
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TARGET_REF: ${{ inputs.ref }}
|
|
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
|
|
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
dispatch_and_wait() {
|
|
local workflow="$1"
|
|
shift
|
|
|
|
local before_json dispatch_output run_id status conclusion url poll_count
|
|
before_json="$(gh run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
|
|
|
|
dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
|
|
printf '%s\n' "$dispatch_output"
|
|
run_id="$(
|
|
printf '%s\n' "$dispatch_output" |
|
|
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
|
|
tail -n 1
|
|
)"
|
|
|
|
if [[ -z "$run_id" ]]; then
|
|
for _ in $(seq 1 60); do
|
|
run_id="$(
|
|
BEFORE_IDS="$before_json" gh run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
|
|
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
|
|
)"
|
|
if [[ -n "$run_id" ]]; then
|
|
break
|
|
fi
|
|
sleep 5
|
|
done
|
|
fi
|
|
|
|
if [[ -z "${run_id:-}" ]]; then
|
|
echo "Could not find dispatched run for ${workflow}." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
|
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
|
|
|
|
cancel_child() {
|
|
if [[ -n "${run_id:-}" ]]; then
|
|
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
|
|
gh run cancel "$run_id" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
trap cancel_child EXIT INT TERM
|
|
|
|
poll_count=0
|
|
while true; do
|
|
status="$(gh run view "$run_id" --json status --jq '.status')"
|
|
if [[ "$status" == "completed" ]]; then
|
|
break
|
|
fi
|
|
poll_count=$((poll_count + 1))
|
|
if (( poll_count % 10 == 0 )); then
|
|
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
|
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.status != "completed") | {name, status, url}' || true
|
|
fi
|
|
sleep 30
|
|
done
|
|
trap - EXIT INT TERM
|
|
|
|
conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')"
|
|
url="$(gh run view "$run_id" --json url --jq '.url')"
|
|
echo "${workflow} finished with ${conclusion}: ${url}"
|
|
echo "url=${url}" >> "$GITHUB_OUTPUT"
|
|
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
|
|
if [[ "$conclusion" != "success" ]]; then
|
|
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
|
|
fi
|
|
}
|
|
|
|
{
|
|
echo "### Normal CI"
|
|
echo
|
|
echo "- Target ref: \`${TARGET_REF}\`"
|
|
echo "- Target SHA: \`${TARGET_SHA}\`"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
dispatch_and_wait ci.yml -f target_ref="$TARGET_SHA" -f include_android=true
|
|
|
|
plugin_prerelease:
|
|
name: Run plugin prerelease validation
|
|
needs: [resolve_target]
|
|
if: contains(fromJSON('["all","plugin-prerelease"]'), inputs.rerun_group)
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 300
|
|
outputs:
|
|
run_id: ${{ steps.dispatch.outputs.run_id }}
|
|
url: ${{ steps.dispatch.outputs.url }}
|
|
conclusion: ${{ steps.dispatch.outputs.conclusion }}
|
|
steps:
|
|
- name: Dispatch and monitor plugin prerelease
|
|
id: dispatch
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TARGET_REF: ${{ inputs.ref }}
|
|
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
|
|
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
dispatch_and_wait() {
|
|
local workflow="$1"
|
|
shift
|
|
|
|
local before_json dispatch_output run_id status conclusion url poll_count
|
|
before_json="$(gh run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
|
|
|
|
dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
|
|
printf '%s\n' "$dispatch_output"
|
|
run_id="$(
|
|
printf '%s\n' "$dispatch_output" |
|
|
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
|
|
tail -n 1
|
|
)"
|
|
|
|
if [[ -z "$run_id" ]]; then
|
|
for _ in $(seq 1 60); do
|
|
run_id="$(
|
|
BEFORE_IDS="$before_json" gh run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
|
|
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
|
|
)"
|
|
if [[ -n "$run_id" ]]; then
|
|
break
|
|
fi
|
|
sleep 5
|
|
done
|
|
fi
|
|
|
|
if [[ -z "${run_id:-}" ]]; then
|
|
echo "Could not find dispatched run for ${workflow}." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
|
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
|
|
|
|
cancel_child() {
|
|
if [[ -n "${run_id:-}" ]]; then
|
|
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
|
|
gh run cancel "$run_id" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
trap cancel_child EXIT INT TERM
|
|
|
|
poll_count=0
|
|
while true; do
|
|
status="$(gh run view "$run_id" --json status --jq '.status')"
|
|
if [[ "$status" == "completed" ]]; then
|
|
break
|
|
fi
|
|
poll_count=$((poll_count + 1))
|
|
if (( poll_count % 10 == 0 )); then
|
|
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
|
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.status != "completed") | {name, status, url}' || true
|
|
fi
|
|
sleep 30
|
|
done
|
|
trap - EXIT INT TERM
|
|
|
|
conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')"
|
|
url="$(gh run view "$run_id" --json url --jq '.url')"
|
|
echo "${workflow} finished with ${conclusion}: ${url}"
|
|
echo "url=${url}" >> "$GITHUB_OUTPUT"
|
|
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
|
|
if [[ "$conclusion" != "success" ]]; then
|
|
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
|
|
fi
|
|
}
|
|
|
|
{
|
|
echo "### Plugin prerelease"
|
|
echo
|
|
echo "- Target ref: \`${TARGET_REF}\`"
|
|
echo "- Target SHA: \`${TARGET_SHA}\`"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
dispatch_and_wait plugin-prerelease.yml -f target_ref="$TARGET_SHA" -f expected_sha="$TARGET_SHA" -f full_release_validation=true
|
|
|
|
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:
|
|
run_id: ${{ steps.dispatch.outputs.run_id }}
|
|
url: ${{ steps.dispatch.outputs.url }}
|
|
conclusion: ${{ steps.dispatch.outputs.conclusion }}
|
|
steps:
|
|
- name: Dispatch and monitor release checks
|
|
id: dispatch
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TARGET_REF: ${{ inputs.ref }}
|
|
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
|
|
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
|
|
PROVIDER: ${{ inputs.provider }}
|
|
MODE: ${{ inputs.mode }}
|
|
RELEASE_PROFILE: ${{ inputs.release_profile }}
|
|
RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'full' }}
|
|
RERUN_GROUP: ${{ inputs.rerun_group }}
|
|
LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}
|
|
CROSS_OS_SUITE_FILTER: ${{ inputs.cross_os_suite_filter }}
|
|
PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
dispatch_and_wait() {
|
|
local workflow="$1"
|
|
shift
|
|
|
|
local before_json dispatch_output run_id status conclusion url poll_count
|
|
before_json="$(gh run list --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
|
|
|
|
dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
|
|
printf '%s\n' "$dispatch_output"
|
|
run_id="$(
|
|
printf '%s\n' "$dispatch_output" |
|
|
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
|
|
tail -n 1
|
|
)"
|
|
|
|
if [[ -z "$run_id" ]]; then
|
|
for _ in $(seq 1 60); do
|
|
run_id="$(
|
|
BEFORE_IDS="$before_json" gh run list --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
|
|
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
|
|
)"
|
|
if [[ -n "$run_id" ]]; then
|
|
break
|
|
fi
|
|
sleep 5
|
|
done
|
|
fi
|
|
|
|
if [[ -z "${run_id:-}" ]]; then
|
|
echo "Could not find dispatched run for ${workflow}." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
|
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
|
|
|
|
cancel_child() {
|
|
if [[ -n "${run_id:-}" ]]; then
|
|
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
|
|
gh run cancel "$run_id" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
trap cancel_child EXIT INT TERM
|
|
|
|
poll_count=0
|
|
while true; do
|
|
status="$(gh run view "$run_id" --json status --jq '.status')"
|
|
if [[ "$status" == "completed" ]]; then
|
|
break
|
|
fi
|
|
poll_count=$((poll_count + 1))
|
|
if (( poll_count % 10 == 0 )); then
|
|
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
|
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.status != "completed") | {name, status, url}' || true
|
|
fi
|
|
sleep 30
|
|
done
|
|
trap - EXIT INT TERM
|
|
|
|
conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')"
|
|
url="$(gh run view "$run_id" --json url --jq '.url')"
|
|
echo "${workflow} finished with ${conclusion}: ${url}"
|
|
echo "url=${url}" >> "$GITHUB_OUTPUT"
|
|
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
|
|
if [[ "$conclusion" != "success" ]]; then
|
|
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
|
|
fi
|
|
}
|
|
|
|
{
|
|
echo "### Release/live/Docker/QA validation"
|
|
echo
|
|
echo "- Target ref: \`${TARGET_REF}\`"
|
|
echo "- Target SHA: \`${TARGET_SHA}\`"
|
|
echo "- Provider: \`${PROVIDER}\`"
|
|
echo "- Cross-OS mode: \`${MODE}\`"
|
|
echo "- Release profile: \`${RELEASE_PROFILE}\`"
|
|
echo "- Release soak lanes: \`${RUN_RELEASE_SOAK}\`"
|
|
echo "- Rerun group: \`${RERUN_GROUP}\`"
|
|
if [[ -n "${LIVE_SUITE_FILTER// }" ]]; then
|
|
echo "- Live suite filter: \`${LIVE_SUITE_FILTER}\`"
|
|
fi
|
|
if [[ -n "${CROSS_OS_SUITE_FILTER// }" ]]; then
|
|
echo "- Cross-OS suite filter: \`${CROSS_OS_SUITE_FILTER}\`"
|
|
fi
|
|
if [[ -n "${PACKAGE_ACCEPTANCE_PACKAGE_SPEC// }" ]]; then
|
|
echo "- Package Acceptance package spec: \`${PACKAGE_ACCEPTANCE_PACKAGE_SPEC}\`"
|
|
fi
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
child_rerun_group="$RERUN_GROUP"
|
|
if [[ "$child_rerun_group" == "release-checks" ]]; then
|
|
child_rerun_group=all
|
|
fi
|
|
|
|
args=(
|
|
-f ref="$TARGET_SHA"
|
|
-f expected_sha="$TARGET_SHA"
|
|
-f provider="$PROVIDER"
|
|
-f mode="$MODE"
|
|
-f release_profile="$RELEASE_PROFILE"
|
|
-f run_release_soak="$RUN_RELEASE_SOAK"
|
|
-f rerun_group="$child_rerun_group"
|
|
)
|
|
if [[ -n "${LIVE_SUITE_FILTER// }" ]]; then
|
|
args+=(-f live_suite_filter="$LIVE_SUITE_FILTER")
|
|
fi
|
|
if [[ -n "${CROSS_OS_SUITE_FILTER// }" ]]; then
|
|
args+=(-f cross_os_suite_filter="$CROSS_OS_SUITE_FILTER")
|
|
fi
|
|
if [[ -n "${PACKAGE_ACCEPTANCE_PACKAGE_SPEC// }" ]]; then
|
|
args+=(-f package_acceptance_package_spec="$PACKAGE_ACCEPTANCE_PACKAGE_SPEC")
|
|
fi
|
|
|
|
dispatch_and_wait openclaw-release-checks.yml "${args[@]}"
|
|
|
|
prepare_release_package:
|
|
name: Prepare release package artifact
|
|
needs: [resolve_target]
|
|
if: ${{ inputs.npm_telegram_package_spec == '' && inputs.rerun_group == 'all' && inputs.release_profile == 'full' }}
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 60
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
outputs:
|
|
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
|
|
with:
|
|
persist-credentials: false
|
|
ref: ${{ github.ref_name }}
|
|
fetch-depth: 0
|
|
|
|
- name: Set artifact metadata
|
|
id: artifact
|
|
run: echo "name=release-package-under-test" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
pnpm-version: ${{ env.PNPM_VERSION }}
|
|
install-bun: "true"
|
|
install-deps: "false"
|
|
|
|
- name: Resolve release package artifact
|
|
id: package
|
|
shell: bash
|
|
env:
|
|
PACKAGE_REF: ${{ needs.resolve_target.outputs.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
node scripts/resolve-openclaw-package-candidate.mjs \
|
|
--source ref \
|
|
--package-ref "$PACKAGE_REF" \
|
|
--output-dir .artifacts/docker-e2e-package \
|
|
--output-name openclaw-current.tgz \
|
|
--metadata .artifacts/docker-e2e-package/package-candidate.json \
|
|
--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
|
|
echo "- Artifact: \`release-package-under-test\`"
|
|
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
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: release-package-under-test
|
|
path: |
|
|
.artifacts/docker-e2e-package/openclaw-current.tgz
|
|
.artifacts/docker-e2e-package/package-candidate.json
|
|
if-no-files-found: error
|
|
|
|
npm_telegram:
|
|
name: Run package Telegram E2E
|
|
needs: [resolve_target, prepare_release_package]
|
|
if: ${{ always() && contains(fromJSON('["all","npm-telegram"]'), inputs.rerun_group) && (inputs.npm_telegram_package_spec != '' || (inputs.rerun_group == 'all' && inputs.release_profile == 'full')) }}
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 120
|
|
outputs:
|
|
run_id: ${{ steps.dispatch.outputs.run_id }}
|
|
url: ${{ steps.dispatch.outputs.url }}
|
|
conclusion: ${{ steps.dispatch.outputs.conclusion }}
|
|
steps:
|
|
- name: Dispatch and monitor npm Telegram E2E
|
|
id: dispatch
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
|
|
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
|
|
PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec }}
|
|
PACKAGE_ARTIFACT_NAME: ${{ needs.prepare_release_package.outputs.artifact_name }}
|
|
PREPARE_PACKAGE_RESULT: ${{ needs.prepare_release_package.result }}
|
|
PROVIDER_MODE: ${{ inputs.npm_telegram_provider_mode }}
|
|
SCENARIO: ${{ inputs.npm_telegram_scenario }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
before_json="$(gh run list --workflow npm-telegram-beta-e2e.yml --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
|
|
|
|
args=(-f package_spec="${PACKAGE_SPEC:-openclaw@beta}" -f harness_ref="$TARGET_SHA" -f provider_mode="$PROVIDER_MODE")
|
|
if [[ -z "${PACKAGE_SPEC// }" ]]; then
|
|
if [[ "$PREPARE_PACKAGE_RESULT" != "success" || -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then
|
|
echo "Full release Telegram requires either npm_telegram_package_spec or a prepared release-package-under-test artifact." >&2
|
|
exit 1
|
|
fi
|
|
args+=(
|
|
-f package_artifact_name="$PACKAGE_ARTIFACT_NAME"
|
|
-f package_artifact_run_id="${GITHUB_RUN_ID}"
|
|
-f package_label="full-release-${TARGET_SHA:0:12}"
|
|
)
|
|
fi
|
|
if [[ -n "${SCENARIO// }" ]]; then
|
|
args+=(-f scenario="$SCENARIO")
|
|
fi
|
|
|
|
gh workflow run npm-telegram-beta-e2e.yml --ref "$CHILD_WORKFLOW_REF" "${args[@]}"
|
|
|
|
run_id=""
|
|
for _ in $(seq 1 60); do
|
|
run_id="$(
|
|
BEFORE_IDS="$before_json" gh run list --workflow npm-telegram-beta-e2e.yml --event workflow_dispatch --limit 50 --json databaseId,createdAt \
|
|
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
|
|
)"
|
|
if [[ -n "$run_id" ]]; then
|
|
break
|
|
fi
|
|
sleep 5
|
|
done
|
|
|
|
if [[ -z "$run_id" ]]; then
|
|
echo "Could not find dispatched run for npm-telegram-beta-e2e.yml." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Dispatched npm-telegram-beta-e2e.yml: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
|
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
|
|
|
|
cancel_child() {
|
|
if [[ -n "${run_id:-}" ]]; then
|
|
echo "Cancelling child workflow npm-telegram-beta-e2e.yml: ${run_id}" >&2
|
|
gh run cancel "$run_id" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
trap cancel_child EXIT INT TERM
|
|
|
|
poll_count=0
|
|
while true; do
|
|
status="$(gh run view "$run_id" --json status --jq '.status')"
|
|
if [[ "$status" == "completed" ]]; then
|
|
break
|
|
fi
|
|
poll_count=$((poll_count + 1))
|
|
if (( poll_count % 10 == 0 )); then
|
|
echo "Still waiting on npm-telegram-beta-e2e.yml: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
|
|
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.status != "completed") | {name, status, url}' || true
|
|
fi
|
|
sleep 30
|
|
done
|
|
trap - EXIT INT TERM
|
|
|
|
conclusion="$(gh run view "$run_id" --json conclusion --jq '.conclusion')"
|
|
url="$(gh run view "$run_id" --json url --jq '.url')"
|
|
echo "npm-telegram-beta-e2e.yml finished with ${conclusion}: ${url}"
|
|
echo "url=${url}" >> "$GITHUB_OUTPUT"
|
|
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
|
|
if [[ "$conclusion" != "success" ]]; then
|
|
gh run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
|
|
fi
|
|
|
|
summary:
|
|
name: Verify full validation
|
|
needs: [resolve_target, normal_ci, plugin_prerelease, release_checks, npm_telegram]
|
|
if: always()
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: Request private evidence update
|
|
env:
|
|
RELEASE_PRIVATE_DISPATCH_TOKEN: ${{ secrets.OPENCLAW_RELEASES_PRIVATE_DISPATCH_TOKEN }}
|
|
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
|
|
fi
|
|
|
|
release_id="${TARGET_REF#refs/tags/}"
|
|
release_id="${release_id#v}"
|
|
if [[ "$PACKAGE_SPEC" =~ ^openclaw@(.+)$ ]]; then
|
|
release_id="${BASH_REMATCH[1]}"
|
|
fi
|
|
release_id="$(printf '%s' "$release_id" | tr '/:@ ' '----' | tr -cd 'A-Za-z0-9._-')"
|
|
if [[ -z "$release_id" ]]; then
|
|
echo "::error::Could not derive release evidence id from target ref '${TARGET_REF}'."
|
|
exit 1
|
|
fi
|
|
|
|
payload="$(
|
|
jq -cn \
|
|
--arg full_validation_run_id "$GITHUB_RUN_ID_VALUE" \
|
|
--arg release_id "$release_id" \
|
|
--arg release_ref "$TARGET_REF" \
|
|
--arg package_spec "$PACKAGE_SPEC" \
|
|
--arg notes "Automatically requested by Full Release Validation ${GITHUB_RUN_ID_VALUE} after child workflows completed; the parent summary re-checks current child run conclusions." \
|
|
'{
|
|
event_type: "openclaw_full_release_validation_completed",
|
|
client_payload: {
|
|
full_validation_run_id: $full_validation_run_id,
|
|
release_id: $release_id,
|
|
release_ref: $release_ref,
|
|
package_spec: $package_spec,
|
|
notes: $notes
|
|
}
|
|
}'
|
|
)"
|
|
|
|
curl --fail-with-body \
|
|
-X POST \
|
|
-H "Accept: application/vnd.github+json" \
|
|
-H "Authorization: Bearer ${RELEASE_PRIVATE_DISPATCH_TOKEN}" \
|
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
https://api.github.com/repos/openclaw/releases-private/dispatches \
|
|
-d "$payload"
|
|
|
|
- name: Verify child workflow results
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
NORMAL_CI_RUN_ID: ${{ needs.normal_ci.outputs.run_id }}
|
|
PLUGIN_PRERELEASE_RUN_ID: ${{ needs.plugin_prerelease.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 }}
|
|
PLUGIN_PRERELEASE_RESULT: ${{ needs.plugin_prerelease.result }}
|
|
RELEASE_CHECKS_RESULT: ${{ needs.release_checks.result }}
|
|
NPM_TELEGRAM_RESULT: ${{ needs.npm_telegram.result }}
|
|
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
check_child() {
|
|
local label="$1"
|
|
local run_id="$2"
|
|
local required="$3"
|
|
|
|
if [[ -z "${run_id// }" ]]; then
|
|
if [[ "$required" == "0" ]]; then
|
|
echo "${label}: skipped"
|
|
return 0
|
|
fi
|
|
echo "::error::${label} did not record a child run id."
|
|
return 1
|
|
fi
|
|
|
|
local run_json status conclusion url attempt head_sha
|
|
run_json="$(gh run view "$run_id" --json status,conclusion,url,attempt,headSha,jobs)"
|
|
status="$(jq -r '.status' <<< "$run_json")"
|
|
conclusion="$(jq -r '.conclusion' <<< "$run_json")"
|
|
url="$(jq -r '.url' <<< "$run_json")"
|
|
attempt="$(jq -r '.attempt' <<< "$run_json")"
|
|
head_sha="$(jq -r '.headSha // ""' <<< "$run_json")"
|
|
echo "${label}: ${status}/${conclusion} attempt ${attempt} head ${head_sha}: ${url}"
|
|
|
|
if [[ -n "${TARGET_SHA// }" && "$head_sha" != "$TARGET_SHA" ]]; then
|
|
echo "::error::${label} child run used ${head_sha}, expected ${TARGET_SHA}. Dispatch Full Release Validation from a ref pinned to the target SHA, not a moving branch."
|
|
return 1
|
|
fi
|
|
|
|
if [[ "$status" != "completed" || "$conclusion" != "success" ]]; then
|
|
echo "::error::${label} child run ended with ${status}/${conclusion}: ${url}"
|
|
jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, status, conclusion, url}' <<< "$run_json" || true
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
append_child_overview() {
|
|
{
|
|
echo
|
|
echo "### Child workflow overview"
|
|
echo
|
|
echo "| Child | Result | Minutes | Head SHA | Run |"
|
|
echo "| --- | --- | ---: | --- | --- |"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
append_child_row() {
|
|
local label="$1"
|
|
local run_id="$2"
|
|
local result="$3"
|
|
|
|
if [[ -z "${run_id// }" ]]; then
|
|
echo "| \`${label}\` | \`${result}\` | | skipped |" >> "$GITHUB_STEP_SUMMARY"
|
|
return 0
|
|
fi
|
|
|
|
local run_json row
|
|
run_json="$(gh run view "$run_id" --json status,conclusion,url,createdAt,updatedAt,headSha)"
|
|
row="$(
|
|
jq -r --arg label "$label" '
|
|
def ts: fromdateiso8601;
|
|
. as $run |
|
|
($run.createdAt // "") as $created |
|
|
($run.updatedAt // "") as $updated |
|
|
(if ($created | length) > 0 and ($updated | length) > 0
|
|
then (((($updated | ts) - ($created | ts)) / 60) * 10 | round / 10 | tostring)
|
|
else ""
|
|
end) as $minutes |
|
|
($run.headSha // "") as $head |
|
|
"| `" + $label + "` | `" + ($run.status // "") + "/" + ($run.conclusion // "") + "` | " + $minutes + " | `" + $head + "` | [run](" + ($run.url // "") + ") |"
|
|
' <<< "$run_json"
|
|
)"
|
|
echo "$row" >> "$GITHUB_STEP_SUMMARY"
|
|
}
|
|
|
|
append_child_row "normal_ci" "$NORMAL_CI_RUN_ID" "$NORMAL_CI_RESULT"
|
|
append_child_row "plugin_prerelease" "$PLUGIN_PRERELEASE_RUN_ID" "$PLUGIN_PRERELEASE_RESULT"
|
|
append_child_row "release_checks" "$RELEASE_CHECKS_RUN_ID" "$RELEASE_CHECKS_RESULT"
|
|
append_child_row "npm_telegram" "$NPM_TELEGRAM_RUN_ID" "$NPM_TELEGRAM_RESULT"
|
|
}
|
|
|
|
summarize_child_timing() {
|
|
local label="$1"
|
|
local run_id="$2"
|
|
if [[ -z "${run_id// }" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
{
|
|
echo
|
|
echo "### Slowest jobs: ${label}"
|
|
echo
|
|
gh run view "$run_id" --json jobs --jq '
|
|
def ts: fromdateiso8601;
|
|
"| Job | Result | Minutes |",
|
|
"| --- | --- | ---: |",
|
|
([.jobs[]
|
|
| select(.startedAt != "0001-01-01T00:00:00Z" and .completedAt != "0001-01-01T00:00:00Z")
|
|
| . + {durationMin: ((((.completedAt | ts) - (.startedAt | ts)) / 60) * 10 | round / 10)}
|
|
| {name, conclusion, durationMin}]
|
|
| sort_by(.durationMin)
|
|
| reverse
|
|
| .[0:10]
|
|
| map("| `" + (.name | gsub("\\|"; "\\|")) + "` | `" + ((.conclusion // "") | tostring) + "` | " + (.durationMin | tostring) + " |")
|
|
| .[])
|
|
' || echo "_Unable to summarize jobs for run ${run_id}._"
|
|
echo
|
|
echo "### Longest queues: ${label}"
|
|
echo
|
|
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq ".jobs[] | @json" | jq -sr '
|
|
def ts: fromdateiso8601;
|
|
"| Job | Result | Queue minutes | Run minutes |",
|
|
"| --- | --- | ---: | ---: |",
|
|
([.[]
|
|
| select(.created_at != null and .started_at != null)
|
|
| . + {
|
|
queueMin: ((((.started_at | ts) - (.created_at | ts)) / 60) * 10 | round / 10),
|
|
durationMin: (if .completed_at == null then null else ((((.completed_at | ts) - (.started_at | ts)) / 60) * 10 | round / 10) end)
|
|
}
|
|
| select(.queueMin > 0)
|
|
| {name, conclusion, queueMin, durationMin}]
|
|
| sort_by(.queueMin)
|
|
| reverse
|
|
| .[0:10]
|
|
| map("| `" + (.name | gsub("\\|"; "\\|")) + "` | `" + ((.conclusion // "") | tostring) + "` | " + (.queueMin | tostring) + " | " + ((.durationMin // "") | tostring) + " |")
|
|
| .[])
|
|
' || echo "_Unable to summarize queue times for run ${run_id}._"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
}
|
|
|
|
failed=0
|
|
|
|
append_child_overview
|
|
|
|
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 [[ "$PLUGIN_PRERELEASE_RESULT" == "skipped" && -z "${PLUGIN_PRERELEASE_RUN_ID// }" ]]; then
|
|
check_child "plugin_prerelease" "" 0 || failed=1
|
|
else
|
|
check_child "plugin_prerelease" "$PLUGIN_PRERELEASE_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
|
|
else
|
|
check_child "npm_telegram" "$NPM_TELEGRAM_RUN_ID" 1 || failed=1
|
|
fi
|
|
|
|
summarize_child_timing "normal_ci" "$NORMAL_CI_RUN_ID"
|
|
summarize_child_timing "plugin_prerelease" "$PLUGIN_PRERELEASE_RUN_ID"
|
|
summarize_child_timing "release_checks" "$RELEASE_CHECKS_RUN_ID"
|
|
summarize_child_timing "npm_telegram" "$NPM_TELEGRAM_RUN_ID"
|
|
|
|
exit "$failed"
|