name: Mantis Telegram Live on: issue_comment: types: [created] workflow_dispatch: inputs: candidate_ref: description: Ref, tag, or SHA to verify with Telegram live QA required: true default: main type: string pr_number: description: Optional PR number to receive the QA evidence comment required: false type: string scenario: description: Optional comma-separated Telegram scenario ids required: false default: telegram-status-command type: string provider_mode: description: QA provider mode required: false default: live-frontier type: choice options: - live-frontier - mock-openai crabbox_provider: description: Crabbox provider for the desktop transcript capture required: false default: aws type: choice options: - aws - hetzner crabbox_lease_id: description: Optional existing Crabbox desktop/browser lease id or slug to reuse required: false type: string permissions: actions: read contents: read pull-requests: read env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" NODE_VERSION: "24.x" OPENCLAW_BUILD_PRIVATE_QA: "1" OPENCLAW_ENABLE_PRIVATE_QA_CLI: "1" CRABBOX_REF: main CRABBOX_AWS_REGION: us-east-1 CRABBOX_CAPACITY_REGIONS: us-east-1 jobs: authorize_actor: name: Authorize workflow actor if: >- ${{ github.event_name == 'workflow_dispatch' || ( github.event_name == 'issue_comment' && github.event.issue.pull_request && ( contains(github.event.comment.body, '@openclaw-mantis') || contains(github.event.comment.body, '/openclaw-mantis') ) ) }} runs-on: ubuntu-24.04 outputs: authorized: ${{ steps.permission.outputs.authorized }} steps: - name: Require maintainer-level repository access id: permission uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | const allowed = new Set(["admin", "maintain", "write"]); const { owner, repo } = context.repo; const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username: context.actor, }); const permission = data.permission; core.info(`Actor ${context.actor} permission: ${permission}`); if (!allowed.has(permission)) { core.notice( `Workflow requires write/maintain/admin access. Actor "${context.actor}" has "${permission}".`, ); core.setOutput("authorized", "false"); return; } core.setOutput("authorized", "true"); resolve_request: name: Resolve Mantis request needs: authorize_actor if: needs.authorize_actor.outputs.authorized == 'true' uses: ./.github/workflows/mantis-resolve-request.yml with: request-pattern: telegram exclude-pattern: desktop proof|desktop-proof|telegram desktop|native telegram|visible proof|visible-proof|telegram-visible-proof skip-notice: Comment mentioned Mantis but did not request Telegram live QA. dispatch-params: >- ${{ github.event_name == 'workflow_dispatch' && toJSON(inputs) || '{"scenario":"telegram-status-command","provider_mode":"live-frontier","crabbox_provider":"aws"}' }} runner: ubuntu-24.04 secrets: MANTIS_GITHUB_APP_ID: ${{ secrets.MANTIS_GITHUB_APP_ID }} MANTIS_GITHUB_APP_PRIVATE_KEY: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }} validate_request: name: Validate Telegram request needs: resolve_request if: ${{ needs.resolve_request.outputs.should_run == 'true' }} runs-on: ubuntu-24.04 permissions: {} steps: - name: Validate Telegram request parameters env: CRABBOX_PROVIDER: ${{ fromJSON(needs.resolve_request.outputs.params).crabbox_provider }} PROVIDER_MODE: ${{ fromJSON(needs.resolve_request.outputs.params).provider_mode }} shell: bash run: | set -euo pipefail case "$CRABBOX_PROVIDER" in aws | hetzner) ;; *) echo "Unsupported Crabbox provider for Mantis Telegram: ${CRABBOX_PROVIDER}" >&2 exit 1 ;; esac case "$PROVIDER_MODE" in live-frontier | mock-openai) ;; *) echo "Unsupported provider mode for Mantis Telegram: ${PROVIDER_MODE}" >&2 exit 1 ;; esac validate_ref: name: Validate candidate ref needs: resolve_request if: ${{ needs.resolve_request.outputs.should_run == 'true' }} runs-on: ubuntu-24.04 outputs: candidate_revision: ${{ steps.validate.outputs.candidate-revision }} steps: - name: Checkout harness ref uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false fetch-depth: 0 - name: Validate ref is trusted id: validate uses: ./.github/actions/mantis-validate-trusted-ref env: GH_TOKEN: ${{ github.token }} with: candidate-ref: ${{ needs.resolve_request.outputs.candidate_ref }} run_telegram_live: name: Run Telegram live QA with Crabbox evidence needs: [resolve_request, validate_request, validate_ref] if: ${{ needs.resolve_request.outputs.should_run == 'true' }} runs-on: ubuntu-24.04 timeout-minutes: 180 environment: qa-live-shared outputs: comparison_status: ${{ steps.run_mantis.outputs.comparison_status }} output_dir: ${{ steps.run_mantis.outputs.output_dir }} steps: - name: Wait for older Mantis Telegram account run env: GH_TOKEN: ${{ github.token }} shell: bash run: | set -euo pipefail current_created="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" --jq .created_at)" stale_before="$(date -u -d '8 hours ago' +%Y-%m-%dT%H:%M:%SZ)" run_has_active_jobs() { local run_id="$1" local run_state="$2" if [[ "$run_state" != "in_progress" ]]; then return 0 fi local active_jobs active_jobs="$(gh run view "$run_id" --repo "$GITHUB_REPOSITORY" --json jobs --jq '[.jobs[] | select(.status == "queued" or .status == "in_progress" or .status == "waiting" or .status == "pending" or .status == "requested")] | length')" [[ "$active_jobs" != "0" ]] } while true; do candidates="$( for workflow in mantis-telegram-desktop-proof.yml mantis-telegram-live.yml; do for status in queued in_progress waiting pending requested; do gh run list --repo "$GITHUB_REPOSITORY" --workflow "$workflow" --status "$status" --limit 100 --json databaseId,status,createdAt,url \ | jq -r \ --argjson current_id "$GITHUB_RUN_ID" \ --arg current_created "$current_created" \ --arg stale_before "$stale_before" \ '.[] | select(.databaseId != $current_id) | select(.createdAt >= $stale_before) | select(.createdAt < $current_created or (.createdAt == $current_created and .databaseId < $current_id)) | "\(.createdAt)\t#\(.databaseId)\t\(.status)\t\(.url)"' done done | sort -u )" blockers="" while IFS=$'\t' read -r created run_id run_state url; do if [[ -n "$run_id" ]] && run_has_active_jobs "${run_id#\#}" "$run_state"; then blockers+="${created}"$'\t'"${run_id}"$'\t'"${run_state}"$'\t'"${url}"$'\n' fi done <<<"$candidates" if [[ -z "$blockers" ]]; then break fi echo "Waiting for older Mantis Telegram account run:" printf '%s\n' "$blockers" | head -n 10 sleep 60 done - name: Checkout harness ref uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false fetch-depth: 0 - name: Setup Node environment uses: ./.github/actions/setup-node-env with: node-version: ${{ env.NODE_VERSION }} install-bun: "true" - name: Build Mantis harness run: pnpm build - name: Cache Mantis candidate pnpm store uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: | ~/.local/share/pnpm/store ~/.cache/pnpm key: mantis-telegram-pnpm-${{ runner.os }}-${{ env.NODE_VERSION }}-${{ hashFiles('pnpm-lock.yaml') }} restore-keys: | mantis-telegram-pnpm-${{ runner.os }}-${{ env.NODE_VERSION }}- - name: Setup Go for Crabbox CLI uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: "1.26.x" cache: false - name: Install Crabbox CLI shell: bash run: | set -euo pipefail install_dir="${RUNNER_TEMP}/crabbox" mkdir -p "$install_dir/src" "$HOME/.local/bin" git init "$install_dir/src" git -C "$install_dir/src" remote add origin https://github.com/openclaw/crabbox.git timeout --signal=TERM --kill-after=10s 120s git -C "$install_dir/src" fetch --depth 1 origin "$CRABBOX_REF" git -C "$install_dir/src" checkout --detach FETCH_HEAD go build -C "$install_dir/src" -o "$HOME/.local/bin/crabbox" ./cmd/crabbox echo "$HOME/.local/bin" >> "$GITHUB_PATH" "$HOME/.local/bin/crabbox" --version "$HOME/.local/bin/crabbox" warmup --help > "$install_dir/warmup-help.txt" 2>&1 grep -q -- "-desktop" "$install_dir/warmup-help.txt" "$HOME/.local/bin/crabbox" media preview --help >/dev/null - name: Prepare candidate worktree env: CANDIDATE_SHA: ${{ needs.validate_ref.outputs.candidate_revision }} shell: bash run: | set -euo pipefail worktree_root=".artifacts/qa-e2e/mantis/telegram-live-worktrees" mkdir -p "$worktree_root" git worktree add --detach "$worktree_root/candidate" "$CANDIDATE_SHA" pnpm --dir "$worktree_root/candidate" install --frozen-lockfile --prefer-offline pnpm --dir "$worktree_root/candidate" build - name: Run Telegram live scenario and capture desktop evidence id: run_mantis env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }} OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }} OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000" OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1" CRABBOX_COORDINATOR: ${{ secrets.CRABBOX_COORDINATOR }} CRABBOX_COORDINATOR_TOKEN: ${{ secrets.CRABBOX_COORDINATOR_TOKEN }} OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR: ${{ secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR }} OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN: ${{ secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN }} CRABBOX_ACCESS_CLIENT_ID: ${{ secrets.CRABBOX_ACCESS_CLIENT_ID }} CRABBOX_ACCESS_CLIENT_SECRET: ${{ secrets.CRABBOX_ACCESS_CLIENT_SECRET }} CRABBOX_AWS_REGION: ${{ env.CRABBOX_AWS_REGION }} CRABBOX_CAPACITY_REGIONS: ${{ env.CRABBOX_CAPACITY_REGIONS }} CRABBOX_LEASE_ID: ${{ fromJSON(needs.resolve_request.outputs.params).lease_id }} CRABBOX_PROVIDER: ${{ fromJSON(needs.resolve_request.outputs.params).crabbox_provider }} PROVIDER_MODE: ${{ fromJSON(needs.resolve_request.outputs.params).provider_mode }} SCENARIO_INPUT: ${{ fromJSON(needs.resolve_request.outputs.params).scenario }} CANDIDATE_SHA: ${{ needs.validate_ref.outputs.candidate_revision }} shell: bash run: | set -euo pipefail require_var() { local key="$1" if [[ -z "${!key:-}" ]]; then echo "Missing required ${key}." >&2 exit 1 fi } CRABBOX_COORDINATOR="${CRABBOX_COORDINATOR:-${OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR:-}}" CRABBOX_COORDINATOR_TOKEN="${CRABBOX_COORDINATOR_TOKEN:-${OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN:-}}" export CRABBOX_COORDINATOR CRABBOX_COORDINATOR_TOKEN require_var OPENCLAW_QA_CONVEX_SITE_URL require_var OPENCLAW_QA_CONVEX_SECRET_CI require_var CRABBOX_COORDINATOR_TOKEN candidate_repo="$(pwd)/.artifacts/qa-e2e/mantis/telegram-live-worktrees/candidate" output_rel=".artifacts/qa-e2e/mantis/telegram-live" root="$candidate_repo/$output_rel" echo "output_dir=${root}" >> "$GITHUB_OUTPUT" qa_args=( --repo-root "$candidate_repo" --output-dir "$output_rel" --provider-mode "$PROVIDER_MODE" ) case "$PROVIDER_MODE" in live-frontier) require_var OPENAI_API_KEY model="${OPENCLAW_CI_OPENAI_MODEL:-openai/gpt-5.6-luna}" qa_args+=(--model "$model" --alt-model "$model" --fast) ;; mock-openai) ;; *) echo "Unsupported provider mode for Mantis Telegram: ${PROVIDER_MODE}" >&2 exit 1 ;; esac qa_args+=( --credential-source convex --credential-role ci --allow-failures ) scenario_args=() if [[ -n "${SCENARIO_INPUT// }" ]]; then IFS=',' read -r -a raw_scenarios <<<"${SCENARIO_INPUT}" for raw in "${raw_scenarios[@]}"; do scenario="$(printf '%s' "${raw}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" if [[ -n "${scenario}" ]]; then scenario_args+=(--scenario "${scenario}") fi done fi set +e pnpm --dir "$candidate_repo" openclaw qa telegram "${qa_args[@]}" "${scenario_args[@]}" telegram_exit=$? set -e if [[ ! -f "$root/qa-evidence.json" ]]; then echo "Telegram live QA did not produce an evidence summary." >&2 exit "$telegram_exit" fi echo "telegram_exit=${telegram_exit}" >> "$GITHUB_OUTPUT" node --import tsx "${GITHUB_WORKSPACE}/scripts/mantis/build-telegram-evidence.mts" \ --output-dir "$root" \ --candidate-ref "$CANDIDATE_SHA" \ --candidate-sha "$CANDIDATE_SHA" \ --scenario-label "${SCENARIO_INPUT:-telegram-live}" comparison_status="$(jq -r 'if .comparison.pass then "pass" else "fail" end' "$root/mantis-evidence.json")" echo "comparison_status=${comparison_status}" >> "$GITHUB_OUTPUT" desktop_args=() if [[ -n "${CRABBOX_LEASE_ID:-}" ]]; then desktop_args+=(--lease-id "$CRABBOX_LEASE_ID") fi pnpm --dir "$candidate_repo" openclaw qa mantis desktop-browser-smoke \ --repo-root "$candidate_repo" \ --html-file "$output_rel/telegram-live-transcript.html" \ --output-dir "$output_rel/desktop-browser" \ --provider "$CRABBOX_PROVIDER" \ --class standard \ --idle-timeout 45m \ --ttl 120m \ --video-duration 18 \ "${desktop_args[@]}" cp "$root/desktop-browser/desktop-browser-smoke.png" "$root/telegram-live-desktop.png" if [[ -f "$root/desktop-browser/desktop-browser-smoke.mp4" ]]; then cp "$root/desktop-browser/desktop-browser-smoke.mp4" "$root/telegram-live.mp4" fi if [[ -f "$root/telegram-live.mp4" ]]; then if ! command -v ffmpeg >/dev/null 2>&1 || ! command -v ffprobe >/dev/null 2>&1; then sudo apt-get update -y >/tmp/mantis-telegram-ffmpeg-apt.log 2>&1 || true sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ffmpeg >>/tmp/mantis-telegram-ffmpeg-apt.log 2>&1 || true fi if ! crabbox media preview \ --input "$root/telegram-live.mp4" \ --output "$root/telegram-live-preview.gif" \ --trimmed-video-output "$root/telegram-live-change.mp4" \ --json > "$root/telegram-live-preview.json"; then rm -f "$root/telegram-live-preview.gif" rm -f "$root/telegram-live-change.mp4" rm -f "$root/telegram-live-preview.json" echo "::warning::Could not generate Telegram motion-trimmed desktop preview." fi fi cat "$root/qa-suite-report.md" >> "$GITHUB_STEP_SUMMARY" - name: Upload Mantis Telegram artifacts id: upload_artifact if: ${{ always() && steps.run_mantis.outputs.output_dir != '' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: mantis-telegram-live-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ steps.run_mantis.outputs.output_dir }} retention-days: 14 if-no-files-found: error - name: Create Mantis GitHub App token id: mantis_app_token if: ${{ always() && needs.resolve_request.outputs.pr_number != '' }} uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 with: app-id: ${{ secrets.MANTIS_GITHUB_APP_ID }} private-key: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }} owner: ${{ github.repository_owner }} repositories: ${{ github.event.repository.name }} permission-issues: write permission-pull-requests: write - name: Comment PR with inline QA evidence if: ${{ always() && needs.resolve_request.outputs.pr_number != '' && steps.run_mantis.outputs.output_dir != '' }} env: GH_TOKEN: ${{ steps.mantis_app_token.outputs.token }} ARTIFACT_URL: ${{ steps.upload_artifact.outputs.artifact-url }} MANTIS_ARTIFACT_R2_ACCESS_KEY_ID: ${{ secrets.MANTIS_ARTIFACT_R2_ACCESS_KEY_ID }} MANTIS_ARTIFACT_R2_BUCKET: openclaw-crabbox-artifacts MANTIS_ARTIFACT_R2_ENDPOINT: ${{ vars.MANTIS_ARTIFACT_R2_ENDPOINT }} MANTIS_ARTIFACT_R2_PUBLIC_BASE_URL: https://artifacts.openclaw.ai MANTIS_ARTIFACT_R2_REGION: auto MANTIS_ARTIFACT_R2_SECRET_ACCESS_KEY: ${{ secrets.MANTIS_ARTIFACT_R2_SECRET_ACCESS_KEY }} REQUEST_SOURCE: ${{ needs.resolve_request.outputs.request_source }} TARGET_PR: ${{ needs.resolve_request.outputs.pr_number }} shell: bash run: | set -euo pipefail root="${{ steps.run_mantis.outputs.output_dir }}" if [[ ! -f "$root/mantis-evidence.json" ]]; then echo "No Mantis evidence manifest found; skipping PR evidence comment." exit 0 fi artifact_url_args=() if [[ -n "${ARTIFACT_URL:-}" ]]; then artifact_url_args=(--artifact-url "$ARTIFACT_URL") fi node scripts/mantis/publish-pr-evidence.mjs \ --manifest "$root/mantis-evidence.json" \ --target-pr "$TARGET_PR" \ --artifact-root "mantis/telegram-live/pr-${TARGET_PR}/run-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \ --marker "" \ "${artifact_url_args[@]}" \ --run-url "https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ --request-source "$REQUEST_SOURCE" - name: Fail when Mantis Telegram failed if: ${{ always() && steps.run_mantis.outputs.output_dir != '' && (steps.run_mantis.outputs.comparison_status != 'pass' || steps.run_mantis.outputs.telegram_exit != '0') }} env: COMPARISON_STATUS: ${{ steps.run_mantis.outputs.comparison_status }} TELEGRAM_EXIT: ${{ steps.run_mantis.outputs.telegram_exit }} run: | echo "Mantis Telegram live failed: comparison=${COMPARISON_STATUS:-unset} telegram_exit=${TELEGRAM_EXIT:-unset}." >&2 exit 1 clear_issue_comment_reaction: name: Clear Mantis command reaction needs: [resolve_request, validate_ref, run_telegram_live] if: ${{ always() && github.event_name == 'issue_comment' && needs.resolve_request.outputs.request_source == 'issue_comment' && needs.resolve_request.outputs.reaction_id != '' }} permissions: {} uses: ./.github/workflows/mantis-clear-reaction.yml with: comment-id: ${{ format('{0}', github.event.comment.id) }} reaction-id: ${{ needs.resolve_request.outputs.reaction_id }} secrets: MANTIS_GITHUB_APP_ID: ${{ secrets.MANTIS_GITHUB_APP_ID }} MANTIS_GITHUB_APP_PRIVATE_KEY: ${{ secrets.MANTIS_GITHUB_APP_PRIVATE_KEY }}