name: OpenClaw Release Checks run-name: ${{ inputs.dispatch_id != '' && format('OpenClaw Release Checks {0}', inputs.dispatch_id) || 'OpenClaw Release Checks' }} on: workflow_dispatch: inputs: ref: description: Branch, tag, or full commit SHA to validate required: true type: string expected_sha: description: Optional full SHA that ref must resolve to 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 release_profile=stable and full required: false default: false type: boolean run_maturity_scorecard: description: Render advisory maturity scorecard release docs; default release checks rely on dedicated package, QA, live, and E2E gates required: false default: false type: boolean rerun_group: description: Release check group to run required: false default: all type: choice options: - all - install-smoke - cross-os - live-e2e - package - qa - qa-parity - qa-live live_suite_filter: description: Optional exact live/E2E suite id, or comma-separated QA live lanes such as qa-live-matrix,qa-live-telegram,qa-live-discord,qa-live-whatsapp; 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 release_package_spec: description: Optional published package spec for release checks; blank builds the selected SHA package artifact required: false default: "" type: string package_acceptance_package_spec: description: Optional published package spec for Package Acceptance; blank uses the prepared 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 dispatch_id: description: Optional parent workflow dispatch identifier required: false default: "" type: string concurrency: group: openclaw-release-checks-${{ inputs.expected_sha || inputs.ref }}-${{ inputs.rerun_group }} cancel-in-progress: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }} env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" NODE_VERSION: "24.15.0" OPENCLAW_CI_OPENAI_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL || 'openai/gpt-5.6-luna' }} jobs: resolve_target: runs-on: ubuntu-24.04 timeout-minutes: 30 permissions: contents: read outputs: ref: ${{ steps.inputs.outputs.ref }} revision: ${{ steps.ref.outputs.sha }} provider: ${{ steps.inputs.outputs.provider }} mode: ${{ steps.inputs.outputs.mode }} release_profile: ${{ steps.inputs.outputs.release_profile }} run_release_soak: ${{ steps.inputs.outputs.run_release_soak }} run_maturity_scorecard: ${{ steps.inputs.outputs.run_maturity_scorecard }} rerun_group: ${{ steps.inputs.outputs.rerun_group }} live_suite_filter: ${{ steps.inputs.outputs.live_suite_filter }} cross_os_suite_filter: ${{ steps.inputs.outputs.cross_os_suite_filter }} qa_live_matrix_enabled: ${{ steps.inputs.outputs.qa_live_matrix_enabled }} qa_live_telegram_enabled: ${{ steps.inputs.outputs.qa_live_telegram_enabled }} qa_live_discord_enabled: ${{ steps.inputs.outputs.qa_live_discord_enabled }} qa_live_whatsapp_enabled: ${{ steps.inputs.outputs.qa_live_whatsapp_enabled }} qa_live_slack_enabled: ${{ steps.inputs.outputs.qa_live_slack_enabled }} release_package_spec: ${{ steps.inputs.outputs.release_package_spec }} package_acceptance_package_spec: ${{ steps.inputs.outputs.package_acceptance_package_spec }} codex_plugin_spec: ${{ steps.inputs.outputs.codex_plugin_spec }} steps: - name: Require trusted workflow ref for release checks env: RELEASE_REF: ${{ inputs.ref }} WORKFLOW_REF: ${{ github.ref }} run: | set -euo pipefail tideclaw_alpha_check=false if [[ "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then workflow_branch="${WORKFLOW_REF#refs/heads/}" if [[ "${RELEASE_REF}" == *"-alpha."* || "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ || "${RELEASE_REF}" == "${workflow_branch}" || "${RELEASE_REF}" == "refs/heads/${workflow_branch}" ]]; then tideclaw_alpha_check=true fi fi if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release-ci/[0-9a-f]{12}-[0-9]+$ ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/extended-stable/[0-9]{4}\.([1-9]|1[0-2])\.33$ ]] && [[ "${tideclaw_alpha_check}" != "true" ]]; then echo "Release checks must be dispatched from main, release/YYYY.M.PATCH, extended-stable/YYYY.M.33, a Full Release Validation release-ci/- ref, or a Tideclaw alpha branch for alpha prereleases." >&2 exit 1 fi - name: Validate ref input env: RELEASE_REF: ${{ inputs.ref }} EXPECTED_SHA: ${{ inputs.expected_sha }} run: | set -euo pipefail if [[ -z "${RELEASE_REF// }" ]] || [[ "${RELEASE_REF}" == -* ]]; then echo "Expected a branch, tag, or full commit SHA; got: ${RELEASE_REF}" >&2 exit 1 fi if [[ -n "${EXPECTED_SHA// }" ]] && [[ ! "${EXPECTED_SHA}" =~ ^[0-9a-fA-F]{40}$ ]]; then echo "Expected expected_sha to be a full commit SHA; got: ${EXPECTED_SHA}" >&2 exit 1 fi - name: Checkout trusted workflow helper uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: persist-credentials: false ref: ${{ github.sha }} path: workflow fetch-depth: 1 - name: Fast-resolve selected ref id: fast_ref env: RELEASE_REF: ${{ inputs.ref }} EXPECTED_SHA: ${{ inputs.expected_sha }} run: | bash workflow/scripts/github/resolve-openclaw-ref.sh \ --ref "$RELEASE_REF" \ --expected-sha "$EXPECTED_SHA" \ --fallback-ok \ --github-output "$GITHUB_OUTPUT" - name: Checkout selected ref for reachability fallback if: steps.fast_ref.outputs.fallback == 'true' uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: persist-credentials: false ref: ${{ inputs.ref }} path: source fetch-depth: 0 - name: Resolve checked-out fallback SHA if: steps.fast_ref.outputs.fallback == 'true' id: fallback_ref working-directory: source run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - name: Validate selected ref belongs to this repository if: steps.fast_ref.outputs.fallback == 'true' working-directory: source env: RELEASE_REF: ${{ inputs.ref }} GITHUB_TOKEN: ${{ github.token }} run: | set -euo pipefail SELECTED_SHA="$(git rev-parse HEAD)" git_fetch_with_checkout_auth() { if git config --get-all http.https://github.com/.extraheader >/dev/null; then git fetch "$@" return fi local auth_header auth_header="$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')" git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" fetch "$@" } git_fetch_with_checkout_auth --no-tags origin '+refs/heads/*:refs/remotes/origin/*' git_fetch_with_checkout_auth --tags origin '+refs/tags/*:refs/tags/*' if git tag --points-at "${SELECTED_SHA}" | grep -Eq '^v'; then exit 0 fi if git for-each-ref --format='%(refname:short)' --contains "${SELECTED_SHA}" refs/remotes/origin | grep -Eq '^origin/'; then exit 0 fi echo "Ref '${RELEASE_REF}' resolved to ${SELECTED_SHA}, but that commit is not reachable from an OpenClaw branch or release tag." >&2 echo "Secret-bearing release checks only run repository-owned branch/tag history, not arbitrary unreferenced commits." >&2 exit 1 - name: Finalize resolved SHA id: ref env: FAST_SHA: ${{ steps.fast_ref.outputs.sha }} FALLBACK_SHA: ${{ steps.fallback_ref.outputs.sha }} EXPECTED_SHA: ${{ inputs.expected_sha }} USED_FALLBACK: ${{ steps.fast_ref.outputs.fallback }} run: | set -euo pipefail selected_sha="$FAST_SHA" if [[ "$USED_FALLBACK" == "true" ]]; then selected_sha="$FALLBACK_SHA" fi if [[ -z "$selected_sha" ]]; then echo "Failed to resolve selected ref SHA." >&2 exit 1 fi if [[ -n "${EXPECTED_SHA// }" ]] && [[ "${selected_sha,,}" != "${EXPECTED_SHA,,}" ]]; then echo "Ref resolved to ${selected_sha}, expected ${EXPECTED_SHA}." >&2 exit 1 fi echo "sha=${selected_sha,,}" >> "$GITHUB_OUTPUT" - name: Validate Tideclaw alpha target matches workflow branch if: startsWith(github.ref, 'refs/heads/tideclaw/alpha/') working-directory: workflow env: SELECTED_SHA: ${{ steps.ref.outputs.sha }} WORKFLOW_REF: ${{ github.ref }} GITHUB_TOKEN: ${{ github.token }} run: | set -euo pipefail if [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then echo "Tideclaw alpha release checks must run from tideclaw/alpha/YYYY-MM-DD-HHMMZ." >&2 exit 1 fi alpha_branch="${WORKFLOW_REF#refs/heads/}" git_fetch_with_checkout_auth() { if git config --get-all http.https://github.com/.extraheader >/dev/null; then git fetch "$@" return fi local auth_header auth_header="$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')" git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" fetch "$@" } git_fetch_with_checkout_auth --no-tags origin "+refs/heads/${alpha_branch}:refs/remotes/origin/${alpha_branch}" if ! git merge-base --is-ancestor "${SELECTED_SHA}" "refs/remotes/origin/${alpha_branch}"; then echo "Alpha release target ${SELECTED_SHA} must be reachable from ${alpha_branch}." >&2 exit 1 fi - name: Capture selected inputs id: inputs env: RELEASE_REF_INPUT: ${{ inputs.ref }} RELEASE_PROVIDER_INPUT: ${{ inputs.provider }} RELEASE_MODE_INPUT: ${{ inputs.mode }} RELEASE_PROFILE_INPUT: ${{ inputs.release_profile }} RELEASE_RUN_RELEASE_SOAK_INPUT: ${{ inputs.run_release_soak }} RELEASE_RUN_MATURITY_SCORECARD_INPUT: ${{ inputs.run_maturity_scorecard }} RELEASE_RERUN_GROUP_INPUT: ${{ inputs.rerun_group }} RELEASE_LIVE_SUITE_FILTER_INPUT: ${{ inputs.live_suite_filter }} RELEASE_CROSS_OS_SUITE_FILTER_INPUT: ${{ inputs.cross_os_suite_filter }} RELEASE_QA_DISCORD_LIVE_CI_ENABLED: ${{ vars.OPENCLAW_RELEASE_QA_DISCORD_LIVE_CI_ENABLED || 'false' }} RELEASE_QA_WHATSAPP_LIVE_CI_ENABLED: ${{ vars.OPENCLAW_RELEASE_QA_WHATSAPP_LIVE_CI_ENABLED || 'false' }} RELEASE_QA_SLACK_LIVE_CI_ENABLED: ${{ vars.OPENCLAW_RELEASE_QA_SLACK_LIVE_CI_ENABLED || 'false' }} RELEASE_PACKAGE_SPEC_INPUT: ${{ inputs.release_package_spec }} RELEASE_PACKAGE_ACCEPTANCE_PACKAGE_SPEC_INPUT: ${{ inputs.package_acceptance_package_spec }} RELEASE_CODEX_PLUGIN_SPEC_INPUT: ${{ inputs.codex_plugin_spec }} run: | set -euo pipefail qa_live_matrix_enabled=true qa_live_telegram_enabled=true qa_live_discord_ci_enabled="$(printf '%s' "$RELEASE_QA_DISCORD_LIVE_CI_ENABLED" | tr '[:upper:]' '[:lower:]')" if [[ "$qa_live_discord_ci_enabled" != "true" && "$qa_live_discord_ci_enabled" != "1" && "$qa_live_discord_ci_enabled" != "yes" ]]; then qa_live_discord_ci_enabled=false else qa_live_discord_ci_enabled=true fi qa_live_whatsapp_ci_enabled="$(printf '%s' "$RELEASE_QA_WHATSAPP_LIVE_CI_ENABLED" | tr '[:upper:]' '[:lower:]')" if [[ "$qa_live_whatsapp_ci_enabled" != "true" && "$qa_live_whatsapp_ci_enabled" != "1" && "$qa_live_whatsapp_ci_enabled" != "yes" ]]; then qa_live_whatsapp_ci_enabled=false else qa_live_whatsapp_ci_enabled=true fi qa_live_slack_ci_enabled="$(printf '%s' "$RELEASE_QA_SLACK_LIVE_CI_ENABLED" | tr '[:upper:]' '[:lower:]')" if [[ "$qa_live_slack_ci_enabled" != "true" && "$qa_live_slack_ci_enabled" != "1" && "$qa_live_slack_ci_enabled" != "yes" ]]; then qa_live_slack_ci_enabled=false else qa_live_slack_ci_enabled=true fi qa_live_discord_enabled="$qa_live_discord_ci_enabled" qa_live_whatsapp_enabled="$qa_live_whatsapp_ci_enabled" qa_live_slack_enabled="$qa_live_slack_ci_enabled" run_release_soak="$(printf '%s' "$RELEASE_RUN_RELEASE_SOAK_INPUT" | tr '[:upper:]' '[:lower:]')" if [[ "$run_release_soak" != "true" && "$run_release_soak" != "1" && "$run_release_soak" != "yes" ]]; then run_release_soak=false else run_release_soak=true fi run_maturity_scorecard="$(printf '%s' "$RELEASE_RUN_MATURITY_SCORECARD_INPUT" | tr '[:upper:]' '[:lower:]')" if [[ "$run_maturity_scorecard" != "true" && "$run_maturity_scorecard" != "1" && "$run_maturity_scorecard" != "yes" ]]; then run_maturity_scorecard=false else run_maturity_scorecard=true fi release_profile="$RELEASE_PROFILE_INPUT" if [[ "$release_profile" == "minimum" ]]; then release_profile=beta fi case "$release_profile" in beta|stable|full) ;; *) echo "release_profile must be one of: beta, stable, full" >&2 exit 1 ;; esac if [[ "$release_profile" == "stable" || "$release_profile" == "full" ]]; then run_release_soak=true fi codex_plugin_spec="$RELEASE_CODEX_PLUGIN_SPEC_INPUT" if [[ -z "${codex_plugin_spec// }" && "$RELEASE_PACKAGE_SPEC_INPUT" =~ ^openclaw@(.+)$ ]]; then codex_plugin_spec="npm:@openclaw/codex@${BASH_REMATCH[1]}" fi filter="$(printf '%s' "$RELEASE_LIVE_SUITE_FILTER_INPUT" | tr '[:upper:]' '[:lower:]')" if [[ -n "${filter// }" ]]; then qa_filter_seen=false matrix_selected=false telegram_selected=false discord_selected=false whatsapp_selected=false slack_selected=false disabled_required_lanes=() IFS=', ' read -r -a filter_tokens <<< "$filter" for token in "${filter_tokens[@]}"; do token="${token//$'\t'/}" token="${token//$'\r'/}" token="${token//$'\n'/}" [[ -z "$token" ]] && continue case "$token" in qa-live|qa-live-all|qa-all) qa_filter_seen=true matrix_selected=true telegram_selected=true discord_selected="$qa_live_discord_ci_enabled" whatsapp_selected="$qa_live_whatsapp_ci_enabled" slack_selected="$qa_live_slack_ci_enabled" [[ "$qa_live_discord_ci_enabled" == "true" ]] || disabled_required_lanes+=("qa-live-discord") [[ "$qa_live_whatsapp_ci_enabled" == "true" ]] || disabled_required_lanes+=("qa-live-whatsapp") [[ "$qa_live_slack_ci_enabled" == "true" ]] || disabled_required_lanes+=("qa-live-slack") ;; qa-live-non-slack|qa-non-slack|non-slack|no-slack|without-slack) qa_filter_seen=true matrix_selected=true telegram_selected=true discord_selected="$qa_live_discord_ci_enabled" whatsapp_selected="$qa_live_whatsapp_ci_enabled" [[ "$qa_live_discord_ci_enabled" == "true" ]] || disabled_required_lanes+=("qa-live-discord") [[ "$qa_live_whatsapp_ci_enabled" == "true" ]] || disabled_required_lanes+=("qa-live-whatsapp") ;; qa-live-matrix|qa-matrix|matrix) qa_filter_seen=true matrix_selected=true ;; qa-live-telegram|qa-telegram|telegram) qa_filter_seen=true telegram_selected=true ;; qa-live-discord|qa-discord|discord) qa_filter_seen=true discord_selected="$qa_live_discord_ci_enabled" [[ "$qa_live_discord_ci_enabled" == "true" ]] || disabled_required_lanes+=("qa-live-discord") ;; qa-live-whatsapp|qa-whatsapp|whatsapp) qa_filter_seen=true whatsapp_selected="$qa_live_whatsapp_ci_enabled" [[ "$qa_live_whatsapp_ci_enabled" == "true" ]] || disabled_required_lanes+=("qa-live-whatsapp") ;; qa-live-slack|qa-slack|slack) qa_filter_seen=true slack_selected="$qa_live_slack_ci_enabled" [[ "$qa_live_slack_ci_enabled" == "true" ]] || disabled_required_lanes+=("qa-live-slack") ;; esac done if [[ "${#disabled_required_lanes[@]}" -gt 0 ]]; then echo "live_suite_filter explicitly requested disabled QA live lane(s): ${disabled_required_lanes[*]}" >&2 echo "Enable the matching OPENCLAW_RELEASE_QA_*_LIVE_CI_ENABLED repo variable or remove the lane from live_suite_filter." >&2 exit 1 fi if [[ "$qa_filter_seen" == "true" ]]; then qa_live_matrix_enabled="$matrix_selected" qa_live_telegram_enabled="$telegram_selected" qa_live_discord_enabled="$discord_selected" qa_live_whatsapp_enabled="$whatsapp_selected" qa_live_slack_enabled="$slack_selected" fi fi { printf 'ref=%s\n' "$RELEASE_REF_INPUT" printf 'provider=%s\n' "$RELEASE_PROVIDER_INPUT" printf 'mode=%s\n' "$RELEASE_MODE_INPUT" printf 'release_profile=%s\n' "$release_profile" printf 'run_release_soak=%s\n' "$run_release_soak" printf 'run_maturity_scorecard=%s\n' "$run_maturity_scorecard" printf 'rerun_group=%s\n' "$RELEASE_RERUN_GROUP_INPUT" printf 'live_suite_filter=%s\n' "$RELEASE_LIVE_SUITE_FILTER_INPUT" printf 'cross_os_suite_filter=%s\n' "$RELEASE_CROSS_OS_SUITE_FILTER_INPUT" printf 'qa_live_matrix_enabled=%s\n' "$qa_live_matrix_enabled" printf 'qa_live_telegram_enabled=%s\n' "$qa_live_telegram_enabled" printf 'qa_live_discord_enabled=%s\n' "$qa_live_discord_enabled" printf 'qa_live_whatsapp_enabled=%s\n' "$qa_live_whatsapp_enabled" printf 'qa_live_slack_enabled=%s\n' "$qa_live_slack_enabled" printf 'release_package_spec=%s\n' "$RELEASE_PACKAGE_SPEC_INPUT" printf 'package_acceptance_package_spec=%s\n' "$RELEASE_PACKAGE_ACCEPTANCE_PACKAGE_SPEC_INPUT" printf 'codex_plugin_spec=%s\n' "$codex_plugin_spec" } >> "$GITHUB_OUTPUT" - name: Summarize validated ref env: RELEASE_REF: ${{ inputs.ref }} RELEASE_SHA: ${{ steps.ref.outputs.sha }} RELEASE_REF_FAST_PATH: ${{ steps.fast_ref.outputs.fast }} RELEASE_PROVIDER: ${{ inputs.provider }} RELEASE_MODE: ${{ inputs.mode }} RELEASE_PROFILE: ${{ steps.inputs.outputs.release_profile }} RUN_RELEASE_SOAK: ${{ steps.inputs.outputs.run_release_soak }} RUN_MATURITY_SCORECARD: ${{ steps.inputs.outputs.run_maturity_scorecard }} RELEASE_RERUN_GROUP: ${{ inputs.rerun_group }} RELEASE_LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }} RELEASE_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: ${{ steps.inputs.outputs.codex_plugin_spec }} run: | { echo "## Release checks" echo echo "- Requested ref: \`${RELEASE_REF}\`" echo "- Validated SHA: \`${RELEASE_SHA}\`" echo "- Ref resolution fast path: \`${RELEASE_REF_FAST_PATH}\`" echo "- Cross-OS provider: \`${RELEASE_PROVIDER}\`" echo "- Cross-OS mode: \`${RELEASE_MODE}\`" echo "- Release profile: \`${RELEASE_PROFILE}\`" echo "- Release soak lanes: \`${RUN_RELEASE_SOAK}\`" echo "- Maturity scorecard docs: \`${RUN_MATURITY_SCORECARD}\`" echo "- Rerun group: \`${RELEASE_RERUN_GROUP}\`" if [[ -n "${RELEASE_LIVE_SUITE_FILTER// }" ]]; then echo "- Live suite filter: \`${RELEASE_LIVE_SUITE_FILTER}\`" fi if [[ -n "${RELEASE_CROSS_OS_SUITE_FILTER// }" ]]; then echo "- Cross-OS suite filter: \`${RELEASE_CROSS_OS_SUITE_FILTER}\`" fi echo "- QA live lanes: Matrix \`${{ steps.inputs.outputs.qa_live_matrix_enabled }}\`, Telegram \`${{ steps.inputs.outputs.qa_live_telegram_enabled }}\`, Discord \`${{ steps.inputs.outputs.qa_live_discord_enabled }}\`, WhatsApp \`${{ steps.inputs.outputs.qa_live_whatsapp_enabled }}\`, Slack \`${{ steps.inputs.outputs.qa_live_slack_enabled }}\`" 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}\`" elif [[ -n "${RELEASE_PACKAGE_SPEC// }" ]]; then echo "- Package Acceptance package spec: \`${RELEASE_PACKAGE_SPEC}\`" else echo "- Package Acceptance package spec: prepared release artifact" fi if [[ -n "${CODEX_PLUGIN_SPEC// }" ]]; then echo "- Codex plugin spec: \`${CODEX_PLUGIN_SPEC}\`" else echo "- Codex plugin spec: packed from selected ref" fi if [[ "$RUN_RELEASE_SOAK" == "true" ]]; then echo "- This run will execute blocking release validation plus exhaustive live/Docker soak coverage." else echo "- This run will execute blocking release validation. Exhaustive live/Docker soak lanes are skipped unless \`run_release_soak=true\`, \`release_profile=full\`, or \`rerun_group=live-e2e\` is selected." fi } >> "$GITHUB_STEP_SUMMARY" prepare_release_package: name: Prepare release package artifact needs: [resolve_target] if: contains(fromJSON('["all","cross-os","package"]'), needs.resolve_target.outputs.rerun_group) || (needs.resolve_target.outputs.rerun_group == 'live-e2e' && needs.resolve_target.outputs.live_suite_filter == '') runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: contents: read packages: read outputs: artifact_digest: ${{ steps.release_package_upload.outputs.artifact-digest }} artifact_id: ${{ steps.release_package_upload.outputs.artifact-id }} artifact_name: ${{ steps.artifact.outputs.name }} artifact_run_attempt: ${{ steps.artifact.outputs.run_attempt }} artifact_run_id: ${{ steps.artifact.outputs.run_id }} package_file_name: ${{ steps.artifact.outputs.file_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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: persist-credentials: false ref: ${{ github.sha }} fetch-depth: 0 - name: Set artifact metadata id: artifact run: | { echo "file_name=openclaw-current.tgz" echo "name=release-package-under-test-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" echo "run_attempt=${GITHUB_RUN_ATTEMPT}" echo "run_id=${GITHUB_RUN_ID}" } >> "$GITHUB_OUTPUT" - name: Setup Node environment uses: ./.github/actions/setup-node-env with: node-version: ${{ env.NODE_VERSION }} install-bun: "true" install-deps: "true" - name: Resolve release package artifact id: package shell: bash env: PACKAGE_REF: ${{ needs.resolve_target.outputs.revision }} RELEASE_PACKAGE_SPEC: ${{ needs.resolve_target.outputs.release_package_spec }} run: | set -euo pipefail source_args=(--source ref --package-ref "$PACKAGE_REF") package_label="ref:${PACKAGE_REF}" if [[ -n "${RELEASE_PACKAGE_SPEC// }" ]]; then source_args=(--source npm --package-spec "$RELEASE_PACKAGE_SPEC") package_label="$RELEASE_PACKAGE_SPEC" fi node scripts/resolve-openclaw-package-candidate.mjs \ "${source_args[@]}" \ --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")" if [[ "$source_sha" != "$PACKAGE_REF" ]]; then echo "Release package source SHA differs from the selected release SHA: expected $PACKAGE_REF, found ${source_sha:-}." >&2 exit 1 fi echo "source_sha=$source_sha" >> "$GITHUB_OUTPUT" { echo "## Release package artifact" echo echo "- Artifact: \`${{ steps.artifact.outputs.name }}\`" echo "- Package: \`$package_label\`" echo "- SHA-256: \`$digest\`" echo "- Version: \`$version\`" echo "- Source SHA: \`$source_sha\`" } >> "$GITHUB_STEP_SUMMARY" - name: Upload release package artifact id: release_package_upload uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: ${{ steps.artifact.outputs.name }} path: | .artifacts/docker-e2e-package/${{ steps.artifact.outputs.file_name }} .artifacts/docker-e2e-package/package-candidate.json retention-days: 14 if-no-files-found: error - name: Validate release package artifact binding env: ARTIFACT_DIGEST: ${{ steps.release_package_upload.outputs.artifact-digest }} ARTIFACT_ID: ${{ steps.release_package_upload.outputs.artifact-id }} ARTIFACT_NAME: ${{ steps.artifact.outputs.name }} ARTIFACT_RUN_ATTEMPT: ${{ steps.artifact.outputs.run_attempt }} ARTIFACT_RUN_ID: ${{ steps.artifact.outputs.run_id }} PACKAGE_FILE_NAME: ${{ steps.artifact.outputs.file_name }} PACKAGE_SHA256: ${{ steps.package.outputs.sha256 }} PACKAGE_SOURCE_SHA: ${{ steps.package.outputs.source_sha }} PACKAGE_VERSION: ${{ steps.package.outputs.package_version }} run: | set -euo pipefail [[ "$ARTIFACT_ID" =~ ^[1-9][0-9]*$ ]] || { echo "Release package artifact ID is missing or invalid." >&2 exit 1 } [[ "$ARTIFACT_DIGEST" =~ ^[a-f0-9]{64}$ ]] || { echo "Release package artifact digest is missing or invalid." >&2 exit 1 } [[ "$ARTIFACT_RUN_ID" == "$GITHUB_RUN_ID" && "$ARTIFACT_RUN_ATTEMPT" == "$GITHUB_RUN_ATTEMPT" ]] || { echo "Release package artifact run binding is invalid." >&2 exit 1 } [[ "$ARTIFACT_NAME" == "release-package-under-test-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" && "$PACKAGE_FILE_NAME" == "openclaw-current.tgz" ]] || { echo "Release package artifact name or tarball filename is invalid." >&2 exit 1 } [[ "$PACKAGE_SHA256" =~ ^[a-f0-9]{64}$ && "$PACKAGE_SOURCE_SHA" =~ ^[a-f0-9]{40}$ && -n "${PACKAGE_VERSION// }" ]] || { echo "Release package identity is incomplete." >&2 exit 1 } install_smoke_release_checks: needs: [resolve_target] if: contains(fromJSON('["all","install-smoke"]'), needs.resolve_target.outputs.rerun_group) permissions: actions: read contents: read packages: read uses: ./.github/workflows/install-smoke-reusable.yml with: ref: ${{ needs.resolve_target.outputs.revision }} run_bun_global_install_smoke: true cross_os_release_checks: needs: [resolve_target, prepare_release_package] if: contains(fromJSON('["all","cross-os"]'), needs.resolve_target.outputs.rerun_group) permissions: read-all uses: ./.github/workflows/openclaw-cross-os-release-checks-reusable.yml with: advisory: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }} ref: ${{ needs.resolve_target.outputs.revision }} provider: ${{ needs.resolve_target.outputs.provider }} mode: ${{ needs.resolve_target.outputs.mode }} suite_filter: ${{ needs.resolve_target.outputs.cross_os_suite_filter }} candidate_artifact_digest: ${{ needs.prepare_release_package.outputs.artifact_digest }} candidate_artifact_id: ${{ needs.prepare_release_package.outputs.artifact_id }} candidate_artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }} candidate_artifact_run_attempt: ${{ needs.prepare_release_package.outputs.artifact_run_attempt }} candidate_artifact_run_id: ${{ needs.prepare_release_package.outputs.artifact_run_id }} candidate_file_name: ${{ needs.prepare_release_package.outputs.package_file_name }} candidate_sha256: ${{ needs.prepare_release_package.outputs.package_sha256 }} candidate_version: ${{ needs.prepare_release_package.outputs.package_version }} candidate_source_sha: ${{ needs.prepare_release_package.outputs.source_sha }} openai_model: openai/gpt-5.6-luna ubuntu_runner: ubuntu-24.04 windows_runner: windows-2025 macos_runner: macos-26 secrets: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} OPENCLAW_DISCORD_SMOKE_BOT_TOKEN: ${{ secrets.OPENCLAW_DISCORD_SMOKE_BOT_TOKEN }} OPENCLAW_DISCORD_SMOKE_GUILD_ID: ${{ secrets.OPENCLAW_DISCORD_SMOKE_GUILD_ID }} OPENCLAW_DISCORD_SMOKE_CHANNEL_ID: ${{ secrets.OPENCLAW_DISCORD_SMOKE_CHANNEL_ID }} live_repo_e2e_release_checks: name: Run repo/live E2E validation needs: [resolve_target] if: needs.resolve_target.outputs.rerun_group == 'live-e2e' || (needs.resolve_target.outputs.rerun_group == 'all' && needs.resolve_target.outputs.run_release_soak == 'true') permissions: actions: read contents: read packages: read pull-requests: read uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml with: advisory: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }} # Live-provider suites depend on third-party model deployments; beta # treats only those as advisory while repo E2E stays blocking and # stable/full keep everything blocking. live_advisory: ${{ needs.resolve_target.outputs.release_profile == 'beta' }} ref: ${{ needs.resolve_target.outputs.revision }} include_repo_e2e: true include_release_path_suites: false include_openwebui: false include_live_suites: true release_test_profile: ${{ needs.resolve_target.outputs.release_profile }} live_suite_filter: ${{ needs.resolve_target.outputs.live_suite_filter }} shared_image_artifact_namespace: release-live shared_image_policy: no-push-artifact secrets: &live_e2e_release_secrets OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }} ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }} FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }} BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }} CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }} DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }} DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }} MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }} MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }} MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }} OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }} OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }} OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }} OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }} OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }} FAL_KEY: ${{ secrets.FAL_KEY }} RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }} DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }} TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }} VYDRA_API_KEY: ${{ secrets.VYDRA_API_KEY }} XAI_API_KEY: ${{ secrets.XAI_API_KEY }} ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }} Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }} BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }} BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }} CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }} OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }} OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }} OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }} OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }} OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }} OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }} FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }} docker_e2e_release_checks: name: Run Docker release-path validation needs: [resolve_target, prepare_release_package] if: (needs.resolve_target.outputs.rerun_group == 'live-e2e' || (needs.resolve_target.outputs.rerun_group == 'all' && needs.resolve_target.outputs.run_release_soak == 'true')) && needs.resolve_target.outputs.live_suite_filter == '' permissions: actions: read contents: read packages: read pull-requests: read uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml with: advisory: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }} ref: ${{ needs.resolve_target.outputs.revision }} include_repo_e2e: false include_release_path_suites: true include_openwebui: ${{ needs.resolve_target.outputs.release_profile != 'beta' }} include_live_suites: false release_test_profile: ${{ needs.resolve_target.outputs.release_profile }} package_artifact_digest: ${{ needs.prepare_release_package.outputs.artifact_digest }} package_artifact_id: ${{ needs.prepare_release_package.outputs.artifact_id }} package_artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }} package_artifact_run_attempt: ${{ needs.prepare_release_package.outputs.artifact_run_attempt }} package_artifact_run_id: ${{ needs.prepare_release_package.outputs.artifact_run_id }} package_file_name: ${{ needs.prepare_release_package.outputs.package_file_name }} package_sha256: ${{ needs.prepare_release_package.outputs.package_sha256 }} package_source_sha: ${{ needs.prepare_release_package.outputs.source_sha }} package_version: ${{ needs.prepare_release_package.outputs.package_version }} codex_plugin_spec: ${{ needs.resolve_target.outputs.codex_plugin_spec }} shared_image_artifact_namespace: release-docker shared_image_policy: no-push-artifact secrets: *live_e2e_release_secrets package_acceptance_release_checks: name: Run package acceptance needs: [resolve_target, prepare_release_package] if: contains(fromJSON('["all","package"]'), needs.resolve_target.outputs.rerun_group) permissions: actions: read contents: read packages: read pull-requests: read uses: ./.github/workflows/package-acceptance.yml with: advisory: false workflow_ref: ${{ github.sha }} source: ${{ (needs.resolve_target.outputs.package_acceptance_package_spec != '' || needs.resolve_target.outputs.release_package_spec != '') && 'npm' || 'artifact' }} package_spec: ${{ needs.resolve_target.outputs.package_acceptance_package_spec || needs.resolve_target.outputs.release_package_spec || 'openclaw@beta' }} artifact_digest: ${{ needs.prepare_release_package.outputs.artifact_digest }} artifact_id: ${{ needs.prepare_release_package.outputs.artifact_id }} artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }} artifact_run_attempt: ${{ needs.prepare_release_package.outputs.artifact_run_attempt }} artifact_run_id: ${{ needs.prepare_release_package.outputs.artifact_run_id }} package_file_name: ${{ needs.prepare_release_package.outputs.package_file_name }} package_sha256: ${{ (needs.resolve_target.outputs.package_acceptance_package_spec == '' && needs.resolve_target.outputs.release_package_spec == '') && needs.prepare_release_package.outputs.package_sha256 || '' }} package_source_sha: ${{ needs.prepare_release_package.outputs.source_sha }} package_version: ${{ needs.prepare_release_package.outputs.package_version }} suite_profile: custom docker_lanes: doctor-switch update-channel-switch skill-install update-corrupt-plugin upgrade-survivor published-upgrade-survivor root-managed-vps-upgrade update-restart-auth plugins-offline plugin-update plugin-binding-command-escape published_upgrade_survivor_baselines: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'last-stable-4 2026.4.23 2026.5.2 2026.4.15' || '' }} published_upgrade_survivor_scenarios: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'reported-issues' || '' }} telegram_mode: mock-openai shared_image_artifact_namespace: release-package shared_image_policy: no-push-artifact secrets: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }} ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }} FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }} BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }} CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }} DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }} DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }} MODELSTUDIO_API_KEY: ${{ secrets.MODELSTUDIO_API_KEY }} MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }} MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} OPENCODE_ZEN_API_KEY: ${{ secrets.OPENCODE_ZEN_API_KEY }} OPENCLAW_LIVE_BROWSER_CDP_URL: ${{ secrets.OPENCLAW_LIVE_BROWSER_CDP_URL }} OPENCLAW_LIVE_SETUP_TOKEN: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN }} OPENCLAW_LIVE_SETUP_TOKEN_MODEL: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_MODEL }} OPENCLAW_LIVE_SETUP_TOKEN_PROFILE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_PROFILE }} OPENCLAW_LIVE_SETUP_TOKEN_VALUE: ${{ secrets.OPENCLAW_LIVE_SETUP_TOKEN_VALUE }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }} FAL_KEY: ${{ secrets.FAL_KEY }} RUNWAY_API_KEY: ${{ secrets.RUNWAY_API_KEY }} DEEPGRAM_API_KEY: ${{ secrets.DEEPGRAM_API_KEY }} TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }} VYDRA_API_KEY: ${{ secrets.VYDRA_API_KEY }} XAI_API_KEY: ${{ secrets.XAI_API_KEY }} ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }} Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }} BYTEPLUS_ACCESS_KEY_ID: ${{ secrets.BYTEPLUS_ACCESS_KEY_ID }} BYTEPLUS_SECRET_ACCESS_KEY: ${{ secrets.BYTEPLUS_SECRET_ACCESS_KEY }} CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} OPENCLAW_CODEX_AUTH_JSON: ${{ secrets.OPENCLAW_CODEX_AUTH_JSON }} OPENCLAW_CODEX_CONFIG_TOML: ${{ secrets.OPENCLAW_CODEX_CONFIG_TOML }} OPENCLAW_CLAUDE_JSON: ${{ secrets.OPENCLAW_CLAUDE_JSON }} OPENCLAW_CLAUDE_CREDENTIALS_JSON: ${{ secrets.OPENCLAW_CLAUDE_CREDENTIALS_JSON }} OPENCLAW_CLAUDE_SETTINGS_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_JSON }} OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON: ${{ secrets.OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON }} OPENCLAW_GEMINI_SETTINGS_JSON: ${{ secrets.OPENCLAW_GEMINI_SETTINGS_JSON }} FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }} OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }} OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }} maturity_scorecard_release_checks: name: Render maturity scorecard release docs needs: [resolve_target] if: contains(fromJSON('["all","qa"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.run_maturity_scorecard == 'true' permissions: actions: read contents: read uses: ./.github/workflows/maturity-scorecard.yml with: ref: ${{ needs.resolve_target.outputs.ref }} expected_sha: ${{ needs.resolve_target.outputs.revision }} secrets: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} qa_lab_parity_lane_release_checks: name: Run QA Lab parity lane (${{ matrix.lane }}) needs: [resolve_target] if: contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group) continue-on-error: true runs-on: ubuntu-24.04 timeout-minutes: 30 permissions: contents: read strategy: fail-fast: false matrix: include: - lane: candidate output_dir: openai-candidate - lane: baseline output_dir: anthropic-baseline env: QA_PARITY_CONCURRENCY: "1" OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000" OPENAI_API_KEY: "" ANTHROPIC_API_KEY: "" OPENCLAW_LIVE_OPENAI_KEY: "" OPENCLAW_LIVE_ANTHROPIC_KEY: "" OPENCLAW_LIVE_GEMINI_KEY: "" OPENCLAW_LIVE_SETUP_TOKEN_VALUE: "" OPENCLAW_BUILD_PRIVATE_QA: "1" OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1" steps: - name: Checkout selected ref uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: persist-credentials: false ref: ${{ needs.resolve_target.outputs.revision }} fetch-depth: 1 - name: Setup Node environment uses: ./.github/actions/setup-node-env with: node-version: ${{ env.NODE_VERSION }} install-bun: "true" - name: Build private QA runtime env: NODE_OPTIONS: --max-old-space-size=8192 run: node scripts/build-all.mjs qaRuntime - name: Run parity lane id: run_lane env: QA_PARITY_LANE: ${{ matrix.lane }} QA_PARITY_OUTPUT_DIR: ${{ matrix.output_dir }} run: | set -euo pipefail case "${QA_PARITY_LANE}" in candidate) model="${OPENCLAW_CI_OPENAI_MODEL}" alt_model="openai/gpt-5.6-luna-alt" ;; baseline) model="anthropic/claude-opus-4-8" alt_model="anthropic/claude-sonnet-4-6" ;; *) echo "Unknown QA parity lane: ${QA_PARITY_LANE}" >&2 exit 1 ;; esac pnpm openclaw qa suite \ --provider-mode mock-openai \ --parity-pack agentic \ --concurrency "${QA_PARITY_CONCURRENCY}" \ --model "${model}" \ --alt-model "${alt_model}" \ --output-dir ".artifacts/qa-e2e/${QA_PARITY_OUTPUT_DIR}" - name: Upload parity lane artifacts id: upload_parity_lane_artifacts if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-qa-parity-${{ matrix.lane }}-${{ needs.resolve_target.outputs.revision }} path: .artifacts/qa-e2e/ retention-days: 14 if-no-files-found: error - name: Record advisory status if: always() shell: bash env: RELEASE_CHECK_JOB: qa_lab_parity_lane_release_checks RELEASE_CHECK_VARIANT: ${{ matrix.lane }} RELEASE_CHECK_TARGET_SHA: ${{ needs.resolve_target.outputs.revision }} JOB_STATUS: ${{ job.status }} RELEASE_CHECK_STEP_OUTCOMES: ${{ steps.run_lane.outcome }} ${{ steps.upload_parity_lane_artifacts.outcome }} run: &record_release_check_advisory_status | set -euo pipefail status="success" mark_status() { case "$1" in failure) status="failure" ;; cancelled) if [[ "$status" != "failure" ]]; then status="cancelled" fi ;; success|skipped|"") ;; *) status="failure" ;; esac } mark_status "${JOB_STATUS:-}" for outcome in ${RELEASE_CHECK_STEP_OUTCOMES:-}; do mark_status "$outcome" done mkdir -p .artifacts/release-check-status status_name="$RELEASE_CHECK_JOB" if [[ -n "${RELEASE_CHECK_VARIANT:-}" ]]; then status_name="${status_name}-${RELEASE_CHECK_VARIANT}" fi status_path=".artifacts/release-check-status/${status_name}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.env" { printf 'run_id=%s\n' "${GITHUB_RUN_ID:?}" printf 'run_attempt=%s\n' "${GITHUB_RUN_ATTEMPT:?}" printf 'target_sha=%s\n' "${RELEASE_CHECK_TARGET_SHA:?}" printf 'job=%s\n' "$RELEASE_CHECK_JOB" printf 'variant=%s\n' "${RELEASE_CHECK_VARIANT:-}" printf 'status=%s\n' "$status" printf 'job_status=%s\n' "${JOB_STATUS:-}" printf 'step_outcomes=%s\n' "${RELEASE_CHECK_STEP_OUTCOMES:-}" } > "$status_path" - name: Upload advisory status if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-check-status-qa-parity-${{ matrix.lane }}-${{ needs.resolve_target.outputs.revision }}-${{ github.run_id }}-${{ github.run_attempt }} path: .artifacts/release-check-status/qa_lab_parity_lane_release_checks-${{ matrix.lane }}-${{ github.run_id }}-${{ github.run_attempt }}.env retention-days: 14 if-no-files-found: error qa_lab_parity_report_release_checks: name: Run QA Lab parity report needs: [resolve_target, qa_lab_parity_lane_release_checks] if: contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group) continue-on-error: true runs-on: ubuntu-24.04 timeout-minutes: 20 permissions: contents: read actions: read env: OPENCLAW_BUILD_PRIVATE_QA: "1" OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1" steps: - name: Checkout selected ref uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: persist-credentials: false ref: ${{ needs.resolve_target.outputs.revision }} fetch-depth: 1 - name: Setup Node environment uses: ./.github/actions/setup-node-env with: node-version: ${{ env.NODE_VERSION }} install-bun: "true" - name: Download parity lane artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: pattern: release-qa-parity-*-${{ needs.resolve_target.outputs.revision }} path: .artifacts/qa-e2e/ merge-multiple: true - name: Build private QA runtime env: NODE_OPTIONS: --max-old-space-size=8192 run: node scripts/build-all.mjs qaRuntime - name: Generate parity report id: generate_report run: | pnpm openclaw qa parity-report \ --repo-root . \ --candidate-summary .artifacts/qa-e2e/openai-candidate/qa-suite-summary.json \ --baseline-summary .artifacts/qa-e2e/anthropic-baseline/qa-suite-summary.json \ --candidate-label "${OPENCLAW_CI_OPENAI_MODEL}" \ --baseline-label anthropic/claude-opus-4-8 \ --output-dir .artifacts/qa-e2e/parity - name: Upload parity artifacts id: upload_parity_artifacts if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-qa-parity-${{ needs.resolve_target.outputs.revision }} path: .artifacts/qa-e2e/ retention-days: 14 if-no-files-found: error - name: Record advisory status if: always() shell: bash env: RELEASE_CHECK_JOB: qa_lab_parity_report_release_checks RELEASE_CHECK_TARGET_SHA: ${{ needs.resolve_target.outputs.revision }} JOB_STATUS: ${{ job.status }} RELEASE_CHECK_STEP_OUTCOMES: ${{ steps.generate_report.outcome }} ${{ steps.upload_parity_artifacts.outcome }} run: *record_release_check_advisory_status - name: Upload advisory status if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-check-status-qa-parity-report-${{ needs.resolve_target.outputs.revision }}-${{ github.run_id }}-${{ github.run_attempt }} path: .artifacts/release-check-status/qa_lab_parity_report_release_checks-${{ github.run_id }}-${{ github.run_attempt }}.env retention-days: 14 if-no-files-found: error qa_lab_runtime_parity_release_checks: name: Run QA Lab runtime parity lane needs: [resolve_target] if: contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group) continue-on-error: true runs-on: blacksmith-8vcpu-ubuntu-2404 timeout-minutes: 45 permissions: contents: read env: QA_PARITY_CONCURRENCY: "1" OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000" OPENAI_API_KEY: "" ANTHROPIC_API_KEY: "" OPENCLAW_LIVE_OPENAI_KEY: "" OPENCLAW_LIVE_ANTHROPIC_KEY: "" OPENCLAW_LIVE_GEMINI_KEY: "" OPENCLAW_LIVE_SETUP_TOKEN_VALUE: "" OPENCLAW_BUILD_PRIVATE_QA: "1" OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1" steps: - name: Checkout selected ref uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: persist-credentials: false ref: ${{ needs.resolve_target.outputs.revision }} fetch-depth: 1 - name: Setup Node environment uses: ./.github/actions/setup-node-env with: node-version: ${{ env.NODE_VERSION }} install-bun: "true" - name: Build private QA runtime env: NODE_OPTIONS: --max-old-space-size=8192 run: node scripts/build-all.mjs qaRuntime - name: Run runtime parity lane id: runtime_parity_lane run: | set -euo pipefail pnpm openclaw qa suite \ --provider-mode mock-openai \ --parity-pack agentic \ --concurrency "${QA_PARITY_CONCURRENCY}" \ --model "${OPENCLAW_CI_OPENAI_MODEL}" \ --alt-model "openai/gpt-5.6-luna-alt" \ --runtime-pair openclaw,codex \ --output-dir ".artifacts/qa-e2e/runtime-parity" - name: Run standard runtime parity tier id: runtime_parity_standard_lane if: ${{ always() && steps.runtime_parity_lane.outcome != 'skipped' && steps.runtime_parity_lane.outcome != 'cancelled' }} run: | set -euo pipefail pnpm openclaw qa suite \ --provider-mode mock-openai \ --runtime-parity-tier standard \ --concurrency "${QA_PARITY_CONCURRENCY}" \ --model "${OPENCLAW_CI_OPENAI_MODEL}" \ --alt-model "openai/gpt-5.6-luna-alt" \ --runtime-pair openclaw,codex \ --output-dir ".artifacts/qa-e2e/runtime-parity-standard" - name: Run soak runtime parity tier id: runtime_parity_soak_lane if: ${{ always() && needs.resolve_target.outputs.run_release_soak == 'true' && steps.runtime_parity_lane.outcome != 'skipped' && steps.runtime_parity_lane.outcome != 'cancelled' }} run: | set -euo pipefail pnpm openclaw qa suite \ --provider-mode mock-openai \ --runtime-parity-tier soak \ --concurrency "${QA_PARITY_CONCURRENCY}" \ --model "${OPENCLAW_CI_OPENAI_MODEL}" \ --alt-model "openai/gpt-5.6-luna-alt" \ --runtime-pair openclaw,codex \ --output-dir ".artifacts/qa-e2e/runtime-parity-soak" - name: Generate runtime parity report id: generate_runtime_parity_report if: always() run: | set -euo pipefail pnpm openclaw qa parity-report \ --repo-root . \ --runtime-axis \ --summary .artifacts/qa-e2e/runtime-parity/qa-suite-summary.json \ --output-dir .artifacts/qa-e2e/runtime-parity-report - name: Generate standard runtime parity report id: generate_runtime_parity_standard_report if: always() run: | set -euo pipefail pnpm openclaw qa parity-report \ --repo-root . \ --runtime-axis \ --summary .artifacts/qa-e2e/runtime-parity-standard/qa-suite-summary.json \ --output-dir .artifacts/qa-e2e/runtime-parity-standard-report - name: Generate soak runtime parity report id: generate_runtime_parity_soak_report if: ${{ always() && needs.resolve_target.outputs.run_release_soak == 'true' && steps.runtime_parity_soak_lane.outcome != 'skipped' && steps.runtime_parity_soak_lane.outcome != 'cancelled' }} run: | set -euo pipefail summary=".artifacts/qa-e2e/runtime-parity-soak/qa-suite-summary.json" if [[ ! -f "$summary" ]]; then echo "No soak runtime parity summary was produced." exit 0 fi pnpm openclaw qa parity-report \ --repo-root . \ --runtime-axis \ --summary "$summary" \ --output-dir .artifacts/qa-e2e/runtime-parity-soak-report - name: Upload runtime parity artifacts id: upload_runtime_parity_artifacts if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }} path: .artifacts/qa-e2e/ retention-days: 14 if-no-files-found: error - name: Record advisory status if: always() shell: bash env: RELEASE_CHECK_JOB: qa_lab_runtime_parity_release_checks RELEASE_CHECK_TARGET_SHA: ${{ needs.resolve_target.outputs.revision }} JOB_STATUS: ${{ job.status }} RELEASE_CHECK_STEP_OUTCOMES: ${{ steps.runtime_parity_lane.outcome }} ${{ steps.runtime_parity_standard_lane.outcome }} ${{ steps.runtime_parity_soak_lane.outcome }} ${{ steps.generate_runtime_parity_report.outcome }} ${{ steps.generate_runtime_parity_standard_report.outcome }} ${{ steps.generate_runtime_parity_soak_report.outcome }} ${{ steps.upload_runtime_parity_artifacts.outcome }} run: *record_release_check_advisory_status - name: Upload advisory status if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-check-status-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}-${{ github.run_id }}-${{ github.run_attempt }} path: .artifacts/release-check-status/qa_lab_runtime_parity_release_checks-${{ github.run_id }}-${{ github.run_attempt }}.env retention-days: 14 if-no-files-found: error runtime_tool_coverage_release_checks: name: Enforce QA Lab runtime tool coverage needs: [resolve_target, qa_lab_runtime_parity_release_checks] if: contains(fromJSON('["all","qa","qa-parity"]'), needs.resolve_target.outputs.rerun_group) runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: contents: read actions: read env: OPENCLAW_BUILD_PRIVATE_QA: "1" OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1" steps: - name: Checkout selected ref uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: persist-credentials: false ref: ${{ needs.resolve_target.outputs.revision }} fetch-depth: 1 - name: Setup Node environment uses: ./.github/actions/setup-node-env with: node-version: ${{ env.NODE_VERSION }} install-bun: "true" - name: Download runtime parity status uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: release-check-status-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}-${{ github.run_id }}-${{ github.run_attempt }} path: .artifacts/release-check-status/ - name: Verify runtime parity producer status id: verify_runtime_parity_status shell: bash env: RELEASE_CHECK_TARGET_SHA: ${{ needs.resolve_target.outputs.revision }} run: | set -euo pipefail status_path=".artifacts/release-check-status/qa_lab_runtime_parity_release_checks-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.env" status_field() { local key="$1" awk -v key="$key" ' index($0, key "=") == 1 { count += 1 value = substr($0, length(key) + 2) } END { if (count != 1) { exit 1 } print value } ' "$status_path" } run_id="$(status_field run_id)" run_attempt="$(status_field run_attempt)" target_sha="$(status_field target_sha)" job_name="$(status_field job)" variant="$(status_field variant)" if [[ "$run_id" != "$GITHUB_RUN_ID" || "$run_attempt" != "$GITHUB_RUN_ATTEMPT" || "$target_sha" != "$RELEASE_CHECK_TARGET_SHA" || "$job_name" != "qa_lab_runtime_parity_release_checks" || -n "$variant" ]]; then echo "Runtime parity producer status does not match the current run, attempt, target, job, and variant." >&2 exit 1 fi status="$(sed -n 's/^status=//p' "$status_path" | tail -n 1)" if [[ "$status" != "success" ]]; then echo "Runtime parity producer status is ${status:-missing}; skipping coverage artifact consumer." echo "ready=false" >> "$GITHUB_OUTPUT" exit 0 fi echo "ready=true" >> "$GITHUB_OUTPUT" - name: Download runtime parity artifacts if: steps.verify_runtime_parity_status.outputs.ready == 'true' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: release-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }} path: .artifacts/qa-e2e/ - name: Enforce standard runtime tool coverage if: steps.verify_runtime_parity_status.outputs.ready == 'true' run: | set -euo pipefail pnpm openclaw qa coverage \ --repo-root . \ --tools \ --summary .artifacts/qa-e2e/runtime-parity-standard/qa-suite-summary.json \ --output .artifacts/qa-e2e/runtime-parity-standard-report/qa-runtime-tool-coverage-report.md - name: Upload runtime tool coverage artifacts if: ${{ always() && steps.verify_runtime_parity_status.outputs.ready == 'true' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-qa-runtime-tool-coverage-${{ needs.resolve_target.outputs.revision }} path: .artifacts/qa-e2e/runtime-parity-standard-report/ retention-days: 14 if-no-files-found: error qa_live_matrix_release_checks: name: Run QA Lab live Matrix lane needs: [resolve_target] if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_matrix_enabled == 'true' continue-on-error: true runs-on: blacksmith-16vcpu-ubuntu-2404 timeout-minutes: 60 permissions: contents: read pull-requests: read environment: qa-live-shared env: OPENCLAW_BUILD_PRIVATE_QA: "1" OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1" steps: - name: Checkout selected ref uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: persist-credentials: false ref: ${{ needs.resolve_target.outputs.revision }} fetch-depth: 1 - name: Setup Node environment uses: ./.github/actions/setup-node-env with: node-version: ${{ env.NODE_VERSION }} install-bun: "true" - name: Build private QA runtime env: NODE_OPTIONS: --max-old-space-size=8192 run: node scripts/build-all.mjs qaRuntime - name: Run Matrix live lane id: run_lane shell: bash env: OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1" OPENCLAW_QA_MATRIX_CANARY_TIMEOUT_MS: "90000" OPENCLAW_QA_MATRIX_NO_REPLY_WINDOW_MS: "3000" run: | set -euo pipefail output_dir=".artifacts/qa-e2e/matrix-live-release-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT" matrix_args=( --repo-root . \ --provider-mode mock-openai \ --model mock-openai/gpt-5.6-luna \ --alt-model mock-openai/gpt-5.6-luna-alt \ --profile fast \ --fast ) if pnpm openclaw qa matrix --help 2>/dev/null | grep -F -q -- "--fail-fast"; then matrix_args+=(--fail-fast) fi for attempt in 1 2; do attempt_output_dir="${output_dir}/attempt-${attempt}" if pnpm openclaw qa matrix --output-dir "${attempt_output_dir}" "${matrix_args[@]}"; then exit 0 fi if [[ "${attempt}" == "2" ]]; then exit 1 fi echo "Matrix live lane failed on attempt ${attempt}; retrying once..." >&2 sleep 10 done - name: Upload Matrix QA artifacts id: upload_matrix_qa_artifacts if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-qa-live-matrix-${{ needs.resolve_target.outputs.revision }} path: .artifacts/qa-e2e/ retention-days: 14 if-no-files-found: error - name: Record advisory status if: always() shell: bash env: RELEASE_CHECK_JOB: qa_live_matrix_release_checks RELEASE_CHECK_TARGET_SHA: ${{ needs.resolve_target.outputs.revision }} JOB_STATUS: ${{ job.status }} RELEASE_CHECK_STEP_OUTCOMES: ${{ steps.run_lane.outcome }} ${{ steps.upload_matrix_qa_artifacts.outcome }} run: *record_release_check_advisory_status - name: Upload advisory status if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-check-status-qa-live-matrix-${{ needs.resolve_target.outputs.revision }}-${{ github.run_id }}-${{ github.run_attempt }} path: .artifacts/release-check-status/qa_live_matrix_release_checks-${{ github.run_id }}-${{ github.run_attempt }}.env retention-days: 14 if-no-files-found: error # The dispatched child owns Telegram evidence/status artifacts; this blocking job # carries its exact conclusion into the parent summary without copying secrets or artifacts. qa_live_telegram_release_checks: name: Run QA Lab live Telegram lane needs: [resolve_target] if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_telegram_enabled == 'true' runs-on: ubuntu-24.04 timeout-minutes: 210 permissions: actions: write contents: read steps: - name: Dispatch and await trusted Telegram QA env: GH_TOKEN: ${{ github.token }} TARGET_REF: ${{ needs.resolve_target.outputs.ref }} TARGET_SHA: ${{ needs.resolve_target.outputs.revision }} shell: bash run: | set -euo pipefail workflow="openclaw-release-telegram-qa.yml" run_id="" run_name="" find_child_runs() { RUN_NAME="$run_name" gh api -X GET \ "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/runs" \ -F event=workflow_dispatch \ -F branch=main \ -F per_page=100 \ --jq '[.workflow_runs[] | select(.display_title == env.RUN_NAME) | {id, head_sha}]' } cancel_child() { if [[ -n "${run_id:-}" ]]; then gh api -X POST "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/cancel" >/dev/null 2>&1 || true fi } cancel_child_on_failure() { status="$?" trap - EXIT if [[ "$status" -ne 0 ]]; then set +e if [[ -z "${run_id:-}" && -n "${run_name:-}" ]]; then # The nonce makes delayed discovery safe after an ambiguous dispatch response. for _ in $(seq 1 6); do matches_json="$(find_child_runs 2>/dev/null)" || matches_json="[]" if [[ "$(jq 'length' <<<"$matches_json" 2>/dev/null)" == "1" ]]; then run_id="$(jq -r '.[0].id' <<<"$matches_json")" break fi sleep 2 done fi cancel_child fi exit "$status" } trap cancel_child_on_failure EXIT trap 'exit 130' INT trap 'exit 143' TERM for dispatch_attempt in 1 2 3 4 5; do expected_trusted_workflow_sha="$( gh api "repos/${GITHUB_REPOSITORY}/commits/main" --jq .sha )" dispatch_id="release-checks-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${dispatch_attempt}-$(openssl rand -hex 16)" run_name="OpenClaw Release Telegram QA ${dispatch_id}" run_id="" child_head_sha="" gh workflow run "$workflow" \ --repo "$GITHUB_REPOSITORY" \ --ref main \ -f dispatch_id="$dispatch_id" \ -f expected_trusted_workflow_sha="$expected_trusted_workflow_sha" \ -f target_ref="$TARGET_REF" \ -f target_sha="$TARGET_SHA" for _ in $(seq 1 60); do matches_json="$(find_child_runs)" match_count="$(jq 'length' <<<"$matches_json")" if ((match_count > 1)); then echo "Multiple Telegram QA runs matched ${run_name}; refusing to guess." >&2 exit 1 fi if ((match_count == 1)); then run_id="$(jq -r '.[0].id' <<<"$matches_json")" child_head_sha="$(jq -r '.[0].head_sha' <<<"$matches_json")" break fi sleep 5 done if [[ -z "$run_id" ]]; then echo "Could not find exact dispatched Telegram QA run ${run_name}." >&2 exit 1 fi if [[ "$child_head_sha" == "$expected_trusted_workflow_sha" ]]; then break fi echo "Trusted main moved from ${expected_trusted_workflow_sha} to ${child_head_sha} during dispatch attempt ${dispatch_attempt}; retrying." >&2 cancel_child run_id="" done if [[ -z "$run_id" ]]; then echo "Trusted main kept moving during Telegram QA dispatch; refusing an unpinned child." >&2 exit 1 fi child_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}" echo "Dispatched trusted Telegram QA: ${child_url}" for _ in $(seq 1 1080); do run_json="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}")" status="$(jq -r .status <<<"$run_json")" if [[ "$status" == "completed" ]]; then conclusion="$(jq -r .conclusion <<<"$run_json")" if [[ "$conclusion" != "success" ]]; then echo "Trusted Telegram QA concluded ${conclusion}: ${child_url}" >&2 exit 1 fi exit 0 fi sleep 10 done echo "Timed out awaiting trusted Telegram QA: ${child_url}" >&2 exit 1 qa_live_discord_release_checks: name: Run QA Lab live Discord lane needs: [resolve_target] if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_discord_enabled == 'true' continue-on-error: true runs-on: ubuntu-24.04 timeout-minutes: 60 permissions: contents: read pull-requests: read environment: qa-live-shared env: OPENCLAW_BUILD_PRIVATE_QA: "1" OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1" steps: - name: Checkout selected ref uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: persist-credentials: false ref: ${{ needs.resolve_target.outputs.revision }} fetch-depth: 1 - name: Setup Node environment uses: ./.github/actions/setup-node-env with: node-version: ${{ env.NODE_VERSION }} install-bun: "true" - &release_qa_required_credentials_step name: Validate required QA credential env env: OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }} OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }} shell: bash run: | set -euo pipefail require_var() { local key="$1" if [[ -z "${!key:-}" ]]; then echo "Missing required ${key}." >&2 exit 1 fi } require_var OPENCLAW_QA_CONVEX_SITE_URL require_var OPENCLAW_QA_CONVEX_SECRET_CI - name: Build private QA runtime env: NODE_OPTIONS: --max-old-space-size=8192 run: node scripts/build-all.mjs qaRuntime - name: Run Discord live lane id: run_lane shell: bash env: OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }} OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }} OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1" OPENCLAW_QA_DISCORD_CAPTURE_CONTENT: "1" run: | set -euo pipefail output_dir=".artifacts/qa-e2e/discord-live-release-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT" for attempt in 1 2; do attempt_output_dir="${output_dir}/attempt-${attempt}" if pnpm openclaw qa discord \ --repo-root . \ --output-dir "${attempt_output_dir}" \ --provider-mode mock-openai \ --model mock-openai/gpt-5.6-luna \ --alt-model mock-openai/gpt-5.6-luna-alt \ --fast \ --credential-source convex \ --credential-role ci; then exit 0 fi if [[ "${attempt}" == "2" ]]; then exit 1 fi echo "Discord live lane failed on attempt ${attempt}; retrying once..." >&2 sleep 10 done - name: Upload Discord QA artifacts id: upload_discord_qa_artifacts if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-qa-live-discord-${{ needs.resolve_target.outputs.revision }} path: .artifacts/qa-e2e/ retention-days: 14 if-no-files-found: error - name: Record advisory status if: always() shell: bash env: RELEASE_CHECK_JOB: qa_live_discord_release_checks RELEASE_CHECK_TARGET_SHA: ${{ needs.resolve_target.outputs.revision }} JOB_STATUS: ${{ job.status }} RELEASE_CHECK_STEP_OUTCOMES: ${{ steps.run_lane.outcome }} ${{ steps.upload_discord_qa_artifacts.outcome }} run: *record_release_check_advisory_status - name: Upload advisory status if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-check-status-qa-live-discord-${{ needs.resolve_target.outputs.revision }}-${{ github.run_id }}-${{ github.run_attempt }} path: .artifacts/release-check-status/qa_live_discord_release_checks-${{ github.run_id }}-${{ github.run_attempt }}.env retention-days: 14 if-no-files-found: error qa_live_whatsapp_release_checks: name: Run QA Lab live WhatsApp lane needs: [resolve_target] if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_whatsapp_enabled == 'true' continue-on-error: true runs-on: ubuntu-24.04 timeout-minutes: 60 concurrency: group: qa-live-whatsapp-shared cancel-in-progress: false permissions: contents: read pull-requests: read environment: qa-live-shared env: OPENCLAW_BUILD_PRIVATE_QA: "1" OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1" steps: - name: Checkout selected ref uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: persist-credentials: false ref: ${{ needs.resolve_target.outputs.revision }} fetch-depth: 1 - name: Setup Node environment uses: ./.github/actions/setup-node-env with: node-version: ${{ env.NODE_VERSION }} install-bun: "true" - *release_qa_required_credentials_step - name: Build private QA runtime env: NODE_OPTIONS: --max-old-space-size=8192 run: node scripts/build-all.mjs qaRuntime - name: Run WhatsApp live lane id: run_lane shell: bash env: OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }} OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }} OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1" OPENCLAW_QA_WHATSAPP_CAPTURE_CONTENT: "1" run: | set -euo pipefail output_dir=".artifacts/qa-e2e/whatsapp-live-release-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT" for attempt in 1 2; do attempt_output_dir="${output_dir}/attempt-${attempt}" if pnpm openclaw qa whatsapp \ --repo-root . \ --output-dir "${attempt_output_dir}" \ --provider-mode mock-openai \ --model mock-openai/gpt-5.6-luna \ --alt-model mock-openai/gpt-5.6-luna-alt \ --fast \ --credential-source convex \ --credential-role ci; then exit 0 fi if [[ "${attempt}" == "2" ]]; then exit 1 fi echo "WhatsApp live lane failed on attempt ${attempt}; retrying once..." >&2 sleep 10 done - name: Upload WhatsApp QA artifacts id: upload_whatsapp_qa_artifacts if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-qa-live-whatsapp-${{ needs.resolve_target.outputs.revision }} path: .artifacts/qa-e2e/ retention-days: 14 if-no-files-found: error - name: Record advisory status if: always() shell: bash env: RELEASE_CHECK_JOB: qa_live_whatsapp_release_checks RELEASE_CHECK_TARGET_SHA: ${{ needs.resolve_target.outputs.revision }} JOB_STATUS: ${{ job.status }} RELEASE_CHECK_STEP_OUTCOMES: ${{ steps.run_lane.outcome }} ${{ steps.upload_whatsapp_qa_artifacts.outcome }} run: *record_release_check_advisory_status - name: Upload advisory status if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-check-status-qa-live-whatsapp-${{ needs.resolve_target.outputs.revision }}-${{ github.run_id }}-${{ github.run_attempt }} path: .artifacts/release-check-status/qa_live_whatsapp_release_checks-${{ github.run_id }}-${{ github.run_attempt }}.env retention-days: 14 if-no-files-found: error qa_live_slack_release_checks: name: Run QA Lab live Slack lane needs: [resolve_target] if: contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_slack_enabled == 'true' continue-on-error: true runs-on: ubuntu-24.04 timeout-minutes: 60 permissions: contents: read pull-requests: read environment: qa-live-shared env: OPENCLAW_BUILD_PRIVATE_QA: "1" OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1" steps: - name: Checkout selected ref uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: persist-credentials: false ref: ${{ needs.resolve_target.outputs.revision }} fetch-depth: 1 - name: Setup Node environment uses: ./.github/actions/setup-node-env with: node-version: ${{ env.NODE_VERSION }} install-bun: "true" - *release_qa_required_credentials_step - name: Build private QA runtime env: NODE_OPTIONS: --max-old-space-size=8192 run: node scripts/build-all.mjs qaRuntime - name: Run Slack live lane id: run_lane shell: bash env: OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }} OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }} OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1" OPENCLAW_QA_SLACK_CAPTURE_CONTENT: "1" run: | set -euo pipefail output_dir=".artifacts/qa-e2e/slack-live-release-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT" for attempt in 1 2; do attempt_output_dir="${output_dir}/attempt-${attempt}" if pnpm openclaw qa slack \ --repo-root . \ --output-dir "${attempt_output_dir}" \ --provider-mode mock-openai \ --model mock-openai/gpt-5.6-luna \ --alt-model mock-openai/gpt-5.6-luna-alt \ --fast \ --credential-source convex \ --credential-role ci; then exit 0 fi if [[ "${attempt}" == "2" ]]; then exit 1 fi echo "Slack live lane failed on attempt ${attempt}; retrying once..." >&2 sleep 10 done - name: Upload Slack QA artifacts id: upload_slack_qa_artifacts if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-qa-live-slack-${{ needs.resolve_target.outputs.revision }} path: .artifacts/qa-e2e/ retention-days: 14 if-no-files-found: error - name: Record advisory status if: always() shell: bash env: RELEASE_CHECK_JOB: qa_live_slack_release_checks RELEASE_CHECK_TARGET_SHA: ${{ needs.resolve_target.outputs.revision }} JOB_STATUS: ${{ job.status }} RELEASE_CHECK_STEP_OUTCOMES: ${{ steps.run_lane.outcome }} ${{ steps.upload_slack_qa_artifacts.outcome }} run: *record_release_check_advisory_status - name: Upload advisory status if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-check-status-qa-live-slack-${{ needs.resolve_target.outputs.revision }}-${{ github.run_id }}-${{ github.run_attempt }} path: .artifacts/release-check-status/qa_live_slack_release_checks-${{ github.run_id }}-${{ github.run_attempt }}.env retention-days: 14 if-no-files-found: error summary: name: Verify release checks needs: - resolve_target - prepare_release_package - install_smoke_release_checks - cross_os_release_checks - live_repo_e2e_release_checks - docker_e2e_release_checks - package_acceptance_release_checks - qa_lab_parity_lane_release_checks - maturity_scorecard_release_checks - qa_lab_parity_report_release_checks - qa_lab_runtime_parity_release_checks - runtime_tool_coverage_release_checks - qa_live_matrix_release_checks - qa_live_telegram_release_checks - qa_live_discord_release_checks - qa_live_whatsapp_release_checks - qa_live_slack_release_checks if: always() runs-on: ubuntu-24.04 permissions: actions: read timeout-minutes: 5 steps: - name: Download advisory status artifacts if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: pattern: release-check-status-*-${{ needs.resolve_target.outputs.revision }}-${{ github.run_id }}-${{ github.run_attempt }} path: .artifacts/release-check-status merge-multiple: true - name: Verify release check results shell: bash env: RESOLVE_TARGET_RESULT: ${{ needs.resolve_target.result }} PREPARE_RELEASE_PACKAGE_RESULT: ${{ needs.prepare_release_package.result }} INSTALL_SMOKE_RELEASE_CHECKS_RESULT: ${{ needs.install_smoke_release_checks.result }} CROSS_OS_RELEASE_CHECKS_RESULT: ${{ needs.cross_os_release_checks.result }} LIVE_REPO_E2E_RELEASE_CHECKS_RESULT: ${{ needs.live_repo_e2e_release_checks.result }} DOCKER_E2E_RELEASE_CHECKS_RESULT: ${{ needs.docker_e2e_release_checks.result }} PACKAGE_ACCEPTANCE_RELEASE_CHECKS_RESULT: ${{ needs.package_acceptance_release_checks.result }} QA_LAB_PARITY_LANE_RELEASE_CHECKS_RESULT: ${{ needs.qa_lab_parity_lane_release_checks.result }} MATURITY_SCORECARD_RELEASE_CHECKS_RESULT: ${{ needs.maturity_scorecard_release_checks.result }} QA_LAB_PARITY_REPORT_RELEASE_CHECKS_RESULT: ${{ needs.qa_lab_parity_report_release_checks.result }} QA_LAB_RUNTIME_PARITY_RELEASE_CHECKS_RESULT: ${{ needs.qa_lab_runtime_parity_release_checks.result }} RUNTIME_TOOL_COVERAGE_RELEASE_CHECKS_RESULT: ${{ needs.runtime_tool_coverage_release_checks.result }} QA_LIVE_MATRIX_RELEASE_CHECKS_RESULT: ${{ needs.qa_live_matrix_release_checks.result }} QA_LIVE_TELEGRAM_RELEASE_CHECKS_RESULT: ${{ needs.qa_live_telegram_release_checks.result }} QA_LIVE_TELEGRAM_SELECTED: ${{ contains(fromJSON('["all","qa","qa-live"]'), needs.resolve_target.outputs.rerun_group) && needs.resolve_target.outputs.qa_live_telegram_enabled == 'true' }} QA_LIVE_DISCORD_RELEASE_CHECKS_RESULT: ${{ needs.qa_live_discord_release_checks.result }} QA_LIVE_WHATSAPP_RELEASE_CHECKS_RESULT: ${{ needs.qa_live_whatsapp_release_checks.result }} QA_LIVE_SLACK_RELEASE_CHECKS_RESULT: ${{ needs.qa_live_slack_release_checks.result }} RELEASE_CHECK_RUN_ID: ${{ github.run_id }} RELEASE_CHECK_RUN_ATTEMPT: ${{ github.run_attempt }} RELEASE_CHECK_TARGET_SHA: ${{ needs.resolve_target.outputs.revision }} WORKFLOW_REF: ${{ github.ref }} run: | set -euo pipefail failed=0 release_check_items=( "resolve_target=${RESOLVE_TARGET_RESULT}" "prepare_release_package=${PREPARE_RELEASE_PACKAGE_RESULT}" "install_smoke_release_checks=${INSTALL_SMOKE_RELEASE_CHECKS_RESULT}" "cross_os_release_checks=${CROSS_OS_RELEASE_CHECKS_RESULT}" "live_repo_e2e_release_checks=${LIVE_REPO_E2E_RELEASE_CHECKS_RESULT}" "docker_e2e_release_checks=${DOCKER_E2E_RELEASE_CHECKS_RESULT}" "package_acceptance_release_checks=${PACKAGE_ACCEPTANCE_RELEASE_CHECKS_RESULT}" "qa_lab_parity_lane_release_checks=${QA_LAB_PARITY_LANE_RELEASE_CHECKS_RESULT}" "maturity_scorecard_release_checks=${MATURITY_SCORECARD_RELEASE_CHECKS_RESULT}" "qa_lab_parity_report_release_checks=${QA_LAB_PARITY_REPORT_RELEASE_CHECKS_RESULT}" "qa_lab_runtime_parity_release_checks=${QA_LAB_RUNTIME_PARITY_RELEASE_CHECKS_RESULT}" "runtime_tool_coverage_release_checks=${RUNTIME_TOOL_COVERAGE_RELEASE_CHECKS_RESULT}" "qa_live_matrix_release_checks=${QA_LIVE_MATRIX_RELEASE_CHECKS_RESULT}" "qa_live_telegram_release_checks=${QA_LIVE_TELEGRAM_RELEASE_CHECKS_RESULT}" "qa_live_discord_release_checks=${QA_LIVE_DISCORD_RELEASE_CHECKS_RESULT}" "qa_live_whatsapp_release_checks=${QA_LIVE_WHATSAPP_RELEASE_CHECKS_RESULT}" "qa_live_slack_release_checks=${QA_LIVE_SLACK_RELEASE_CHECKS_RESULT}" ) advisory_status_items=( "qa_lab_parity_lane_release_checks=${QA_LAB_PARITY_LANE_RELEASE_CHECKS_RESULT}" "qa_lab_parity_report_release_checks=${QA_LAB_PARITY_REPORT_RELEASE_CHECKS_RESULT}" "qa_lab_runtime_parity_release_checks=${QA_LAB_RUNTIME_PARITY_RELEASE_CHECKS_RESULT}" "qa_live_matrix_release_checks=${QA_LIVE_MATRIX_RELEASE_CHECKS_RESULT}" "qa_live_discord_release_checks=${QA_LIVE_DISCORD_RELEASE_CHECKS_RESULT}" "qa_live_whatsapp_release_checks=${QA_LIVE_WHATSAPP_RELEASE_CHECKS_RESULT}" "qa_live_slack_release_checks=${QA_LIVE_SLACK_RELEASE_CHECKS_RESULT}" ) tideclaw_alpha=false if [[ "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then tideclaw_alpha=true fi status_field() { local file="$1" local key="$2" awk -v key="$key" ' index($0, key "=") == 1 { count += 1 value = substr($0, length(key) + 2) } END { if (count != 1) { exit 1 } print value } ' "$file" } validate_status_file() { local file="$1" local expected_job="$2" local expected_variant="$3" local actual_run_id actual_run_attempt actual_target_sha actual_job actual_variant if [[ ! -f "$file" ]]; then echo "::error::Missing advisory status file: ${file}" >&2 return 1 fi actual_run_id="$(status_field "$file" run_id)" || { echo "::error::Advisory status file ${file} has invalid run_id metadata." >&2 return 1 } actual_run_attempt="$(status_field "$file" run_attempt)" || { echo "::error::Advisory status file ${file} has invalid run_attempt metadata." >&2 return 1 } actual_target_sha="$(status_field "$file" target_sha)" || { echo "::error::Advisory status file ${file} has invalid target_sha metadata." >&2 return 1 } actual_job="$(status_field "$file" job)" || { echo "::error::Advisory status file ${file} has invalid job metadata." >&2 return 1 } actual_variant="$(status_field "$file" variant)" || { echo "::error::Advisory status file ${file} has invalid variant metadata." >&2 return 1 } if [[ "$actual_run_id" != "$RELEASE_CHECK_RUN_ID" || "$actual_run_attempt" != "$RELEASE_CHECK_RUN_ATTEMPT" || "$actual_target_sha" != "$RELEASE_CHECK_TARGET_SHA" || "$actual_job" != "$expected_job" || "$actual_variant" != "$expected_variant" ]]; then echo "::error::Advisory status file ${file} does not match the current run, attempt, target, job, and variant." >&2 return 1 fi } expected_status_artifact_count() { local name="$1" local fallback="$2" if [[ "$fallback" == "skipped" ]]; then printf '0\n' return fi if [[ "$name" == "qa_lab_parity_lane_release_checks" ]]; then printf '2\n' else printf '1\n' fi } expected_status_count=0 for item in "${advisory_status_items[@]}"; do name="${item%%=*}" raw_result="${item#*=}" expected_count="$(expected_status_artifact_count "$name" "$raw_result")" expected_status_count=$((expected_status_count + expected_count)) done status_dir=".artifacts/release-check-status" actual_status_count=0 if [[ -d "$status_dir" ]]; then actual_status_count="$(find "$status_dir" -maxdepth 1 -type f -name '*.env' | wc -l | tr -d '[:space:]')" fi if [[ "$actual_status_count" != "$expected_status_count" ]]; then status_count_message="Expected ${expected_status_count} advisory status artifacts for run ${RELEASE_CHECK_RUN_ID} attempt ${RELEASE_CHECK_RUN_ATTEMPT}, found ${actual_status_count}." if [[ "$tideclaw_alpha" == "true" ]]; then echo "::warning::${status_count_message} Tideclaw alpha treats non-package-safety release-check lanes as advisory." else echo "::error::${status_count_message}" failed=1 fi fi release_check_result() { local name="$1" local fallback="$2" local saw=0 local saw_failure=0 local saw_cancelled=0 local saw_skipped=0 local variants=() if [[ "$fallback" != "skipped" ]]; then if [[ "$name" == "qa_lab_parity_lane_release_checks" ]]; then variants=(candidate baseline) else variants=("") fi fi for variant in "${variants[@]}"; do suffix="" if [[ -n "$variant" ]]; then suffix="-${variant}" fi file="${status_dir}/${name}${suffix}-${RELEASE_CHECK_RUN_ID}-${RELEASE_CHECK_RUN_ATTEMPT}.env" if ! validate_status_file "$file" "$name" "$variant"; then saw_failure=1 continue fi saw=1 status="$(status_field "$file" status)" || status="" case "$status" in success) ;; skipped) saw_skipped=1 ;; cancelled) saw_cancelled=1 ;; failure|"") saw_failure=1 ;; *) saw_failure=1 ;; esac done if [[ "$fallback" == "skipped" ]]; then printf 'skipped\n' return fi if [[ "$saw_failure" == "1" ]]; then printf 'failure\n' elif [[ "$saw_cancelled" == "1" ]]; then printf 'cancelled\n' elif [[ "$saw_skipped" == "1" ]]; then printf 'skipped\n' elif [[ "$fallback" != "success" && "$fallback" != "skipped" ]]; then printf '%s\n' "$fallback" elif [[ "$saw" == "1" ]]; then printf 'success\n' elif [[ "$fallback" == "success" ]]; then printf 'failure\n' else printf '%s\n' "$fallback" fi } advisory_status_override_allowed() { case "$1" in qa_lab_parity_lane_release_checks|qa_lab_parity_report_release_checks|qa_lab_runtime_parity_release_checks|qa_live_matrix_release_checks|qa_live_discord_release_checks|qa_live_whatsapp_release_checks|qa_live_slack_release_checks) return 0 ;; *) return 1 ;; esac } for item in "${release_check_items[@]}"; do name="${item%%=*}" raw_result="${item#*=}" if advisory_status_override_allowed "$name"; then result="$(release_check_result "$name" "$raw_result")" else result="$raw_result" fi require_success=false if [[ "$name" == "qa_live_telegram_release_checks" && "$QA_LIVE_TELEGRAM_SELECTED" == "true" ]]; then require_success=true fi if [[ "$result" != "success" && ( "$result" != "skipped" || "$require_success" == "true" ) ]]; then if [[ "$tideclaw_alpha" == "true" ]]; then case "$name" in resolve_target|prepare_release_package|install_smoke_release_checks|package_acceptance_release_checks) ;; *) echo "::warning::${name} ended with ${result}; Tideclaw alpha treats non-package-safety release-check lanes as advisory." continue ;; esac fi echo "::error::${name} ended with ${result}" failed=1 fi done exit "$failed"