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