Files
openclaw/.github/workflows/full-release-validation.yml
Peter Steinberger dbdc61e897 fix(ci): validate frozen release candidates safely (#104697)
* fix(ci): validate frozen release candidates safely

* test(ci): align release workflow types

* fix(ci): validate frozen release tags safely
2026-07-11 14:10:41 -07:00

2039 lines
94 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
target_context_ref:
description: Optional canonical release branch or tag context for an exact-SHA target
required: false
default: ""
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:
- beta
- stable
- full
run_release_soak:
description: Run exhaustive live/Docker and upgrade-survivor soak lanes; forced on for stable and full release profiles
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
- performance
reuse_evidence:
description: Reuse the newest prior green full validation only for the exact same target SHA and inputs
required: false
default: true
type: boolean
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 focused package Telegram E2E rerun
required: false
default: ""
type: string
release_package_spec:
description: Optional published package spec for release checks and package lanes; blank builds a SHA package artifact
required: false
default: ""
type: string
evidence_package_spec:
description: Optional published package spec to prove in the release evidence report
required: false
default: ""
type: string
dispatch_release_evidence:
description: Dispatch the validated run to openclaw/releases after child proof succeeds
required: false
default: false
type: boolean
package_acceptance_package_spec:
description: Optional published package spec for Package Acceptance; blank uses the SHA-built release artifact
required: false
default: ""
type: string
codex_plugin_spec:
description: Optional Codex plugin install spec for live Docker package checks; blank derives from release_package_spec or packs the selected ref
required: false
default: ""
type: string
npm_telegram_provider_mode:
description: Provider mode for the focused package Telegram E2E rerun
required: false
default: mock-openai
type: choice
options:
- mock-openai
- live-frontier
npm_telegram_scenario:
description: Optional comma-separated Telegram scenario ids for the focused package Telegram E2E rerun
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') || startsWith(inputs.ref, 'tideclaw/alpha/') || startsWith(inputs.ref, 'release/') }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
GH_REPO: ${{ github.repository }}
# Read retries and one-shot dispatch recovery share this classifier; dispatch POSTs never retry.
GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN: "HTTP 5[0-9][0-9]|Server Error|error connecting to|context deadline exceeded|connection reset by peer|connection refused|TLS handshake timeout|i/o timeout|network is unreachable|(^|[^A-Za-z0-9_])EOF([^A-Za-z0-9_]|$)|ETIMEDOUT|ECONNRESET|EAI_AGAIN"
NODE_VERSION: "24.15.0"
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.sha }}
path: workflow
fetch-depth: 1
persist-credentials: false
submodules: false
- name: Resolve target SHA
id: resolve
env:
TARGET_REF: ${{ inputs.ref }}
TARGET_CONTEXT_REF: ${{ inputs.target_context_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 }}
RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }}
EVIDENCE_PACKAGE_SPEC: ${{ inputs.evidence_package_spec }}
PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }}
CODEX_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'stable' || 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" == "performance" ]]; then
echo "- Product performance: \`OpenClaw Performance\` with \`target_ref=${TARGET_SHA}\`"
else
echo "- Product performance: 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 "${RELEASE_PACKAGE_SPEC// }" ]]; then
echo "- Published release package: \`${RELEASE_PACKAGE_SPEC}\`"
fi
if [[ "$RERUN_GROUP" == "npm-telegram" && -n "${NPM_TELEGRAM_PACKAGE_SPEC// }" ]]; then
echo "- Published-package Telegram E2E: \`${NPM_TELEGRAM_PACKAGE_SPEC}\`"
elif [[ "$RERUN_GROUP" == "npm-telegram" && -n "${RELEASE_PACKAGE_SPEC// }" ]]; then
echo "- Published-package Telegram E2E: \`${RELEASE_PACKAGE_SPEC}\`"
elif [[ "$RERUN_GROUP" == "npm-telegram" ]]; then
echo "- Package Telegram E2E: focused rerun requires \`release_package_spec\` or \`npm_telegram_package_spec\`"
elif [[ "$RERUN_GROUP" == "all" || "$RERUN_GROUP" == "release-checks" || "$RERUN_GROUP" == "package" ]]; then
echo "- Package Telegram E2E: OpenClaw Release Checks Package Acceptance"
else
echo "- Package Telegram E2E: skipped by rerun group"
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}\`"
elif [[ -n "${RELEASE_PACKAGE_SPEC// }" ]]; then
echo "- Package Acceptance package spec: \`${RELEASE_PACKAGE_SPEC}\`"
else
echo "- Package Acceptance package spec: SHA-built release artifact"
fi
if [[ -n "${CODEX_PLUGIN_SPEC// }" ]]; then
echo "- Codex plugin spec: \`${CODEX_PLUGIN_SPEC}\`"
fi
} >> "$GITHUB_STEP_SUMMARY"
evidence_reuse:
name: Check for reusable validation evidence
needs: [resolve_target]
if: inputs.rerun_group == 'all' && inputs.reuse_evidence && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release-ci/'))
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
reuse: ${{ steps.find.outputs.reuse }}
evidence_run_id: ${{ steps.find.outputs.evidence_run_id }}
evidence_root_run_id: ${{ steps.find.outputs.evidence_root_run_id }}
evidence_run_url: ${{ steps.find.outputs.evidence_run_url }}
evidence_sha: ${{ steps.find.outputs.evidence_sha }}
evidence_manifest: ${{ steps.find.outputs.evidence_manifest }}
changed_paths: ${{ steps.find.outputs.changed_paths }}
steps:
- name: Checkout trusted workflow helper
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ github.sha }}
path: workflow
fetch-depth: 1
persist-credentials: false
submodules: false
- name: Checkout target SHA
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.resolve_target.outputs.sha }}
path: target
fetch-depth: 1
persist-credentials: false
submodules: false
- name: Find reusable validation evidence
id: find
env:
GH_TOKEN: ${{ github.token }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
WORKFLOW_REF: ${{ github.ref_name }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full' }}
PROVIDER: ${{ inputs.provider }}
MODE: ${{ inputs.mode }}
TARGET_CONTEXT_REF: ${{ inputs.target_context_ref }}
LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}
CROSS_OS_SUITE_FILTER: ${{ inputs.cross_os_suite_filter }}
RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }}
PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }}
CODEX_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }}
run: |
set -euo pipefail
# Lane-selection inputs must match the prior run's manifest exactly;
# a default-input run must not stand in for a focused one.
inputs_json="$(jq -nc \
--arg provider "$PROVIDER" \
--arg mode "$MODE" \
--arg targetContextRef "$TARGET_CONTEXT_REF" \
--arg liveSuiteFilter "$LIVE_SUITE_FILTER" \
--arg crossOsSuiteFilter "$CROSS_OS_SUITE_FILTER" \
--arg releasePackageSpec "$RELEASE_PACKAGE_SPEC" \
--arg packageAcceptancePackageSpec "$PACKAGE_ACCEPTANCE_PACKAGE_SPEC" \
--arg codexPluginSpec "$CODEX_PLUGIN_SPEC" \
'{
provider: $provider,
mode: $mode,
targetContextRef: $targetContextRef,
liveSuiteFilter: $liveSuiteFilter,
crossOsSuiteFilter: $crossOsSuiteFilter,
releasePackageSpec: $releasePackageSpec,
packageAcceptancePackageSpec: $packageAcceptancePackageSpec,
codexPluginSpec: $codexPluginSpec
}')"
bash workflow/scripts/github/find-reusable-release-validation.sh \
--target-sha "$TARGET_SHA" \
--workflow-sha "$GITHUB_SHA" \
--workflow-ref "$WORKFLOW_REF" \
--release-profile "$RELEASE_PROFILE" \
--run-release-soak "$RUN_RELEASE_SOAK" \
--inputs-json "$inputs_json" \
--repo "$GITHUB_REPOSITORY" \
--repo-dir target \
--github-output "$GITHUB_OUTPUT"
- name: Summarize evidence reuse
env:
REUSE: ${{ steps.find.outputs.reuse }}
REUSE_REASON: ${{ steps.find.outputs.reuse_reason }}
EVIDENCE_RUN_URL: ${{ steps.find.outputs.evidence_run_url }}
EVIDENCE_SHA: ${{ steps.find.outputs.evidence_sha }}
CHANGED_PATHS: ${{ steps.find.outputs.changed_paths }}
run: |
changed_paths_summary="$(jq -r 'if length == 0 then "none" else join(", ") end' <<< "${CHANGED_PATHS:-[]}")"
{
echo "## Validation evidence reuse"
echo
if [[ "$REUSE" == "true" ]]; then
echo "- Reusing evidence: ${EVIDENCE_RUN_URL}"
echo "- Evidence SHA: \`${EVIDENCE_SHA}\`"
echo "- Exact-target reuse changed paths: \`${changed_paths_summary}\`"
else
echo "- No reusable evidence: ${REUSE_REASON:-unknown}"
fi
} >> "$GITHUB_STEP_SUMMARY"
docker_runtime_assets_preflight:
name: Verify Docker runtime image assets
needs: [resolve_target, evidence_reuse]
if: ${{ always() && needs.resolve_target.result == 'success' && inputs.rerun_group == 'all' && needs.evidence_reuse.outputs.reuse != 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout target SHA
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ needs.resolve_target.outputs.sha }}
fetch-depth: 1
persist-credentials: false
- name: Verify Docker runtime-assets prune path
env:
DOCKER_BUILDKIT: "1"
run: |
set -euo pipefail
timeout --kill-after=30s 15m docker build \
--target runtime-assets \
--build-arg OPENCLAW_EXTENSIONS="diagnostics-otel,codex" \
.
normal_ci:
name: Run normal full CI
needs: [resolve_target, evidence_reuse]
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["all","ci"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: ${{ inputs.release_profile != 'minimum' && 240 || 60 }}
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_CONTEXT_REF: ${{ inputs.target_context_ref }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
PARENT_WORKFLOW_SHA: ${{ github.sha }}
run: |
set -euo pipefail
dispatch_and_wait() {
local workflow="$1"
local dispatch_run_name="$2"
shift 2
local dispatch_output dispatch_status matches_json match_count run_id status conclusion url poll_count child_head_sha encoded_workflow_ref current_workflow_sha
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
set +e
output="$(gh "$@" 2>&1)"
status=$?
set -e
if [[ "$status" -eq 0 ]]; then
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"HTTP 429"* || "$output" == *"abuse detection"* || "$output" == *"Sorry. Your account was suspended"* || "$output" =~ $GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
fi
printf '%s\n' "$output" >&2
return "$status"
done
printf '%s\n' "$output" >&2
return "$status"
}
encoded_workflow_ref="$(jq -rn --arg value "$CHILD_WORKFLOW_REF" '$value | @uri')"
current_workflow_sha="$(
gh_with_retry api "repos/${GITHUB_REPOSITORY}/commits/${encoded_workflow_ref}" --jq .sha
)"
if [[ "$current_workflow_sha" != "$PARENT_WORKFLOW_SHA" ]]; then
echo "::error::Child workflow ref ${CHILD_WORKFLOW_REF} moved to ${current_workflow_sha}, expected ${PARENT_WORKFLOW_SHA}; refusing dispatch." >&2
return 1
fi
# A failed dispatch POST can still create a run. Never retry it; recover only by exact run name.
set +e
dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
dispatch_status=$?
set -e
printf '%s\n' "$dispatch_output"
if [[ "$dispatch_status" -ne 0 && ! "$dispatch_output" =~ $GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN ]]; then
echo "::error::${workflow} dispatch failed with non-ambiguous status ${dispatch_status}; refusing adoption polling." >&2
exit "$dispatch_status"
fi
run_id=""
for _ in $(seq 1 60); do
if matches_json="$(
DISPATCH_RUN_NAME="$dispatch_run_name" CHILD_WORKFLOW_REF="$CHILD_WORKFLOW_REF" \
gh_with_retry api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/runs" \
-F event=workflow_dispatch \
-F per_page=100 \
--jq '[.workflow_runs[] | select(.display_title == env.DISPATCH_RUN_NAME and .head_branch == env.CHILD_WORKFLOW_REF) | .id]'
)"; then
match_count="$(jq 'length' <<< "$matches_json")"
if (( match_count > 1 )); then
echo "::error::Multiple runs matched ${dispatch_run_name}; refusing to guess." >&2
exit 1
fi
if (( match_count == 1 )); then
run_id="$(jq -r '.[0]' <<< "$matches_json")"
break
fi
fi
sleep 5
done
if [[ -z "$run_id" ]]; then
echo "::error::Could not find exact dispatched run ${dispatch_run_name}; dispatch status ${dispatch_status}. The dispatch was not retried to avoid creating a duplicate child." >&2
exit 1
fi
if [[ "$dispatch_status" -ne 0 ]]; then
echo "::warning::${workflow} dispatch returned status ${dispatch_status}; adopted exact run ${run_id}." >&2
fi
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
fetch_child_run_json() {
gh_with_retry api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
}
fetch_child_jobs() {
gh_with_retry api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq '.jobs[]'
}
fail_fast_failed_jobs() {
local failed_jobs_json
failed_jobs_json="$(
fetch_child_jobs |
jq -s '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
)"
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
echo "::error::${workflow} has failed child jobs before the workflow completed; cancelling the remaining matrix."
jq '.[] | {name, conclusion, url: .html_url}' <<< "$failed_jobs_json"
cancel_child
trap - EXIT INT TERM
exit 1
fi
}
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
child_head_sha="$(fetch_child_run_json | jq -r '.head_sha // ""')"
if [[ "$child_head_sha" != "$PARENT_WORKFLOW_SHA" ]]; then
echo "::error::${workflow} child run used workflow SHA ${child_head_sha}, expected parent workflow SHA ${PARENT_WORKFLOW_SHA}."
cancel_child
trap - EXIT INT TERM
exit 1
fi
poll_count=0
while true; do
status="$(fetch_child_run_json | jq -r '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
poll_count=$((poll_count + 1))
if (( poll_count % 2 == 0 )); then
fail_fast_failed_jobs
fi
if (( poll_count % 10 == 0 )); then
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
fi
sleep 30
done
trap - EXIT INT TERM
conclusion="$(fetch_child_run_json | jq -r '.conclusion // ""')"
url="$(fetch_child_run_json | jq -r '.html_url')"
echo "${workflow} finished with ${conclusion}: ${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
if [[ "$conclusion" != "success" ]]; then
fetch_child_jobs | jq 'select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url: .html_url}' || true
exit 1
fi
}
{
echo "### Normal CI"
echo
echo "- Target ref: \`${TARGET_REF}\`"
echo "- Target SHA: \`${TARGET_SHA}\`"
} >> "$GITHUB_STEP_SUMMARY"
dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-ci"
dispatch_run_name="CI ${dispatch_id}"
args=(-f target_ref="$TARGET_SHA" -f include_android=true -f dispatch_id="$dispatch_id")
if [[ "$TARGET_REF" =~ ^v[0-9]{4}\.[0-9]+\.[0-9]+(-(alpha|beta)\.[0-9]+)?$ ]]; then
args+=(-f historical_target_tag="$TARGET_REF")
elif [[ "$TARGET_CONTEXT_REF" =~ ^v[0-9]{4}\.[0-9]+\.[0-9]+(-(alpha|beta)\.[0-9]+)?$ ]]; then
args+=(-f historical_target_tag="$TARGET_CONTEXT_REF")
elif [[ "$TARGET_CONTEXT_REF" =~ ^(release/[0-9]{4}\.[0-9]+\.[0-9]+|extended-stable/[0-9]{4}\.[0-9]+\.33)$ ]]; then
args+=(-f release_candidate_ref="$TARGET_CONTEXT_REF")
fi
dispatch_and_wait ci.yml "$dispatch_run_name" "${args[@]}"
plugin_prerelease:
name: Run plugin prerelease validation
needs: [resolve_target, evidence_reuse]
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["all","plugin-prerelease"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: ${{ inputs.release_profile == 'full' && 300 || inputs.release_profile == 'stable' && 240 || 60 }}
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 }}
PARENT_WORKFLOW_SHA: ${{ github.sha }}
run: |
set -euo pipefail
dispatch_and_wait() {
local workflow="$1"
local dispatch_run_name="$2"
shift 2
local dispatch_output dispatch_status matches_json match_count run_id status conclusion url poll_count child_head_sha encoded_workflow_ref current_workflow_sha
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
set +e
output="$(gh "$@" 2>&1)"
status=$?
set -e
if [[ "$status" -eq 0 ]]; then
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"HTTP 429"* || "$output" == *"abuse detection"* || "$output" == *"Sorry. Your account was suspended"* || "$output" =~ $GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
fi
printf '%s\n' "$output" >&2
return "$status"
done
printf '%s\n' "$output" >&2
return "$status"
}
encoded_workflow_ref="$(jq -rn --arg value "$CHILD_WORKFLOW_REF" '$value | @uri')"
current_workflow_sha="$(
gh_with_retry api "repos/${GITHUB_REPOSITORY}/commits/${encoded_workflow_ref}" --jq .sha
)"
if [[ "$current_workflow_sha" != "$PARENT_WORKFLOW_SHA" ]]; then
echo "::error::Child workflow ref ${CHILD_WORKFLOW_REF} moved to ${current_workflow_sha}, expected ${PARENT_WORKFLOW_SHA}; refusing dispatch." >&2
return 1
fi
# A failed dispatch POST can still create a run. Never retry it; recover only by exact run name.
set +e
dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
dispatch_status=$?
set -e
printf '%s\n' "$dispatch_output"
if [[ "$dispatch_status" -ne 0 && ! "$dispatch_output" =~ $GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN ]]; then
echo "::error::${workflow} dispatch failed with non-ambiguous status ${dispatch_status}; refusing adoption polling." >&2
exit "$dispatch_status"
fi
run_id=""
for _ in $(seq 1 60); do
if matches_json="$(
DISPATCH_RUN_NAME="$dispatch_run_name" CHILD_WORKFLOW_REF="$CHILD_WORKFLOW_REF" \
gh_with_retry api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/runs" \
-F event=workflow_dispatch \
-F per_page=100 \
--jq '[.workflow_runs[] | select(.display_title == env.DISPATCH_RUN_NAME and .head_branch == env.CHILD_WORKFLOW_REF) | .id]'
)"; then
match_count="$(jq 'length' <<< "$matches_json")"
if (( match_count > 1 )); then
echo "::error::Multiple runs matched ${dispatch_run_name}; refusing to guess." >&2
exit 1
fi
if (( match_count == 1 )); then
run_id="$(jq -r '.[0]' <<< "$matches_json")"
break
fi
fi
sleep 5
done
if [[ -z "$run_id" ]]; then
echo "::error::Could not find exact dispatched run ${dispatch_run_name}; dispatch status ${dispatch_status}. The dispatch was not retried to avoid creating a duplicate child." >&2
exit 1
fi
if [[ "$dispatch_status" -ne 0 ]]; then
echo "::warning::${workflow} dispatch returned status ${dispatch_status}; adopted exact run ${run_id}." >&2
fi
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
fetch_child_run_json() {
gh_with_retry api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
}
fetch_child_jobs() {
gh_with_retry api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq '.jobs[]'
}
fail_fast_failed_jobs() {
local failed_jobs_json
failed_jobs_json="$(
fetch_child_jobs |
jq -s '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
)"
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
echo "::error::${workflow} has failed child jobs before the workflow completed; cancelling the remaining matrix."
jq '.[] | {name, conclusion, url: .html_url}' <<< "$failed_jobs_json"
cancel_child
trap - EXIT INT TERM
exit 1
fi
}
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
child_head_sha="$(fetch_child_run_json | jq -r '.head_sha // ""')"
if [[ "$child_head_sha" != "$PARENT_WORKFLOW_SHA" ]]; then
echo "::error::${workflow} child run used workflow SHA ${child_head_sha}, expected parent workflow SHA ${PARENT_WORKFLOW_SHA}."
cancel_child
trap - EXIT INT TERM
exit 1
fi
poll_count=0
while true; do
status="$(fetch_child_run_json | jq -r '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
poll_count=$((poll_count + 1))
if (( poll_count % 2 == 0 )); then
fail_fast_failed_jobs
fi
if (( poll_count % 10 == 0 )); then
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
fi
sleep 30
done
trap - EXIT INT TERM
conclusion="$(fetch_child_run_json | jq -r '.conclusion // ""')"
url="$(fetch_child_run_json | jq -r '.html_url')"
echo "${workflow} finished with ${conclusion}: ${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
if [[ "$conclusion" != "success" ]]; then
fetch_child_jobs | jq 'select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url: .html_url}' || true
exit 1
fi
}
{
echo "### Plugin prerelease"
echo
echo "- Target ref: \`${TARGET_REF}\`"
echo "- Target SHA: \`${TARGET_SHA}\`"
} >> "$GITHUB_STEP_SUMMARY"
dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-plugin-prerelease"
dispatch_run_name="Plugin Prerelease ${dispatch_id}"
dispatch_and_wait plugin-prerelease.yml "$dispatch_run_name" -f target_ref="$TARGET_SHA" -f expected_sha="$TARGET_SHA" -f full_release_validation=true -f dispatch_id="$dispatch_id"
release_checks:
name: Run release/live/Docker/QA validation
needs: [resolve_target, evidence_reuse]
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["all","release-checks","install-smoke","cross-os","live-e2e","package","qa","qa-parity","qa-live"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: ${{ inputs.release_profile != 'minimum' && 240 || 60 }}
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 }}
PARENT_WORKFLOW_SHA: ${{ github.sha }}
PROVIDER: ${{ inputs.provider }}
MODE: ${{ inputs.mode }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'stable' || 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 }}
RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }}
PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }}
CODEX_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }}
run: |
set -euo pipefail
dispatch_and_wait() {
local workflow="$1"
local dispatch_run_name="$2"
shift 2
local dispatch_output dispatch_status matches_json match_count run_id status conclusion url poll_count run_json child_head_sha encoded_workflow_ref current_workflow_sha
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
set +e
output="$(gh "$@" 2>&1)"
status=$?
set -e
if [[ "$status" -eq 0 ]]; then
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"HTTP 429"* || "$output" == *"abuse detection"* || "$output" == *"Sorry. Your account was suspended"* || "$output" =~ $GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
fi
printf '%s\n' "$output" >&2
return "$status"
done
printf '%s\n' "$output" >&2
return "$status"
}
encoded_workflow_ref="$(jq -rn --arg value "$CHILD_WORKFLOW_REF" '$value | @uri')"
current_workflow_sha="$(
gh_with_retry api "repos/${GITHUB_REPOSITORY}/commits/${encoded_workflow_ref}" --jq .sha
)"
if [[ "$current_workflow_sha" != "$PARENT_WORKFLOW_SHA" ]]; then
echo "::error::Child workflow ref ${CHILD_WORKFLOW_REF} moved to ${current_workflow_sha}, expected ${PARENT_WORKFLOW_SHA}; refusing dispatch." >&2
return 1
fi
# A failed dispatch POST can still create a run. Never retry it; recover only by exact run name.
set +e
dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
dispatch_status=$?
set -e
printf '%s\n' "$dispatch_output"
if [[ "$dispatch_status" -ne 0 && ! "$dispatch_output" =~ $GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN ]]; then
echo "::error::${workflow} dispatch failed with non-ambiguous status ${dispatch_status}; refusing adoption polling." >&2
exit "$dispatch_status"
fi
run_id=""
for _ in $(seq 1 60); do
if matches_json="$(
DISPATCH_RUN_NAME="$dispatch_run_name" CHILD_WORKFLOW_REF="$CHILD_WORKFLOW_REF" \
gh_with_retry api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/runs" \
-F event=workflow_dispatch \
-F per_page=100 \
--jq '[.workflow_runs[] | select(.display_title == env.DISPATCH_RUN_NAME and .head_branch == env.CHILD_WORKFLOW_REF) | .id]'
)"; then
match_count="$(jq 'length' <<< "$matches_json")"
if (( match_count > 1 )); then
echo "::error::Multiple runs matched ${dispatch_run_name}; refusing to guess." >&2
exit 1
fi
if (( match_count == 1 )); then
run_id="$(jq -r '.[0]' <<< "$matches_json")"
break
fi
fi
sleep 5
done
if [[ -z "$run_id" ]]; then
echo "::error::Could not find exact dispatched run ${dispatch_run_name}; dispatch status ${dispatch_status}. The dispatch was not retried to avoid creating a duplicate child." >&2
exit 1
fi
if [[ "$dispatch_status" -ne 0 ]]; then
echo "::warning::${workflow} dispatch returned status ${dispatch_status}; adopted exact run ${run_id}." >&2
fi
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
fetch_child_run_json() {
gh_with_retry api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
}
fetch_child_jobs() {
gh_with_retry api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq '.jobs[]'
}
release_check_blocking_job() {
case "$1" in
"resolve_target" | \
"Prepare release package artifact" | \
"install_smoke_release_checks / "* | \
"Run package acceptance" | \
"Run package acceptance / "*)
return 0
;;
esac
return 1
}
release_checks_advisory_only() {
local run_json="$1"
local verifier_conclusion name saw_advisory failed
verifier_conclusion="$(
jq -r '.jobs[] | select(.name == "Verify release checks") | .conclusion' <<< "$run_json" |
tail -n 1
)"
if [[ "$verifier_conclusion" != "success" ]]; then
return 1
fi
saw_advisory=0
failed=0
while IFS= read -r name; do
[[ -z "${name// }" ]] && continue
if release_check_blocking_job "$name"; then
echo "::error::${name} is a package-safety Tideclaw alpha release-check lane."
failed=1
else
saw_advisory=1
fi
done < <(jq -r '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | .name' <<< "$run_json")
[[ "$saw_advisory" == "1" && "$failed" == "0" ]]
}
fail_fast_failed_jobs() {
local failed_jobs_json
if [[ "$workflow" == "openclaw-release-checks.yml" && "$CHILD_WORKFLOW_REF" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
return 0
fi
failed_jobs_json="$(
fetch_child_jobs |
jq -s '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
)"
if [[ "$workflow" == "openclaw-release-checks.yml" && "$RELEASE_PROFILE" == "beta" ]]; then
# Beta treats live-provider suites as advisory (live_advisory in
# openclaw-live-and-e2e-checks-reusable.yml); their failures must
# not fail-fast-cancel the remaining release-check matrix. Repo
# E2E under the same caller stays blocking.
failed_jobs_json="$(
jq '[.[] | select(
(.name | startswith("Run repo/live E2E validation / "))
and ((.name | contains("Docker live"))
or (.name | contains("Live media suites"))
or (.name | contains("validate_live_provider_suites"))
or (.name | contains("validate_release_live_cache"))
or (.name | contains("prepare_live_test_image")))
| not)]' <<< "$failed_jobs_json"
)"
fi
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
echo "::error::${workflow} has failed child jobs before the workflow completed; cancelling the remaining matrix."
jq '.[] | {name, conclusion, url: .html_url}' <<< "$failed_jobs_json"
cancel_child
trap - EXIT INT TERM
exit 1
fi
}
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
child_head_sha="$(fetch_child_run_json | jq -r '.head_sha // ""')"
if [[ "$child_head_sha" != "$PARENT_WORKFLOW_SHA" ]]; then
echo "::error::${workflow} child run used workflow SHA ${child_head_sha}, expected parent workflow SHA ${PARENT_WORKFLOW_SHA}."
cancel_child
trap - EXIT INT TERM
exit 1
fi
poll_count=0
while true; do
status="$(fetch_child_run_json | jq -r '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
poll_count=$((poll_count + 1))
if (( poll_count % 2 == 0 )); then
fail_fast_failed_jobs
fi
if (( poll_count % 10 == 0 )); then
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
fi
sleep 30
done
trap - EXIT INT TERM
jobs_json="$(fetch_child_jobs | jq -s '{jobs: [.[] | {name, conclusion, url: .html_url}]}')"
run_json="$(
jq -s '.[0] + .[1]' \
<(fetch_child_run_json | jq '{conclusion: (.conclusion // ""), url: .html_url}') \
<(printf '%s\n' "$jobs_json")
)"
conclusion="$(jq -r '.conclusion' <<< "$run_json")"
url="$(jq -r '.url' <<< "$run_json")"
echo "${workflow} finished with ${conclusion}: ${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
if [[ "$conclusion" != "success" ]]; then
jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' <<< "$run_json" || true
if [[ "$workflow" == "openclaw-release-checks.yml" && "$CHILD_WORKFLOW_REF" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
if release_checks_advisory_only "$run_json"; then
echo "::warning::${workflow} ended with ${conclusion}, but Verify release checks accepted Tideclaw alpha advisory lanes."
return 0
fi
fi
exit 1
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 "${RELEASE_PACKAGE_SPEC// }" ]]; then
echo "- Release package spec: \`${RELEASE_PACKAGE_SPEC}\`"
fi
if [[ -n "${PACKAGE_ACCEPTANCE_PACKAGE_SPEC// }" ]]; then
echo "- Package Acceptance package spec: \`${PACKAGE_ACCEPTANCE_PACKAGE_SPEC}\`"
fi
if [[ -n "${CODEX_PLUGIN_SPEC// }" ]]; then
echo "- Codex plugin spec: \`${CODEX_PLUGIN_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_REF"
-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 "${RELEASE_PACKAGE_SPEC// }" ]]; then
args+=(-f release_package_spec="$RELEASE_PACKAGE_SPEC")
fi
if [[ -n "${PACKAGE_ACCEPTANCE_PACKAGE_SPEC// }" ]]; then
args+=(-f package_acceptance_package_spec="$PACKAGE_ACCEPTANCE_PACKAGE_SPEC")
fi
if [[ -n "${CODEX_PLUGIN_SPEC// }" ]]; then
args+=(-f codex_plugin_spec="$CODEX_PLUGIN_SPEC")
fi
dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-release-checks"
dispatch_run_name="OpenClaw Release Checks ${dispatch_id}"
args+=(-f dispatch_id="$dispatch_id")
dispatch_and_wait openclaw-release-checks.yml "$dispatch_run_name" "${args[@]}"
npm_telegram:
name: Run package Telegram E2E
needs: [resolve_target]
if: ${{ always() && needs.resolve_target.result == 'success' && inputs.rerun_group == 'npm-telegram' && (inputs.npm_telegram_package_spec != '' || inputs.release_package_spec != '') }}
continue-on-error: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }}
runs-on: ubuntu-24.04
timeout-minutes: ${{ inputs.release_profile == 'full' && 360 || 60 }}
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 }}
PARENT_WORKFLOW_SHA: ${{ github.sha }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
PACKAGE_SPEC: ${{ inputs.npm_telegram_package_spec || inputs.release_package_spec }}
PROVIDER_MODE: ${{ inputs.npm_telegram_provider_mode }}
SCENARIO: ${{ inputs.npm_telegram_scenario }}
run: |
set -euo pipefail
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
set +e
output="$(gh "$@" 2>&1)"
status=$?
set -e
if [[ "$status" -eq 0 ]]; then
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"HTTP 429"* || "$output" == *"abuse detection"* || "$output" == *"Sorry. Your account was suspended"* || "$output" =~ $GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
fi
printf '%s\n' "$output" >&2
return "$status"
done
printf '%s\n' "$output" >&2
return "$status"
}
encoded_workflow_ref="$(jq -rn --arg value "$CHILD_WORKFLOW_REF" '$value | @uri')"
current_workflow_sha="$(
gh_with_retry api "repos/${GITHUB_REPOSITORY}/commits/${encoded_workflow_ref}" --jq .sha
)"
if [[ "$current_workflow_sha" != "$PARENT_WORKFLOW_SHA" ]]; then
echo "::error::Child workflow ref ${CHILD_WORKFLOW_REF} moved to ${current_workflow_sha}, expected ${PARENT_WORKFLOW_SHA}; refusing dispatch." >&2
exit 1
fi
args=(-f package_spec="$PACKAGE_SPEC" -f harness_ref="$TARGET_SHA" -f provider_mode="$PROVIDER_MODE")
if [[ -n "${SCENARIO// }" ]]; then
args+=(-f scenario="$SCENARIO")
fi
dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-npm-telegram"
dispatch_run_name="NPM Telegram Beta E2E ${dispatch_id}"
args+=(-f dispatch_id="$dispatch_id")
# A failed dispatch POST can still create a run. Never retry it; recover only by exact run name.
set +e
dispatch_output="$(gh workflow run npm-telegram-beta-e2e.yml --ref "$CHILD_WORKFLOW_REF" "${args[@]}" 2>&1)"
dispatch_status=$?
set -e
printf '%s\n' "$dispatch_output"
if [[ "$dispatch_status" -ne 0 && ! "$dispatch_output" =~ $GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN ]]; then
echo "::error::npm-telegram-beta-e2e.yml dispatch failed with non-ambiguous status ${dispatch_status}; refusing adoption polling." >&2
exit "$dispatch_status"
fi
run_id=""
for _ in $(seq 1 60); do
if matches_json="$(
DISPATCH_RUN_NAME="$dispatch_run_name" CHILD_WORKFLOW_REF="$CHILD_WORKFLOW_REF" \
gh_with_retry api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/npm-telegram-beta-e2e.yml/runs" \
-F event=workflow_dispatch \
-F per_page=100 \
--jq '[.workflow_runs[] | select(.display_title == env.DISPATCH_RUN_NAME and .head_branch == env.CHILD_WORKFLOW_REF) | .id]'
)"; then
match_count="$(jq 'length' <<< "$matches_json")"
if (( match_count > 1 )); then
echo "::error::Multiple runs matched ${dispatch_run_name}; refusing to guess." >&2
exit 1
fi
if (( match_count == 1 )); then
run_id="$(jq -r '.[0]' <<< "$matches_json")"
break
fi
fi
sleep 5
done
if [[ -z "$run_id" ]]; then
echo "::error::Could not find exact dispatched run ${dispatch_run_name}; dispatch status ${dispatch_status}. The dispatch was not retried to avoid creating a duplicate child." >&2
exit 1
fi
if [[ "$dispatch_status" -ne 0 ]]; then
echo "::warning::npm-telegram-beta-e2e.yml dispatch returned status ${dispatch_status}; adopted exact run ${run_id}." >&2
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
child_head_sha="$(gh_with_retry run view "$run_id" --json headSha --jq '.headSha // ""')"
if [[ "$child_head_sha" != "$PARENT_WORKFLOW_SHA" ]]; then
echo "::error::npm-telegram-beta-e2e.yml child run used workflow SHA ${child_head_sha}, expected parent workflow SHA ${PARENT_WORKFLOW_SHA}."
cancel_child
trap - EXIT INT TERM
exit 1
fi
fail_fast_failed_jobs() {
local failed_jobs_json
failed_jobs_json="$(
gh_with_retry run view "$run_id" --json jobs \
--jq '[.jobs[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
)"
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
echo "::error::npm-telegram-beta-e2e.yml has failed child jobs before the workflow completed; cancelling the remaining run."
jq '.[] | {name, conclusion, url}' <<< "$failed_jobs_json"
cancel_child
trap - EXIT INT TERM
exit 1
fi
}
poll_count=0
while true; do
status="$(gh_with_retry run view "$run_id" --json status --jq '.status')"
if [[ "$status" == "completed" ]]; then
break
fi
poll_count=$((poll_count + 1))
if (( poll_count % 2 == 0 )); then
fail_fast_failed_jobs
fi
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_with_retry 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_with_retry run view "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh_with_retry 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_with_retry run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
exit 1
fi
performance:
name: Run product performance evidence
needs: [resolve_target, evidence_reuse]
if: ${{ always() && needs.resolve_target.result == 'success' && contains(fromJSON('["all","performance"]'), inputs.rerun_group) && needs.evidence_reuse.outputs.reuse != 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: ${{ inputs.release_profile == 'full' && 360 || 120 }}
outputs:
run_id: ${{ steps.dispatch.outputs.run_id }}
url: ${{ steps.dispatch.outputs.url }}
conclusion: ${{ steps.dispatch.outputs.conclusion }}
steps:
- name: Dispatch and monitor OpenClaw Performance
id: dispatch
env:
GH_TOKEN: ${{ github.token }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
PARENT_WORKFLOW_SHA: ${{ github.sha }}
run: |
set -euo pipefail
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
set +e
output="$(gh "$@" 2>&1)"
status=$?
set -e
if [[ "$status" -eq 0 ]]; then
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"HTTP 429"* || "$output" == *"abuse detection"* || "$output" == *"Sorry. Your account was suspended"* || "$output" =~ $GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
fi
printf '%s\n' "$output" >&2
return "$status"
done
printf '%s\n' "$output" >&2
return "$status"
}
encoded_workflow_ref="$(jq -rn --arg value "$CHILD_WORKFLOW_REF" '$value | @uri')"
current_workflow_sha="$(
gh_with_retry api "repos/${GITHUB_REPOSITORY}/commits/${encoded_workflow_ref}" --jq .sha
)"
if [[ "$current_workflow_sha" != "$PARENT_WORKFLOW_SHA" ]]; then
echo "::error::Child workflow ref ${CHILD_WORKFLOW_REF} moved to ${current_workflow_sha}, expected ${PARENT_WORKFLOW_SHA}; refusing dispatch." >&2
exit 1
fi
{
echo "### Product performance"
echo
echo "- Target SHA: \`${TARGET_SHA}\`"
echo "- Profile: \`release\`"
echo "- Repeat: \`3\`"
echo "- Deep profile: \`false\`"
echo "- Live OpenAI candidate: \`false\`"
echo "- Report publication: disabled (artifacts only)"
echo "- Release impact: blocking"
} >> "$GITHUB_STEP_SUMMARY"
dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
dispatch_run_name="OpenClaw Performance ${dispatch_id}"
# A failed dispatch POST can still create a run. Never retry it; recover only by exact run name.
set +e
dispatch_output="$(gh workflow run openclaw-performance.yml \
--ref "$CHILD_WORKFLOW_REF" \
-f target_ref="$TARGET_SHA" \
-f profile=release \
-f repeat=3 \
-f deep_profile=false \
-f live_openai_candidate=false \
-f fail_on_regression=true \
-f publish_reports=false \
-f dispatch_id="$dispatch_id" 2>&1)"
dispatch_status=$?
set -e
printf '%s\n' "$dispatch_output"
if [[ "$dispatch_status" -ne 0 && ! "$dispatch_output" =~ $GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN ]]; then
echo "::error::openclaw-performance.yml dispatch failed with non-ambiguous status ${dispatch_status}; refusing adoption polling." >&2
exit "$dispatch_status"
fi
run_id=""
for _ in $(seq 1 60); do
if matches_json="$(
DISPATCH_RUN_NAME="$dispatch_run_name" CHILD_WORKFLOW_REF="$CHILD_WORKFLOW_REF" \
gh_with_retry api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/openclaw-performance.yml/runs" \
-F event=workflow_dispatch \
-F per_page=100 \
--jq '[.workflow_runs[] | select(.display_title == env.DISPATCH_RUN_NAME and .head_branch == env.CHILD_WORKFLOW_REF) | .id]'
)"; then
match_count="$(jq 'length' <<< "$matches_json")"
if (( match_count > 1 )); then
echo "::error::Multiple runs matched ${dispatch_run_name}; refusing to guess." >&2
exit 1
fi
if (( match_count == 1 )); then
run_id="$(jq -r '.[0]' <<< "$matches_json")"
break
fi
fi
sleep 5
done
if [[ -z "$run_id" ]]; then
echo "::error::Could not find exact dispatched run ${dispatch_run_name}; dispatch status ${dispatch_status}. The dispatch was not retried to avoid creating a duplicate child." >&2
exit 1
fi
if [[ "$dispatch_status" -ne 0 ]]; then
echo "::warning::openclaw-performance.yml dispatch returned status ${dispatch_status}; adopted exact run ${run_id}." >&2
fi
echo "Dispatched openclaw-performance.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 openclaw-performance.yml: ${run_id}" >&2
gh run cancel "$run_id" >/dev/null 2>&1 || true
fi
}
trap cancel_child EXIT INT TERM
child_head_sha="$(gh_with_retry run view "$run_id" --json headSha --jq '.headSha // ""')"
if [[ "$child_head_sha" != "$PARENT_WORKFLOW_SHA" ]]; then
echo "::error::openclaw-performance.yml child run used workflow SHA ${child_head_sha}, expected parent workflow SHA ${PARENT_WORKFLOW_SHA}."
cancel_child
trap - EXIT INT TERM
exit 1
fi
poll_count=0
while true; do
status="$(gh_with_retry 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 openclaw-performance.yml: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
gh_with_retry 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_with_retry run view "$run_id" --json conclusion --jq '.conclusion')"
url="$(gh_with_retry run view "$run_id" --json url --jq '.url')"
echo "openclaw-performance.yml finished with ${conclusion}: ${url}"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "conclusion=${conclusion}" >> "$GITHUB_OUTPUT"
if [[ "$conclusion" != "success" ]]; then
echo "::error::OpenClaw Performance ended with ${conclusion}: ${url}"
gh_with_retry run view "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true
exit 1
fi
summary:
name: Verify full validation
needs:
[
resolve_target,
evidence_reuse,
docker_runtime_assets_preflight,
normal_ci,
plugin_prerelease,
release_checks,
npm_telegram,
performance,
]
if: always()
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- 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 }}
PERFORMANCE_RUN_ID: ${{ needs.performance.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 }}
PERFORMANCE_RESULT: ${{ needs.performance.result }}
DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT: ${{ needs.docker_runtime_assets_preflight.result }}
EVIDENCE_REUSE: ${{ needs.evidence_reuse.outputs.reuse }}
EVIDENCE_ROOT_RUN_ID: ${{ needs.evidence_reuse.outputs.evidence_root_run_id }}
EVIDENCE_RUN_URL: ${{ needs.evidence_reuse.outputs.evidence_run_url }}
EVIDENCE_SHA: ${{ needs.evidence_reuse.outputs.evidence_sha }}
EVIDENCE_MANIFEST: ${{ needs.evidence_reuse.outputs.evidence_manifest }}
RERUN_GROUP: ${{ inputs.rerun_group }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
PARENT_WORKFLOW_SHA: ${{ github.sha }}
run: |
set -euo pipefail
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
set +e
output="$(gh "$@" 2>&1)"
status=$?
set -e
if [[ "$status" -eq 0 ]]; then
printf '%s\n' "$output"
return 0
fi
if [[ "$output" == *"Bad credentials"* || "$output" == *"HTTP 401"* || "$output" == *"secondary rate limit"* || "$output" == *"API rate limit"* || "$output" == *"HTTP 429"* || "$output" == *"abuse detection"* || "$output" == *"Sorry. Your account was suspended"* || "$output" =~ $GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN ]]; then
echo "::warning::gh $* failed on attempt ${attempt}: ${output}" >&2
sleep $((attempt * 10))
continue
fi
printf '%s\n' "$output" >&2
return "$status"
done
printf '%s\n' "$output" >&2
return "$status"
}
release_check_blocking_job() {
case "$1" in
"resolve_target" | \
"Prepare release package artifact" | \
"install_smoke_release_checks / "* | \
"Run package acceptance" | \
"Run package acceptance / "*)
return 0
;;
esac
return 1
}
release_checks_advisory_only() {
local run_json="$1"
local verifier_conclusion name saw_advisory failed
verifier_conclusion="$(
jq -r '.jobs[] | select(.name == "Verify release checks") | .conclusion' <<< "$run_json" |
tail -n 1
)"
if [[ "$verifier_conclusion" != "success" ]]; then
return 1
fi
saw_advisory=0
failed=0
while IFS= read -r name; do
[[ -z "${name// }" ]] && continue
if release_check_blocking_job "$name"; then
echo "::error::${name} is a package-safety Tideclaw alpha release-check lane."
failed=1
else
saw_advisory=1
fi
done < <(jq -r '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | .name' <<< "$run_json")
[[ "$saw_advisory" == "1" && "$failed" == "0" ]]
}
check_child() {
local label="$1"
local run_id="$2"
local required="$3"
local advisory_ok="${4:-0}"
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_with_retry 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 [[ "$head_sha" != "$PARENT_WORKFLOW_SHA" ]]; then
echo "::error::${label} child run used workflow SHA ${head_sha}, expected parent workflow SHA ${PARENT_WORKFLOW_SHA}. Use the SHA-pinned release helper when a moving branch cannot stay fixed."
return 1
fi
if [[ "$status" != "completed" || "$conclusion" != "success" ]]; then
if [[ "$advisory_ok" == "1" && "$label" == "release_checks" ]]; then
if release_checks_advisory_only "$run_json"; then
echo "::warning::${label} child run ended with ${status}/${conclusion}, but Verify release checks accepted Tideclaw alpha advisory lanes: ${url}"
return 0
fi
fi
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 "| --- | --- | ---: | --- | --- |"
echo "| \`docker_runtime_assets_preflight\` | \`${DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT}\` | | current workflow | |"
} >> "$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_with_retry 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"
append_child_row "product_performance" "$PERFORMANCE_RUN_ID" "$PERFORMANCE_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_with_retry 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_with_retry 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"
}
summarize_failed_child() {
local label="$1"
local run_id="$2"
if [[ -z "${run_id// }" ]]; then
return 0
fi
local run_json status conclusion artifacts_json
run_json="$(gh_with_retry run view "$run_id" --json status,conclusion,url,jobs)"
status="$(jq -r '.status' <<< "$run_json")"
conclusion="$(jq -r '.conclusion' <<< "$run_json")"
if [[ "$status" == "completed" && "$conclusion" == "success" ]]; then
return 0
fi
{
echo
echo "### Failed child detail: ${label}"
echo
jq -r '
"- Run: " + (.url // ""),
"- Result: `" + (.status // "") + "/" + (.conclusion // "") + "`",
"",
"Failed jobs:",
(.jobs[]
| select(.conclusion != "success" and .conclusion != "skipped")
| "- `" + (.name | gsub("`"; "\\`")) + "`: `" + ((.conclusion // .status // "") | tostring) + "` " + (.url // ""))
' <<< "$run_json" || true
echo
echo "Artifacts:"
artifacts_json="$(
gh_with_retry api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/artifacts?per_page=100" 2>/dev/null || true
)"
if [[ -n "${artifacts_json// }" ]]; then
jq -r '
if ((.artifacts // []) | length) == 0 then
"- none"
else
(.artifacts[]
| "- `" + (.name | gsub("`"; "\\`")) + "` (" + ((.size_in_bytes // 0) | tostring) + " bytes)")
end
' <<< "$artifacts_json" || echo "- unable to list artifacts"
else
echo "- unable to list artifacts"
fi
} >> "$GITHUB_STEP_SUMMARY"
}
failed=0
normal_ci_required=0
plugin_prerelease_required=0
release_checks_required=0
performance_required=0
if [[ "$RERUN_GROUP" == "all" && "$EVIDENCE_REUSE" == "true" ]]; then
# Lanes were skipped because a prior green validation covers this
# target; re-verify the chain-root run and its recorded child runs
# so evidence that went stale after resolution cannot pass.
evidence_state="$(gh_with_retry run view "$EVIDENCE_ROOT_RUN_ID" --json status,conclusion --jq '(.status // "") + "/" + (.conclusion // "")')"
if [[ "$evidence_state" != "completed/success" ]]; then
echo "::error::Reused evidence run ${EVIDENCE_ROOT_RUN_ID} is ${evidence_state}; evidence is no longer valid."
failed=1
fi
while IFS= read -r evidence_child_run_id; do
[[ -n "$evidence_child_run_id" ]] || continue
evidence_child_state="$(gh_with_retry run view "$evidence_child_run_id" --json status,conclusion --jq '(.status // "") + "/" + (.conclusion // "")')"
if [[ "$evidence_child_state" != "completed/success" ]]; then
echo "::error::Reused evidence child run ${evidence_child_run_id} is ${evidence_child_state}; evidence is no longer valid."
failed=1
fi
done < <(jq -r '[.childRuns.normalCi // "", .childRuns.pluginPrerelease // "", .childRuns.releaseChecks // "", .childRuns.npmTelegram // "", (.childRuns.productPerformance.runId // "")] | map(select(. != "")) | .[]' <<< "$EVIDENCE_MANIFEST")
if [[ "$failed" == "0" ]]; then
{
echo "### Reused validation evidence"
echo
echo "- Evidence run: ${EVIDENCE_RUN_URL}"
echo "- Evidence SHA: \`${EVIDENCE_SHA}\`"
echo "- Target SHA: \`${TARGET_SHA}\` (exact-target evidence reuse)"
} >> "$GITHUB_STEP_SUMMARY"
fi
elif [[ "$RERUN_GROUP" == "all" && "$DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT" != "success" ]]; then
echo "::error::Docker runtime-assets preflight ended with ${DOCKER_RUNTIME_ASSETS_PREFLIGHT_RESULT}."
failed=1
elif [[ "$RERUN_GROUP" == "all" ]]; then
normal_ci_required=1
plugin_prerelease_required=1
release_checks_required=1
performance_required=1
else
case "$RERUN_GROUP" in
ci)
normal_ci_required=1
;;
plugin-prerelease)
plugin_prerelease_required=1
;;
release-checks|install-smoke|cross-os|live-e2e|package|qa|qa-parity|qa-live)
release_checks_required=1
;;
performance)
performance_required=1
;;
esac
fi
append_child_overview
if [[ "$NORMAL_CI_RESULT" == "skipped" && -z "${NORMAL_CI_RUN_ID// }" ]]; then
check_child "normal_ci" "" "$normal_ci_required" || 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" "" "$plugin_prerelease_required" || 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" "" "$release_checks_required" || failed=1
elif [[ "$CHILD_WORKFLOW_REF" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
check_child "release_checks" "$RELEASE_CHECKS_RUN_ID" 1 1 || 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
elif [[ "$CHILD_WORKFLOW_REF" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
check_child "npm_telegram" "$NPM_TELEGRAM_RUN_ID" 0 || echo "::warning::npm_telegram is advisory for Tideclaw alpha validation."
else
check_child "npm_telegram" "$NPM_TELEGRAM_RUN_ID" 1 || failed=1
fi
if [[ "$PERFORMANCE_RESULT" == "skipped" && -z "${PERFORMANCE_RUN_ID// }" ]]; then
check_child "product_performance" "" "$performance_required" || failed=1
else
check_child "product_performance" "$PERFORMANCE_RUN_ID" "$performance_required" || 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"
summarize_child_timing "product_performance" "$PERFORMANCE_RUN_ID"
if [[ "$failed" != "0" ]]; then
summarize_failed_child "normal_ci" "$NORMAL_CI_RUN_ID"
summarize_failed_child "plugin_prerelease" "$PLUGIN_PRERELEASE_RUN_ID"
summarize_failed_child "release_checks" "$RELEASE_CHECKS_RUN_ID"
summarize_failed_child "npm_telegram" "$NPM_TELEGRAM_RUN_ID"
summarize_failed_child "product_performance" "$PERFORMANCE_RUN_ID"
fi
exit "$failed"
- name: Request release evidence update
if: ${{ inputs.dispatch_release_evidence }}
env:
RELEASES_DISPATCH_TOKEN: ${{ secrets.OPENCLAW_RELEASES_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 }}
EVIDENCE_REUSE: ${{ needs.evidence_reuse.outputs.reuse }}
EVIDENCE_ROOT_RUN_ID: ${{ needs.evidence_reuse.outputs.evidence_root_run_id }}
run: |
set -euo pipefail
if [[ "$RELEASE_CHECKS_RESULT" == "skipped" && "$EVIDENCE_REUSE" != "true" ]]; then
echo "Release checks were skipped by rerun group; skipping automatic release evidence update."
exit 0
fi
# In reuse mode the child runs live on the chain-root validation run
# (the evidence consumer scrapes dispatch logs from the given run),
# so durable evidence must reference that run id, not this wrapper.
notes="Automatically requested by Full Release Validation ${GITHUB_RUN_ID_VALUE} after child workflows completed; the parent summary re-checks current child run conclusions."
if [[ "$EVIDENCE_REUSE" == "true" && -n "${EVIDENCE_ROOT_RUN_ID// }" ]]; then
notes="Automatically requested by Full Release Validation ${GITHUB_RUN_ID_VALUE}, which reused green evidence from chain-root run ${EVIDENCE_ROOT_RUN_ID} for the exact same target SHA and inputs."
GITHUB_RUN_ID_VALUE="$EVIDENCE_ROOT_RUN_ID"
fi
if [[ -z "${RELEASES_DISPATCH_TOKEN// }" ]]; then
echo "OPENCLAW_RELEASES_DISPATCH_TOKEN is not configured; skipping automatic release evidence update."
exit 0
fi
evidence_package_spec="$PACKAGE_SPEC"
if [[ -z "${evidence_package_spec// }" ]]; then
tag_ref="${TARGET_REF#refs/tags/}"
if [[ "$tag_ref" =~ ^v([0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-(alpha|beta)\.[1-9][0-9]*)|(-[1-9][0-9]*))?)$ ]]; then
evidence_package_spec="openclaw@${BASH_REMATCH[1]}"
fi
fi
release_id="${TARGET_REF#refs/tags/}"
release_id="${release_id#v}"
if [[ "$evidence_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 "::warning::Could not derive release evidence id from target ref '${TARGET_REF}'; skipping automatic release evidence update."
exit 0
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 "$evidence_package_spec" \
--arg notes "$notes" \
'{
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
}
}'
)"
if ! curl --fail-with-body \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${RELEASES_DISPATCH_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/openclaw/releases/dispatches \
-d "$payload"; then
echo "::warning::Automatic release evidence dispatch failed; child workflow validation remains authoritative."
{
echo "### Release evidence dispatch failed"
echo
echo "Child workflow validation remains authoritative. Backfill durable evidence from \`openclaw/releases\`:"
echo
echo "\`\`\`bash"
echo "gh workflow run openclaw-release-evidence-from-full-validation.yml --repo openclaw/releases --ref main -f full_validation_run_id=${GITHUB_RUN_ID_VALUE} -f release_id=${release_id} -f release_ref=${TARGET_REF} -f package_spec=${evidence_package_spec}"
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"
fi
- name: Write release validation manifest
if: ${{ success() }}
env:
TARGET_REF: ${{ inputs.ref }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
RERUN_GROUP: ${{ inputs.rerun_group }}
RUN_RELEASE_SOAK: ${{ inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full' }}
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 }}
PERFORMANCE_RUN_ID: ${{ needs.performance.outputs.run_id }}
PERFORMANCE_CONCLUSION: ${{ needs.performance.outputs.conclusion }}
EVIDENCE_REUSE: ${{ needs.evidence_reuse.outputs.reuse }}
EVIDENCE_RUN_ID: ${{ needs.evidence_reuse.outputs.evidence_run_id }}
EVIDENCE_ROOT_RUN_ID: ${{ needs.evidence_reuse.outputs.evidence_root_run_id }}
EVIDENCE_SHA: ${{ needs.evidence_reuse.outputs.evidence_sha }}
EVIDENCE_CHANGED_PATHS: ${{ needs.evidence_reuse.outputs.changed_paths }}
EVIDENCE_MANIFEST: ${{ needs.evidence_reuse.outputs.evidence_manifest }}
PROVIDER: ${{ inputs.provider }}
MODE: ${{ inputs.mode }}
TARGET_CONTEXT_REF: ${{ inputs.target_context_ref }}
LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}
CROSS_OS_SUITE_FILTER: ${{ inputs.cross_os_suite_filter }}
RELEASE_PACKAGE_SPEC: ${{ inputs.release_package_spec }}
PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_spec }}
CODEX_PLUGIN_SPEC: ${{ inputs.codex_plugin_spec }}
run: |
set -euo pipefail
manifest_dir="${RUNNER_TEMP}/full-release-validation"
mkdir -p "$manifest_dir"
if [[ "$EVIDENCE_REUSE" == "true" ]]; then
# Inherit the evidence manifest (profile, soak, child runs) so future
# reuse lookups and evidence consumers keep resolving the chain root.
jq \
--arg runId "$GITHUB_RUN_ID" \
--arg runAttempt "$GITHUB_RUN_ATTEMPT" \
--arg workflowRef "$GITHUB_REF_NAME" \
--arg workflowSha "$GITHUB_SHA" \
--arg workflowFullRef "$GITHUB_REF" \
--arg workflowRefType "$GITHUB_REF_TYPE" \
--arg targetRef "$TARGET_REF" \
--arg targetSha "$TARGET_SHA" \
--arg evidenceRunId "$EVIDENCE_RUN_ID" \
--arg evidenceRootRunId "$EVIDENCE_ROOT_RUN_ID" \
--arg evidenceSha "$EVIDENCE_SHA" \
--argjson evidenceChangedPaths "$EVIDENCE_CHANGED_PATHS" \
'. + {
version: 3,
runId: $runId,
runAttempt: $runAttempt,
workflowRef: $workflowRef,
workflowSha: $workflowSha,
workflowFullRef: $workflowFullRef,
workflowRefType: $workflowRefType,
targetRef: $targetRef,
targetSha: $targetSha,
evidenceReuse: {
policy: "exact-target-full-validation-v1",
runId: $evidenceRootRunId,
selectedRunId: $evidenceRunId,
evidenceSha: $evidenceSha,
changedPaths: $evidenceChangedPaths
},
controls: ((.controls // {}) + {
performanceReportPublication: "artifact-only"
})
}' <<< "$EVIDENCE_MANIFEST" > "${manifest_dir}/full-release-validation-manifest.json"
exit 0
fi
jq -n \
--arg workflowName "Full Release Validation" \
--arg runId "$GITHUB_RUN_ID" \
--arg runAttempt "$GITHUB_RUN_ATTEMPT" \
--arg workflowRef "$GITHUB_REF_NAME" \
--arg workflowSha "$GITHUB_SHA" \
--arg workflowFullRef "$GITHUB_REF" \
--arg workflowRefType "$GITHUB_REF_TYPE" \
--arg targetRef "$TARGET_REF" \
--arg targetSha "$TARGET_SHA" \
--arg releaseProfile "$RELEASE_PROFILE" \
--arg rerunGroup "$RERUN_GROUP" \
--arg runReleaseSoak "$RUN_RELEASE_SOAK" \
--arg normalCiRunId "$NORMAL_CI_RUN_ID" \
--arg pluginPrereleaseRunId "$PLUGIN_PRERELEASE_RUN_ID" \
--arg releaseChecksRunId "$RELEASE_CHECKS_RUN_ID" \
--arg npmTelegramRunId "$NPM_TELEGRAM_RUN_ID" \
--arg performanceRunId "$PERFORMANCE_RUN_ID" \
--arg performanceConclusion "$PERFORMANCE_CONCLUSION" \
--arg provider "$PROVIDER" \
--arg mode "$MODE" \
--arg targetContextRef "$TARGET_CONTEXT_REF" \
--arg liveSuiteFilter "$LIVE_SUITE_FILTER" \
--arg crossOsSuiteFilter "$CROSS_OS_SUITE_FILTER" \
--arg releasePackageSpec "$RELEASE_PACKAGE_SPEC" \
--arg packageAcceptancePackageSpec "$PACKAGE_ACCEPTANCE_PACKAGE_SPEC" \
--arg codexPluginSpec "$CODEX_PLUGIN_SPEC" \
'{
version: 3,
workflowName: $workflowName,
runId: $runId,
runAttempt: $runAttempt,
workflowRef: $workflowRef,
workflowSha: $workflowSha,
workflowFullRef: $workflowFullRef,
workflowRefType: $workflowRefType,
targetRef: $targetRef,
targetSha: $targetSha,
releaseProfile: $releaseProfile,
rerunGroup: $rerunGroup,
runReleaseSoak: $runReleaseSoak,
validationInputs: {
provider: $provider,
mode: $mode,
targetContextRef: $targetContextRef,
liveSuiteFilter: $liveSuiteFilter,
crossOsSuiteFilter: $crossOsSuiteFilter,
releasePackageSpec: $releasePackageSpec,
packageAcceptancePackageSpec: $packageAcceptancePackageSpec,
codexPluginSpec: $codexPluginSpec
},
controls: {
stableSoakRequired: ($releaseProfile == "stable" or $releaseProfile == "full"),
performanceBlocking: true,
performanceReportPublication: "artifact-only"
},
childRuns: {
normalCi: $normalCiRunId,
pluginPrerelease: $pluginPrereleaseRunId,
releaseChecks: $releaseChecksRunId,
npmTelegram: $npmTelegramRunId,
productPerformance: {
runId: $performanceRunId,
conclusion: $performanceConclusion,
blocking: true
}
}
}' > "${manifest_dir}/full-release-validation-manifest.json"
- name: Upload release validation manifest
if: ${{ success() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: full-release-validation-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/full-release-validation
if-no-files-found: error
- name: Upload legacy release validation manifest alias
if: ${{ success() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: full-release-validation-${{ github.run_id }}
path: ${{ runner.temp }}/full-release-validation
if-no-files-found: error
overwrite: true