ci: make release child dispatch idempotent (#102858)

(cherry picked from commit 11dda0e5fd)
This commit is contained in:
Peter Steinberger
2026-07-09 15:03:46 +01:00
committed by Dallin Romney
parent 5e02ed4048
commit 2d7c97028e
8 changed files with 275 additions and 61 deletions
+6 -1
View File
@@ -18,6 +18,11 @@ on:
required: false
default: false
type: boolean
dispatch_id:
description: Optional parent workflow dispatch identifier
required: false
default: ""
type: string
push:
branches: [main]
paths-ignore:
@@ -31,7 +36,7 @@ on:
permissions:
contents: read
run-name: ${{ github.event_name == 'workflow_dispatch' && inputs.release_gate && format('CI release gate {0}', inputs.target_ref) || 'CI' }}
run-name: ${{ github.event_name == 'workflow_dispatch' && inputs.dispatch_id != '' && format('CI {0}', inputs.dispatch_id) || (github.event_name == 'workflow_dispatch' && inputs.release_gate && format('CI release gate {0}', inputs.target_ref) || 'CI') }}
concurrency:
group: ${{ github.event_name == 'workflow_dispatch' && format('{0}-manual-v1-{1}', github.workflow, github.run_id) || (github.event_name == 'pull_request' && format('{0}-v7-{1}', github.workflow, github.event.pull_request.number) || (github.repository == 'openclaw/openclaw' && format('{0}-v7-{1}', github.workflow, github.ref) || format('{0}-v7-{1}-{2}', github.workflow, github.ref, github.sha))) }}
+171 -46
View File
@@ -275,9 +275,10 @@ jobs:
dispatch_and_wait() {
local workflow="$1"
shift
local dispatch_run_name="$2"
shift 2
local dispatch_output run_id status conclusion url poll_count
local dispatch_output dispatch_status matches_json match_count run_id status conclusion url poll_count
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
@@ -300,18 +301,42 @@ jobs:
printf '%s\n' "$output" >&2
return "$status"
}
dispatch_output="$(gh_with_retry workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@")"
# 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"
run_id="$(
printf '%s\n' "$dispatch_output" |
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
tail -n 1
)"
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::gh workflow run ${workflow} did not return an Actions run URL; refusing to guess from recent workflow_dispatch runs." >&2
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"
@@ -383,7 +408,9 @@ jobs:
echo "- Target SHA: \`${TARGET_SHA}\`"
} >> "$GITHUB_STEP_SUMMARY"
dispatch_and_wait ci.yml -f target_ref="$TARGET_SHA" -f include_android=true
dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-ci"
dispatch_run_name="CI ${dispatch_id}"
dispatch_and_wait ci.yml "$dispatch_run_name" -f target_ref="$TARGET_SHA" -f include_android=true -f dispatch_id="$dispatch_id"
plugin_prerelease:
name: Run plugin prerelease validation
@@ -408,9 +435,10 @@ jobs:
dispatch_and_wait() {
local workflow="$1"
shift
local dispatch_run_name="$2"
shift 2
local dispatch_output run_id status conclusion url poll_count
local dispatch_output dispatch_status matches_json match_count run_id status conclusion url poll_count
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
@@ -433,18 +461,42 @@ jobs:
printf '%s\n' "$output" >&2
return "$status"
}
dispatch_output="$(gh_with_retry workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@")"
# 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"
run_id="$(
printf '%s\n' "$dispatch_output" |
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
tail -n 1
)"
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::gh workflow run ${workflow} did not return an Actions run URL; refusing to guess from recent workflow_dispatch runs." >&2
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"
@@ -516,7 +568,9 @@ jobs:
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
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
@@ -551,9 +605,10 @@ jobs:
dispatch_and_wait() {
local workflow="$1"
shift
local dispatch_run_name="$2"
shift 2
local dispatch_output run_id status conclusion url poll_count run_json
local dispatch_output dispatch_status matches_json match_count run_id status conclusion url poll_count run_json
gh_with_retry() {
local output status attempt
for attempt in 1 2 3 4 5 6; do
@@ -576,18 +631,42 @@ jobs:
printf '%s\n' "$output" >&2
return "$status"
}
dispatch_output="$(gh_with_retry workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@")"
# 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"
run_id="$(
printf '%s\n' "$dispatch_output" |
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
tail -n 1
)"
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::gh workflow run ${workflow} did not return an Actions run URL; refusing to guess from recent workflow_dispatch runs." >&2
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"
@@ -764,7 +843,10 @@ jobs:
args+=(-f codex_plugin_spec="$CODEX_PLUGIN_SPEC")
fi
dispatch_and_wait openclaw-release-checks.yml "${args[@]}"
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
@@ -818,18 +900,46 @@ jobs:
args+=(-f scenario="$SCENARIO")
fi
dispatch_output="$(gh_with_retry workflow run npm-telegram-beta-e2e.yml --ref "$CHILD_WORKFLOW_REF" "${args[@]}")"
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"
run_id="$(
printf '%s\n' "$dispatch_output" |
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
tail -n 1
)"
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::gh workflow run npm-telegram-beta-e2e.yml did not return an Actions run URL; refusing to guess from recent workflow_dispatch runs." >&2
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"
@@ -942,7 +1052,9 @@ jobs:
dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
dispatch_run_name="OpenClaw Performance ${dispatch_id}"
dispatch_output="$(gh_with_retry workflow run openclaw-performance.yml \
# 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 \
@@ -950,27 +1062,40 @@ jobs:
-f deep_profile=false \
-f live_openai_candidate=false \
-f fail_on_regression=true \
-f dispatch_id="$dispatch_id")"
-f dispatch_id="$dispatch_id" 2>&1)"
dispatch_status=$?
set -e
printf '%s\n' "$dispatch_output"
run_id=""
for _ in $(seq 1 60); do
run_id="$(
DISPATCH_RUN_NAME="$dispatch_run_name" gh_with_retry api -X GET "repos/${GITHUB_REPOSITORY}/actions/workflows/openclaw-performance.yml/runs" \
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 | map(select(.display_title == env.DISPATCH_RUN_NAME)) | sort_by(.created_at) | reverse | .[0].id // empty'
)"
if [[ -n "$run_id" ]]; then
break
--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 dispatched run for ${dispatch_run_name}." >&2
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"
@@ -1,5 +1,7 @@
name: NPM Telegram Beta E2E
run-name: ${{ inputs.dispatch_id != '' && format('NPM Telegram Beta E2E {0}', inputs.dispatch_id) || 'NPM Telegram Beta E2E' }}
on:
workflow_dispatch:
inputs:
@@ -45,6 +47,11 @@ on:
required: false
default: false
type: boolean
dispatch_id:
description: Optional parent workflow dispatch identifier
required: false
default: ""
type: string
workflow_call:
inputs:
advisory:
@@ -86,6 +93,11 @@ on:
required: false
default: ""
type: string
dispatch_id:
description: Optional parent workflow dispatch identifier
required: false
default: ""
type: string
secrets:
OPENAI_API_KEY:
required: false
@@ -1,5 +1,7 @@
name: OpenClaw Release Checks
run-name: ${{ inputs.dispatch_id != '' && format('OpenClaw Release Checks {0}', inputs.dispatch_id) || 'OpenClaw Release Checks' }}
on:
workflow_dispatch:
inputs:
@@ -88,6 +90,11 @@ on:
required: false
default: ""
type: string
dispatch_id:
description: Optional parent workflow dispatch identifier
required: false
default: ""
type: string
concurrency:
group: openclaw-release-checks-${{ inputs.expected_sha || inputs.ref }}-${{ inputs.rerun_group }}
+7
View File
@@ -1,5 +1,7 @@
name: Plugin Prerelease
run-name: ${{ inputs.dispatch_id != '' && format('Plugin Prerelease {0}', inputs.dispatch_id) || 'Plugin Prerelease' }}
on:
workflow_dispatch:
inputs:
@@ -18,6 +20,11 @@ on:
required: false
default: false
type: boolean
dispatch_id:
description: Optional parent workflow dispatch identifier
required: false
default: ""
type: string
permissions:
contents: read