name: 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: - 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: 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; blank runs all selected live suites 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 concurrency: group: openclaw-release-checks-${{ inputs.expected_sha || inputs.ref }}-${{ inputs.rerun_group }} cancel-in-progress: false env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" NODE_VERSION: "24.x" PNPM_VERSION: "10.33.0" OPENCLAW_CI_OPENAI_MODEL: ${{ vars.OPENCLAW_CI_OPENAI_MODEL || 'openai/gpt-5.5' }} 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 }} rerun_group: ${{ steps.inputs.outputs.rerun_group }} live_suite_filter: ${{ steps.inputs.outputs.live_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_slack_enabled: ${{ steps.inputs.outputs.qa_live_slack_enabled }} package_acceptance_package_spec: ${{ steps.inputs.outputs.package_acceptance_package_spec }} steps: - name: Require main or release workflow ref for release checks env: WORKFLOW_REF: ${{ github.ref }} run: | set -euo pipefail 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]+$ ]]; then echo "Release checks must be dispatched from main, release/YYYY.M.D, or a Full Release Validation release-ci/- ref so workflow logic and secrets stay controlled." >&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@v6 with: persist-credentials: false ref: ${{ github.ref_name }} 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@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 }} run: | set -euo pipefail SELECTED_SHA="$(git rev-parse HEAD)" git fetch --no-tags origin '+refs/heads/*:refs/remotes/origin/*' git fetch --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: 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_RERUN_GROUP_INPUT: ${{ inputs.rerun_group }} RELEASE_LIVE_SUITE_FILTER_INPUT: ${{ inputs.live_suite_filter }} RELEASE_QA_SLACK_LIVE_CI_ENABLED: ${{ vars.OPENCLAW_QA_SLACK_LIVE_CI_ENABLED || 'false' }} RELEASE_PACKAGE_ACCEPTANCE_PACKAGE_SPEC_INPUT: ${{ inputs.package_acceptance_package_spec }} run: | set -euo pipefail qa_live_matrix_enabled=true qa_live_telegram_enabled=true qa_live_slack_enabled=false 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 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 if [[ "$RELEASE_PROFILE_INPUT" == "full" ]]; then run_release_soak=true 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 slack_selected=false 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 ;; qa-live-non-slack|qa-non-slack|non-slack|no-slack|without-slack) qa_filter_seen=true matrix_selected=true telegram_selected=true ;; 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-slack|qa-slack|slack) qa_filter_seen=true slack_selected="$qa_live_slack_ci_enabled" ;; esac done if [[ "$qa_filter_seen" == "true" ]]; then qa_live_matrix_enabled="$matrix_selected" qa_live_telegram_enabled="$telegram_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_INPUT" printf 'run_release_soak=%s\n' "$run_release_soak" printf 'rerun_group=%s\n' "$RELEASE_RERUN_GROUP_INPUT" printf 'live_suite_filter=%s\n' "$RELEASE_LIVE_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_slack_enabled=%s\n' "$qa_live_slack_enabled" printf 'package_acceptance_package_spec=%s\n' "$RELEASE_PACKAGE_ACCEPTANCE_PACKAGE_SPEC_INPUT" } >> "$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: ${{ inputs.release_profile }} RUN_RELEASE_SOAK: ${{ steps.inputs.outputs.run_release_soak }} RELEASE_RERUN_GROUP: ${{ inputs.rerun_group }} RELEASE_LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }} PACKAGE_ACCEPTANCE_PACKAGE_SPEC: ${{ inputs.package_acceptance_package_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 "- Rerun group: \`${RELEASE_RERUN_GROUP}\`" if [[ -n "${RELEASE_LIVE_SUITE_FILTER// }" ]]; then echo "- Live suite filter: \`${RELEASE_LIVE_SUITE_FILTER}\`" fi echo "- QA live lanes: Matrix \`${{ steps.inputs.outputs.qa_live_matrix_enabled }}\`, Telegram \`${{ steps.inputs.outputs.qa_live_telegram_enabled }}\`, Slack \`${{ steps.inputs.outputs.qa_live_slack_enabled }}\`" if [[ -n "${PACKAGE_ACCEPTANCE_PACKAGE_SPEC// }" ]]; then echo "- Package Acceptance package spec: \`${PACKAGE_ACCEPTANCE_PACKAGE_SPEC}\`" else echo "- Package Acceptance package spec: prepared release artifact" 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: 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.revision }} 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 retention-days: 14 if-no-files-found: error install_smoke_release_checks: needs: [resolve_target] if: contains(fromJSON('["all","install-smoke"]'), needs.resolve_target.outputs.rerun_group) permissions: contents: read packages: write uses: ./.github/workflows/install-smoke.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: ref: ${{ needs.resolve_target.outputs.revision }} provider: ${{ needs.resolve_target.outputs.provider }} mode: ${{ needs.resolve_target.outputs.mode }} candidate_artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }} candidate_file_name: openclaw-current.tgz candidate_version: ${{ needs.prepare_release_package.outputs.package_version }} candidate_source_sha: ${{ needs.prepare_release_package.outputs.source_sha }} openai_model: openai/gpt-5.4 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: write pull-requests: read uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml with: 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 }} 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 }} 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: write pull-requests: read uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml with: ref: ${{ needs.resolve_target.outputs.revision }} include_repo_e2e: false include_release_path_suites: true include_openwebui: ${{ needs.resolve_target.outputs.release_profile != 'minimum' }} include_live_suites: false release_test_profile: ${{ needs.resolve_target.outputs.release_profile }} package_artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }} 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: write pull-requests: read uses: ./.github/workflows/package-acceptance.yml with: workflow_ref: ${{ github.ref_name }} source: ${{ needs.resolve_target.outputs.package_acceptance_package_spec != '' && 'npm' || 'artifact' }} package_spec: ${{ needs.resolve_target.outputs.package_acceptance_package_spec || 'openclaw@beta' }} artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }} package_sha256: ${{ needs.prepare_release_package.outputs.package_sha256 }} suite_profile: custom docker_lanes: doctor-switch update-channel-switch upgrade-survivor published-upgrade-survivor plugins-offline plugin-update published_upgrade_survivor_baselines: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'all-since-2026.4.23' || '' }} published_upgrade_survivor_scenarios: ${{ needs.resolve_target.outputs.run_release_soak == 'true' && 'reported-issues' || '' }} telegram_mode: mock-openai telegram_scenarios: telegram-help-command,telegram-commands-command,telegram-tools-compact-command,telegram-whoami-command,telegram-context-command,telegram-current-session-status-tool,telegram-mention-gating 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 }} 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 }} 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) runs-on: blacksmith-8vcpu-ubuntu-2404 timeout-minutes: 30 permissions: contents: read strategy: fail-fast: false matrix: include: - lane: candidate output_dir: gpt54 - lane: baseline output_dir: opus46 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@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 }} pnpm-version: ${{ env.PNPM_VERSION }} install-bun: "true" - name: Build private QA runtime run: pnpm build - name: Run parity 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.4-alt" ;; baseline) model="anthropic/claude-opus-4-6" 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 if: always() uses: actions/upload-artifact@v4 with: name: release-qa-parity-${{ matrix.lane }}-${{ needs.resolve_target.outputs.revision }} path: .artifacts/qa-e2e/ retention-days: 14 if-no-files-found: warn 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) runs-on: blacksmith-8vcpu-ubuntu-2404 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@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 }} pnpm-version: ${{ env.PNPM_VERSION }} install-bun: "true" - name: Download parity lane artifacts uses: actions/download-artifact@v4 with: pattern: release-qa-parity-*-${{ needs.resolve_target.outputs.revision }} path: .artifacts/qa-e2e/ merge-multiple: true - name: Build private QA runtime run: pnpm build - name: Generate parity report run: | pnpm openclaw qa parity-report \ --repo-root . \ --candidate-summary .artifacts/qa-e2e/gpt54/qa-suite-summary.json \ --baseline-summary .artifacts/qa-e2e/opus46/qa-suite-summary.json \ --candidate-label "${OPENCLAW_CI_OPENAI_MODEL}" \ --baseline-label anthropic/claude-opus-4-6 \ --output-dir .artifacts/qa-e2e/parity - name: Upload parity artifacts if: always() uses: actions/upload-artifact@v4 with: name: release-qa-parity-${{ needs.resolve_target.outputs.revision }} path: .artifacts/qa-e2e/ retention-days: 14 if-no-files-found: warn 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' runs-on: blacksmith-8vcpu-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@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 }} pnpm-version: ${{ env.PNPM_VERSION }} install-bun: "true" - name: Build private QA runtime run: pnpm build - 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.5 \ --alt-model mock-openai/gpt-5.5-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 if: always() uses: actions/upload-artifact@v4 with: name: release-qa-live-matrix-${{ needs.resolve_target.outputs.revision }} path: .artifacts/qa-e2e/ retention-days: 14 if-no-files-found: warn 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: blacksmith-8vcpu-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@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 }} pnpm-version: ${{ env.PNPM_VERSION }} install-bun: "true" - 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 run: pnpm build - name: Run Telegram 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_TELEGRAM_CAPTURE_CONTENT: "1" run: | set -euo pipefail output_dir=".artifacts/qa-e2e/telegram-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 telegram \ --repo-root . \ --output-dir "${attempt_output_dir}" \ --provider-mode mock-openai \ --model mock-openai/gpt-5.5 \ --alt-model mock-openai/gpt-5.5-alt \ --fast \ --credential-source convex \ --credential-role ci; then exit 0 fi if [[ "${attempt}" == "2" ]]; then exit 1 fi echo "Telegram live lane failed on attempt ${attempt}; retrying once..." >&2 sleep 10 done - name: Upload Telegram QA artifacts if: always() uses: actions/upload-artifact@v4 with: name: release-qa-live-telegram-${{ needs.resolve_target.outputs.revision }} path: .artifacts/qa-e2e/ retention-days: 14 if-no-files-found: warn 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' && vars.OPENCLAW_QA_SLACK_LIVE_CI_ENABLED == 'true' runs-on: blacksmith-8vcpu-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@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 }} pnpm-version: ${{ env.PNPM_VERSION }} install-bun: "true" - 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 run: pnpm build - 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.5 \ --alt-model mock-openai/gpt-5.5-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 if: always() uses: actions/upload-artifact@v4 with: name: release-qa-live-slack-${{ needs.resolve_target.outputs.revision }} path: .artifacts/qa-e2e/ retention-days: 14 if-no-files-found: warn summary: name: Verify release checks needs: - 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 - qa_lab_parity_report_release_checks - qa_live_matrix_release_checks - qa_live_telegram_release_checks - qa_live_slack_release_checks if: always() runs-on: ubuntu-24.04 permissions: {} timeout-minutes: 5 steps: - name: Verify release check results shell: bash run: | set -euo pipefail failed=0 for item in \ "prepare_release_package=${{ needs.prepare_release_package.result }}" \ "install_smoke_release_checks=${{ needs.install_smoke_release_checks.result }}" \ "cross_os_release_checks=${{ needs.cross_os_release_checks.result }}" \ "live_repo_e2e_release_checks=${{ needs.live_repo_e2e_release_checks.result }}" \ "docker_e2e_release_checks=${{ needs.docker_e2e_release_checks.result }}" \ "package_acceptance_release_checks=${{ needs.package_acceptance_release_checks.result }}" \ "qa_lab_parity_lane_release_checks=${{ needs.qa_lab_parity_lane_release_checks.result }}" \ "qa_lab_parity_report_release_checks=${{ needs.qa_lab_parity_report_release_checks.result }}" \ "qa_live_matrix_release_checks=${{ needs.qa_live_matrix_release_checks.result }}" \ "qa_live_telegram_release_checks=${{ needs.qa_live_telegram_release_checks.result }}" \ "qa_live_slack_release_checks=${{ needs.qa_live_slack_release_checks.result }}" do name="${item%%=*}" result="${item#*=}" if [[ "$result" != "success" && "$result" != "skipped" ]]; then echo "::error::${name} ended with ${result}" failed=1 fi done exit "$failed"