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