name: OpenClaw Cross-OS Release Checks (Reusable) on: workflow_dispatch: inputs: ref: description: Public OpenClaw ref to validate (tag, branch, or full commit SHA) required: true default: main type: string workflow_ref: description: Optional openclaw/openclaw ref that provides the reusable workflow harness required: false default: "" type: string provider: description: Provider lane to use for onboarding and the end-to-end turn required: true default: openai type: choice options: - openai - anthropic - minimax mode: description: Which release-check lanes to run required: true default: both type: choice options: - fresh - upgrade - both previous_version: description: Optional baseline version for installer/dev-update and packaged upgrade required: false default: "" type: string ubuntu_runner: description: Optional Linux runner label override required: false default: "" type: string windows_runner: description: Optional Windows runner label override required: false default: "" type: string macos_runner: description: Optional macOS runner label override required: false default: "" type: string workflow_call: inputs: ref: description: Public OpenClaw ref to validate (tag, branch, or full commit SHA) required: true type: string workflow_ref: description: Optional openclaw/openclaw ref that provides the reusable workflow harness required: false default: "" type: string provider: description: Provider lane to use for onboarding and the end-to-end turn required: true type: string mode: description: Which release-check lanes to run required: true type: string previous_version: description: Optional baseline version for the upgrade lane (defaults to npm latest) required: false default: "" type: string ubuntu_runner: description: Optional Linux runner label override required: false default: "" type: string windows_runner: description: Optional Windows runner label override required: false default: "" type: string macos_runner: description: Optional macOS runner label override required: false default: "" type: string secrets: OPENAI_API_KEY: required: false ANTHROPIC_API_KEY: required: false MINIMAX_API_KEY: required: false OPENCLAW_DISCORD_SMOKE_BOT_TOKEN: required: false OPENCLAW_DISCORD_SMOKE_GUILD_ID: required: false OPENCLAW_DISCORD_SMOKE_CHANNEL_ID: required: false permissions: read-all concurrency: group: openclaw-cross-os-release-checks-${{ inputs.ref }}-${{ inputs.provider }}-${{ inputs.mode }} cancel-in-progress: false env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" NODE_VERSION: "24.x" PNPM_VERSION: "10.32.1" OPENCLAW_REPOSITORY: openclaw/openclaw TSX_VERSION: "4.21.0" jobs: prepare: runs-on: ubuntu-latest outputs: baseline_file_name: ${{ steps.baseline_metadata.outputs.file_name }} baseline_spec: ${{ steps.baseline.outputs.value }} candidate_file_name: ${{ steps.candidate_metadata.outputs.file_name }} candidate_version: ${{ steps.candidate_metadata.outputs.version }} matrix: ${{ steps.matrix.outputs.value }} source_sha: ${{ steps.candidate_metadata.outputs.source_sha }} workflow_ref: ${{ steps.workflow_ref.outputs.value }} steps: - name: Validate provider secret availability env: PROVIDER: ${{ inputs.provider }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }} run: | set -euo pipefail case "${PROVIDER}" in openai) [[ -n "${OPENAI_API_KEY}" ]] || { echo "Missing OPENAI_API_KEY secret." >&2; exit 1; } ;; anthropic) [[ -n "${ANTHROPIC_API_KEY}" ]] || { echo "Missing ANTHROPIC_API_KEY secret." >&2; exit 1; } ;; minimax) [[ -n "${MINIMAX_API_KEY}" ]] || { echo "Missing MINIMAX_API_KEY secret." >&2; exit 1; } ;; *) echo "Unsupported provider: ${PROVIDER}" >&2 exit 1 ;; esac - name: Resolve workflow ref id: workflow_ref env: INPUT_WORKFLOW_REF: ${{ inputs.workflow_ref }} CALLER_REPOSITORY: ${{ github.repository }} CURRENT_SHA: ${{ github.sha }} WORKFLOW_CONTEXT_REF: ${{ github.workflow_ref }} WORKFLOW_REPOSITORY: ${{ env.OPENCLAW_REPOSITORY }} run: | set -euo pipefail resolve_unique_remote_ref() { local remote_url="$1" shift local -a refs=("$@") local -a matches=() local ref="" for ref in "${refs[@]}"; do [[ -n "${ref}" ]] || continue mapfile -t matches < <( git ls-remote "${remote_url}" "${ref}" | awk '{print $1}' | awk '!seen[$0]++' ) if [[ "${#matches[@]}" -eq 0 ]]; then continue fi if [[ "${#matches[@]}" -ne 1 ]]; then return 2 fi printf '%s\n' "${matches[0]}" return 0 done return 1 } if [[ -n "${INPUT_WORKFLOW_REF}" ]]; then TARGET_REF="${INPUT_WORKFLOW_REF}" elif [[ "${CALLER_REPOSITORY}" == "${WORKFLOW_REPOSITORY}" ]]; then TARGET_REF="${CURRENT_SHA}" elif [[ "${WORKFLOW_CONTEXT_REF}" == "${WORKFLOW_REPOSITORY}/"* ]] && [[ "${WORKFLOW_CONTEXT_REF}" == *"@"* ]]; then TARGET_REF="${WORKFLOW_CONTEXT_REF##*@}" else echo "Failed to infer workflow ref from github.workflow_ref=${WORKFLOW_CONTEXT_REF}" >&2 exit 1 fi if [[ "${TARGET_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then echo "value=${TARGET_REF}" >> "$GITHUB_OUTPUT" exit 0 fi REMOTE_URL="https://github.com/${WORKFLOW_REPOSITORY}.git" if [[ "${TARGET_REF}" == refs/* ]]; then if [[ "${TARGET_REF}" == refs/tags/* ]]; then mapfile -t MATCHES < <( resolve_unique_remote_ref "${REMOTE_URL}" "${TARGET_REF}^{}" "${TARGET_REF}" || true ) else mapfile -t MATCHES < <(resolve_unique_remote_ref "${REMOTE_URL}" "${TARGET_REF}" || true) fi else mapfile -t BRANCH_MATCHES < <( resolve_unique_remote_ref "${REMOTE_URL}" "refs/heads/${TARGET_REF}" || true ) mapfile -t TAG_MATCHES < <( resolve_unique_remote_ref "${REMOTE_URL}" "refs/tags/${TARGET_REF}^{}" "refs/tags/${TARGET_REF}" || true ) MATCH_COUNT=$(( ${#BRANCH_MATCHES[@]} + ${#TAG_MATCHES[@]} )) if [[ "${MATCH_COUNT}" -eq 1 ]]; then if [[ "${#BRANCH_MATCHES[@]}" -eq 1 ]]; then MATCHES=("${BRANCH_MATCHES[0]}") else MATCHES=("${TAG_MATCHES[0]}") fi elif [[ "${MATCH_COUNT}" -eq 0 ]]; then MATCHES=() else echo "Workflow ref resolved ambiguously: ${TARGET_REF}" >&2 exit 1 fi fi case "${#MATCHES[@]}" in 1) echo "value=${MATCHES[0]}" >> "$GITHUB_OUTPUT" ;; 0) echo "Failed to resolve workflow ref: ${TARGET_REF}" >&2 exit 1 ;; *) echo "Workflow ref resolved ambiguously: ${TARGET_REF}" >&2 exit 1 ;; esac - name: Checkout workflow repo uses: actions/checkout@v6 with: repository: ${{ env.OPENCLAW_REPOSITORY }} ref: ${{ steps.workflow_ref.outputs.value }} path: workflow fetch-depth: 1 persist-credentials: false - name: Checkout public source ref uses: actions/checkout@v6 with: repository: ${{ env.OPENCLAW_REPOSITORY }} ref: ${{ inputs.ref }} path: source fetch-depth: 0 persist-credentials: false submodules: recursive - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: ${{ env.PNPM_VERSION }} run_install: false - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} cache: pnpm cache-dependency-path: source/pnpm-lock.yaml - name: Build candidate artifact once env: OUTPUT_DIR: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare run: | pnpm dlx "tsx@${TSX_VERSION}" workflow/scripts/openclaw-cross-os-release-checks.ts \ --prepare-only \ --source-dir source \ --output-dir "${OUTPUT_DIR}" - name: Resolve baseline package spec if: ${{ inputs.mode != 'fresh' }} id: baseline env: INPUT_PREVIOUS_VERSION: ${{ inputs.previous_version }} run: | set -euo pipefail if [[ -n "${INPUT_PREVIOUS_VERSION}" ]]; then echo "value=openclaw@${INPUT_PREVIOUS_VERSION}" >> "$GITHUB_OUTPUT" exit 0 fi BASELINE_VERSION="$(npm view openclaw@latest version)" echo "value=openclaw@${BASELINE_VERSION}" >> "$GITHUB_OUTPUT" - name: Pack baseline artifact if: ${{ inputs.mode != 'fresh' }} env: BASELINE_SPEC: ${{ steps.baseline.outputs.value }} OUTPUT_DIR: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/baseline run: | mkdir -p "${OUTPUT_DIR}" npm pack --ignore-scripts --json "${BASELINE_SPEC}" --pack-destination "${OUTPUT_DIR}" > "${OUTPUT_DIR}/pack.json" - name: Capture candidate metadata id: candidate_metadata env: CANDIDATE_JSON: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/candidate.json run: | node <<'NODE' >>"$GITHUB_OUTPUT" const fs = require("node:fs"); const payload = JSON.parse(fs.readFileSync(process.env.CANDIDATE_JSON, "utf8")); process.stdout.write(`file_name=${payload.candidateFileName}\n`); process.stdout.write(`version=${payload.candidateVersion}\n`); process.stdout.write(`source_sha=${payload.sourceSha}\n`); NODE - name: Capture baseline metadata if: ${{ inputs.mode != 'fresh' }} id: baseline_metadata env: BASELINE_PACK_JSON: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/baseline/pack.json run: | node <<'NODE' >>"$GITHUB_OUTPUT" const fs = require("node:fs"); const payload = JSON.parse(fs.readFileSync(process.env.BASELINE_PACK_JSON, "utf8")); const entry = Array.isArray(payload) ? payload.at(-1) : null; if (!entry?.filename) { throw new Error("Baseline npm pack did not produce a filename."); } process.stdout.write(`file_name=${entry.filename}\n`); NODE - name: Upload candidate artifact uses: actions/upload-artifact@v7 with: name: openclaw-cross-os-release-checks-candidate-${{ github.run_id }} path: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/package/${{ steps.candidate_metadata.outputs.file_name }} if-no-files-found: error - name: Upload baseline artifact if: ${{ inputs.mode != 'fresh' }} uses: actions/upload-artifact@v7 with: name: openclaw-cross-os-release-checks-baseline-${{ github.run_id }} path: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/baseline/${{ steps.baseline_metadata.outputs.file_name }} if-no-files-found: error - name: Resolve runner matrix id: matrix env: INPUT_REF: ${{ inputs.ref }} INPUT_MODE: ${{ inputs.mode }} INPUT_UBUNTU_RUNNER: ${{ inputs.ubuntu_runner }} INPUT_WINDOWS_RUNNER: ${{ inputs.windows_runner }} INPUT_MACOS_RUNNER: ${{ inputs.macos_runner }} VAR_UBUNTU_RUNNER: ${{ vars.OPENCLAW_RELEASE_CHECKS_UBUNTU_RUNNER }} VAR_WINDOWS_RUNNER: ${{ vars.OPENCLAW_RELEASE_CHECKS_WINDOWS_RUNNER }} VAR_MACOS_RUNNER: ${{ vars.OPENCLAW_RELEASE_CHECKS_MACOS_RUNNER }} run: | MATRIX_JSON="$(pnpm dlx "tsx@${TSX_VERSION}" workflow/scripts/openclaw-cross-os-release-checks.ts \ --resolve-matrix \ --ref "${INPUT_REF}" \ --mode "${INPUT_MODE}" \ --ubuntu-runner "${INPUT_UBUNTU_RUNNER}" \ --windows-runner "${INPUT_WINDOWS_RUNNER}" \ --macos-runner "${INPUT_MACOS_RUNNER}")" echo "value=${MATRIX_JSON}" >> "$GITHUB_OUTPUT" cross_os_release_checks: name: "${{ matrix.display_name }} / ${{ matrix.suite_label }}" needs: prepare strategy: fail-fast: false matrix: ${{ fromJson(needs.prepare.outputs.matrix) }} runs-on: ${{ matrix.runner }} timeout-minutes: 120 steps: - name: Checkout workflow repo uses: actions/checkout@v6 with: repository: ${{ env.OPENCLAW_REPOSITORY }} ref: ${{ needs.prepare.outputs.workflow_ref }} path: workflow fetch-depth: 1 persist-credentials: false - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: ${{ env.PNPM_VERSION }} run_install: false - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ env.NODE_VERSION }} - name: Download candidate artifact uses: actions/download-artifact@v8 with: name: openclaw-cross-os-release-checks-candidate-${{ github.run_id }} path: ${{ runner.temp }}/openclaw-cross-os-release-checks/candidate - name: Download baseline artifact if: ${{ matrix.suite == 'packaged-upgrade' }} uses: actions/download-artifact@v8 with: name: openclaw-cross-os-release-checks-baseline-${{ github.run_id }} path: ${{ runner.temp }}/openclaw-cross-os-release-checks/baseline - name: Run cross-OS release checks shell: bash env: 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 }} OPENCLAW_RELEASE_CHECK_OS: ${{ matrix.os_id }} OPENCLAW_RELEASE_CHECK_RUNNER: ${{ matrix.runner }} run: | DISCORD_ARGS=() if [[ -n "${OPENCLAW_DISCORD_SMOKE_BOT_TOKEN}" ]] && [[ -n "${OPENCLAW_DISCORD_SMOKE_GUILD_ID}" ]] && [[ -n "${OPENCLAW_DISCORD_SMOKE_CHANNEL_ID}" ]]; then DISCORD_ARGS+=(--run-discord-roundtrip true) fi pnpm dlx "tsx@${TSX_VERSION}" workflow/scripts/openclaw-cross-os-release-checks.ts \ --candidate-tgz "$RUNNER_TEMP/openclaw-cross-os-release-checks/candidate/${{ needs.prepare.outputs.candidate_file_name }}" \ --candidate-version "${{ needs.prepare.outputs.candidate_version }}" \ --source-sha "${{ needs.prepare.outputs.source_sha }}" \ --baseline-spec "${{ needs.prepare.outputs.baseline_spec }}" \ --previous-version "${{ inputs.previous_version }}" \ --baseline-tgz "$RUNNER_TEMP/openclaw-cross-os-release-checks/baseline/${{ needs.prepare.outputs.baseline_file_name }}" \ --provider "${{ inputs.provider }}" \ --mode "${{ matrix.lane }}" \ --suite "${{ matrix.suite }}" \ --ref "${{ inputs.ref }}" \ "${DISCORD_ARGS[@]}" \ --output-dir "$RUNNER_TEMP/openclaw-cross-os-release-checks/${{ matrix.artifact_name }}-${{ matrix.suite }}" - name: Summarize release checks if: always() shell: bash env: SUMMARY_PATH: ${{ runner.temp }}/openclaw-cross-os-release-checks/${{ matrix.artifact_name }}-${{ matrix.suite }}/summary.md run: | if [[ -f "${SUMMARY_PATH}" ]]; then cat "${SUMMARY_PATH}" >> "$GITHUB_STEP_SUMMARY" else echo "No summary generated." >> "$GITHUB_STEP_SUMMARY" fi - name: Upload release-check artifacts if: always() uses: actions/upload-artifact@v7 with: name: openclaw-cross-os-release-checks-${{ matrix.artifact_name }}-${{ matrix.suite }}-${{ github.run_id }} path: ${{ runner.temp }}/openclaw-cross-os-release-checks/${{ matrix.artifact_name }}-${{ matrix.suite }} if-no-files-found: error