name: Mantis Telegram Desktop Proof on: issue_comment: types: [created] pull_request_target: # zizmor: ignore[dangerous-triggers] maintainer-owned Mantis label trigger; trusted base workflow validates refs before checkout/use types: [labeled] workflow_dispatch: inputs: pr_number: description: PR number to capture required: true type: string instructions: description: Optional freeform proof instructions for the agent required: false type: string crabbox_provider: description: Crabbox provider for the native Telegram Desktop capture required: false default: aws type: choice options: - aws - hetzner crabbox_lease_id: description: Optional existing Crabbox desktop lease id or slug to reuse required: false type: string publish_artifact_name: description: Optional existing proof artifact name to publish without recapturing required: false type: string publish_run_id: description: Workflow run id that owns publish_artifact_name; required with publish_artifact_name required: false type: string permissions: actions: read contents: read issues: write pull-requests: write 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 MANTIS_OUTPUT_DIR: .artifacts/qa-e2e/mantis/telegram-desktop-proof jobs: authorize_actor: name: Authorize workflow actor if: >- ${{ github.event_name == 'workflow_dispatch' || ( github.event_name == 'pull_request_target' && github.event.action == 'labeled' && github.event.label.name == 'mantis: telegram-visible-proof' ) || ( github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.issue.labels.*.name, 'mantis: telegram-visible-proof') && ( 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: | if (context.eventName === "pull_request_target") { core.info(`Accepted Mantis label trigger from ${context.actor}.`); core.setOutput("authorized", "true"); return; } 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' runs-on: ubuntu-24.04 outputs: baseline_ref: ${{ steps.resolve.outputs.baseline_ref }} candidate_ref: ${{ steps.resolve.outputs.candidate_ref }} crabbox_provider: ${{ steps.resolve.outputs.crabbox_provider }} instructions: ${{ steps.resolve.outputs.instructions }} lease_id: ${{ steps.resolve.outputs.lease_id }} publish_artifact_name: ${{ steps.resolve.outputs.publish_artifact_name }} publish_run_id: ${{ steps.resolve.outputs.publish_run_id }} pr_number: ${{ steps.resolve.outputs.pr_number }} request_source: ${{ steps.resolve.outputs.request_source }} should_run: ${{ steps.resolve.outputs.should_run }} steps: - name: Resolve refs and target PR id: resolve uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | const eventName = context.eventName; function setOutput(name, value) { core.setOutput(name, value ?? ""); core.info(`${name}=${value ?? ""}`); } const inputs = context.payload.inputs ?? {}; const prNumber = eventName === "workflow_dispatch" ? inputs.pr_number : eventName === "pull_request_target" ? String(context.payload.pull_request?.number ?? "") : String(context.payload.issue?.number ?? ""); if (!prNumber) { core.setFailed("Mantis Telegram desktop proof requires a pull request."); return; } const body = eventName === "workflow_dispatch" ? inputs.instructions || "" : eventName === "issue_comment" ? context.payload.comment?.body || "" : ""; if (eventName === "issue_comment") { const normalized = body.toLowerCase(); const requestedDesktopProof = (normalized.includes("@openclaw-mantis") || normalized.includes("/openclaw-mantis")) && (normalized.includes("desktop proof") || normalized.includes("desktop-proof") || normalized.includes("telegram desktop") || normalized.includes("native telegram") || normalized.includes("visible proof") || normalized.includes("visible-proof") || normalized.includes("telegram-visible-proof")); if (!requestedDesktopProof) { core.notice("Comment mentioned Mantis but did not request Telegram desktop proof."); setOutput("should_run", "false"); setOutput("baseline_ref", ""); setOutput("candidate_ref", ""); setOutput("pr_number", ""); setOutput("instructions", ""); setOutput("crabbox_provider", ""); setOutput("lease_id", ""); setOutput("publish_artifact_name", ""); setOutput("publish_run_id", ""); setOutput("request_source", "unsupported_issue_comment"); return; } } const { owner, repo } = context.repo; const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: Number(prNumber), }); const provider = inputs.crabbox_provider || "aws"; if (!["aws", "hetzner"].includes(provider)) { core.setFailed(`Unsupported Crabbox provider for Mantis Telegram desktop proof: ${provider}`); return; } setOutput("should_run", "true"); setOutput("baseline_ref", pr.base.sha); setOutput("candidate_ref", pr.head.sha); setOutput("pr_number", String(pr.number)); setOutput("instructions", body); setOutput("crabbox_provider", provider); setOutput("lease_id", inputs.crabbox_lease_id || ""); setOutput("publish_artifact_name", inputs.publish_artifact_name || ""); setOutput("publish_run_id", inputs.publish_run_id || ""); setOutput("request_source", eventName); if (eventName === "issue_comment") { await github.rest.reactions.createForIssueComment({ owner, repo, comment_id: context.payload.comment.id, content: "eyes", }).catch((error) => core.warning(`Could not add eyes reaction: ${error.message}`)); } validate_refs: name: Validate selected refs needs: resolve_request if: needs.resolve_request.outputs.should_run == 'true' && needs.resolve_request.outputs.publish_artifact_name == '' runs-on: ubuntu-24.04 outputs: baseline_revision: ${{ steps.validate.outputs.baseline_revision }} candidate_revision: ${{ steps.validate.outputs.candidate_revision }} candidate_trust: ${{ steps.validate.outputs.candidate_trust }} steps: - name: Checkout harness ref uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: ref: main persist-credentials: false fetch-depth: 0 - name: Validate refs are trusted id: validate env: BASELINE_REF: ${{ needs.resolve_request.outputs.baseline_ref }} CANDIDATE_REF: ${{ needs.resolve_request.outputs.candidate_ref }} GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ needs.resolve_request.outputs.pr_number }} shell: bash run: | set -euo pipefail git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main resolve_commit() { local input_ref="$2" local revision="" if ! revision="$(git rev-parse --verify "${input_ref}^{commit}" 2>/dev/null)"; then echo "$1 ref '${input_ref}' is not available in the workflow checkout." >&2 exit 1 fi printf '%s\n' "$revision" } baseline_revision="$(resolve_commit baseline "$BASELINE_REF")" if ! git merge-base --is-ancestor "$baseline_revision" refs/remotes/origin/main; then echo "baseline ref '${BASELINE_REF}' resolved to ${baseline_revision}, which is not on main." >&2 exit 1 fi pr_head="$( gh api \ -H "Accept: application/vnd.github+json" \ "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \ --jq '{state, head_sha: .head.sha, head_repo: .head.repo.full_name}' )" pr_state="$(jq -r '.state' <<<"$pr_head")" pr_head_sha="$(jq -r '.head_sha' <<<"$pr_head")" pr_head_repo="$(jq -r '.head_repo' <<<"$pr_head")" candidate_revision="$CANDIDATE_REF" if [[ ! "$candidate_revision" =~ ^[0-9a-f]{40}$ ]]; then echo "candidate ref '${CANDIDATE_REF}' is not an immutable commit SHA." >&2 exit 1 fi if [[ "$pr_state" != "open" || "$candidate_revision" != "$pr_head_sha" ]]; then echo "candidate ref '${CANDIDATE_REF}' resolved to ${candidate_revision}, which is not the open PR head." >&2 exit 1 fi candidate_trust="open-pr-head" if [[ "$pr_head_repo" != "$GITHUB_REPOSITORY" ]]; then candidate_trust="fork-pr-head" fi echo "baseline_revision=${baseline_revision}" >> "$GITHUB_OUTPUT" echo "candidate_revision=${candidate_revision}" >> "$GITHUB_OUTPUT" echo "candidate_trust=${candidate_trust}" >> "$GITHUB_OUTPUT" { echo "baseline: \`${BASELINE_REF}\`" echo "baseline SHA: \`${baseline_revision}\`" echo "baseline trust: \`main-ancestor\`" echo "candidate: \`${CANDIDATE_REF}\`" echo "candidate SHA: \`${candidate_revision}\`" echo "candidate trust: \`${candidate_trust}\`" } >> "$GITHUB_STEP_SUMMARY" run_telegram_desktop_proof: name: Run agentic native Telegram proof needs: [resolve_request, validate_refs] if: needs.resolve_request.outputs.should_run == 'true' && needs.resolve_request.outputs.publish_artifact_name == '' runs-on: blacksmith-16vcpu-ubuntu-2404 timeout-minutes: 360 environment: qa-live-shared outputs: comparison_status: ${{ steps.inspect.outputs.comparison_status }} output_dir: ${{ steps.inspect.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 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: Setup Go for Crabbox CLI uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 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" git init "$install_dir/src" git -C "$install_dir/src" remote add origin https://github.com/openclaw/crabbox.git 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 "$install_dir/crabbox" ./cmd/crabbox sudo install -m 0755 "$install_dir/crabbox" /usr/local/bin/crabbox crabbox --version crabbox media preview --help >/dev/null - name: Install local proof tools shell: bash run: | set -euo pipefail test -f scripts/e2e/telegram-user-driver.py cat >"${RUNNER_TEMP}/openclaw-telegram-user-crabbox-proof" <<'EOF' #!/usr/bin/env bash set -euo pipefail exec node --import tsx "${GITHUB_WORKSPACE}/scripts/e2e/telegram-user-crabbox-proof.ts" "$@" EOF chmod 0755 "${RUNNER_TEMP}/openclaw-telegram-user-crabbox-proof" sudo install -m 0755 "${RUNNER_TEMP}/openclaw-telegram-user-crabbox-proof" /usr/local/bin/openclaw-telegram-user-crabbox-proof /usr/local/bin/openclaw-telegram-user-crabbox-proof --help >/dev/null media_tools="${RUNNER_TEMP}/mantis-media-tools" install -d "$media_tools" curl --fail --location --retry 3 --retry-delay 2 \ --connect-timeout 15 --max-time 180 \ https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz \ --output "$media_tools/ffmpeg.tar.xz" tar -xJf "$media_tools/ffmpeg.tar.xz" -C "$media_tools" bin_dir="$(find "$media_tools" -type d -path '*/bin' | head -n 1)" sudo install -m 0755 "$bin_dir/ffmpeg" /usr/local/bin/ffmpeg sudo install -m 0755 "$bin_dir/ffprobe" /usr/local/bin/ffprobe ffmpeg -version >/dev/null ffprobe -version >/dev/null - name: Ensure agent key exists env: OPENAI_API_KEY: ${{ secrets.OPENCLAW_MANTIS_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }} run: | set -euo pipefail if [ -z "${OPENAI_API_KEY:-}" ]; then echo "Missing OPENCLAW_MANTIS_AGENT_OPENAI_API_KEY or OPENAI_API_KEY secret." >&2 exit 1 fi - name: Prepare Codex user shell: bash run: | set -euo pipefail sudo useradd --create-home --shell /bin/bash codex { printf '%s\n' 'Defaults env_keep += "CODEX_HOME CODEX_INTERNAL_ORIGINATOR_OVERRIDE"' printf '%s\n' 'Defaults env_keep += "BASELINE_REF BASELINE_SHA CANDIDATE_REF CANDIDATE_SHA"' printf '%s\n' 'Defaults env_keep += "CRABBOX_ACCESS_CLIENT_ID CRABBOX_ACCESS_CLIENT_SECRET CRABBOX_COORDINATOR CRABBOX_COORDINATOR_TOKEN CRABBOX_AWS_REGION CRABBOX_CAPACITY_REGIONS CRABBOX_LEASE_ID CRABBOX_PROVIDER"' printf '%s\n' 'Defaults env_keep += "GH_TOKEN MANTIS_CANDIDATE_TRUST MANTIS_INSTRUCTIONS MANTIS_OUTPUT_DIR MANTIS_PR_NUMBER"' printf '%s\n' 'Defaults env_keep += "OPENCLAW_BUILD_PRIVATE_QA OPENCLAW_ENABLE_PRIVATE_QA_CLI OPENCLAW_QA_CONVEX_SECRET_CI OPENCLAW_QA_CONVEX_SITE_URL OPENCLAW_QA_CREDENTIAL_OWNER_ID OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN"' printf '%s\n' 'Defaults env_keep += "OPENCLAW_TELEGRAM_USER_CRABBOX_BIN OPENCLAW_TELEGRAM_USER_CRABBOX_PROVIDER OPENCLAW_TELEGRAM_USER_DRIVER_SCRIPT OPENCLAW_TELEGRAM_USER_PROOF_CMD"' } | sudo tee /etc/sudoers.d/mantis-codex-env >/dev/null sudo chmod 0440 /etc/sudoers.d/mantis-codex-env codex_home="/tmp/mantis-codex-home-${GITHUB_RUN_ID}" sudo install -d -m 0770 -o codex -g codex "$codex_home" sudo setfacl -m u:runner:rwx,u:codex:rwx "$codex_home" sudo setfacl -d -m u:runner:rwx,u:codex:rwx "$codex_home" workspace_parent="$(dirname "$GITHUB_WORKSPACE")" while [ "$workspace_parent" != "/" ]; do sudo setfacl -m u:codex:--x "$workspace_parent" [ "$workspace_parent" = "/home/runner" ] && break workspace_parent="$(dirname "$workspace_parent")" done sudo chown -R codex:codex "$GITHUB_WORKSPACE" - name: Run Codex Mantis Telegram agent uses: openai/codex-action@e0fdf01220eb9a88167c4898839d273e3f2609d1 env: BASELINE_REF: ${{ needs.resolve_request.outputs.baseline_ref }} BASELINE_SHA: ${{ needs.validate_refs.outputs.baseline_revision }} CANDIDATE_REF: ${{ needs.resolve_request.outputs.candidate_ref }} CANDIDATE_SHA: ${{ needs.validate_refs.outputs.candidate_revision }} CRABBOX_ACCESS_CLIENT_ID: ${{ secrets.CRABBOX_ACCESS_CLIENT_ID }} CRABBOX_ACCESS_CLIENT_SECRET: ${{ secrets.CRABBOX_ACCESS_CLIENT_SECRET }} CRABBOX_COORDINATOR: ${{ secrets.CRABBOX_COORDINATOR || secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR }} CRABBOX_COORDINATOR_TOKEN: ${{ secrets.CRABBOX_COORDINATOR_TOKEN || secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN }} CRABBOX_AWS_REGION: ${{ env.CRABBOX_AWS_REGION }} CRABBOX_CAPACITY_REGIONS: ${{ env.CRABBOX_CAPACITY_REGIONS }} CRABBOX_LEASE_ID: ${{ needs.resolve_request.outputs.lease_id }} CRABBOX_PROVIDER: ${{ needs.resolve_request.outputs.crabbox_provider }} GH_TOKEN: ${{ github.token }} MANTIS_CANDIDATE_TRUST: ${{ needs.validate_refs.outputs.candidate_trust }} MANTIS_INSTRUCTIONS: ${{ needs.resolve_request.outputs.instructions }} MANTIS_OUTPUT_DIR: ${{ env.MANTIS_OUTPUT_DIR }} MANTIS_PR_NUMBER: ${{ needs.resolve_request.outputs.pr_number }} OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }} OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }} OPENCLAW_QA_CREDENTIAL_OWNER_ID: mantis-telegram-desktop-${{ github.run_id }}-${{ github.run_attempt }} OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR: ${{ secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR }} OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN: ${{ secrets.OPENCLAW_QA_MANTIS_CRABBOX_COORDINATOR_TOKEN }} OPENCLAW_TELEGRAM_USER_CRABBOX_BIN: /usr/local/bin/crabbox OPENCLAW_TELEGRAM_USER_CRABBOX_PROVIDER: ${{ needs.resolve_request.outputs.crabbox_provider }} OPENCLAW_TELEGRAM_USER_DRIVER_SCRIPT: ${{ github.workspace }}/scripts/e2e/telegram-user-driver.py OPENCLAW_TELEGRAM_USER_PROOF_CMD: /usr/local/bin/openclaw-telegram-user-crabbox-proof with: openai-api-key: ${{ secrets.OPENCLAW_MANTIS_AGENT_OPENAI_API_KEY || secrets.OPENAI_API_KEY }} prompt-file: .github/codex/prompts/mantis-telegram-desktop-proof.md model: ${{ vars.OPENCLAW_CI_OPENAI_MODEL_BARE }} effort: medium sandbox: danger-full-access codex-args: '["-c","service_tier=\"fast\""]' codex-home: /tmp/mantis-codex-home-${{ github.run_id }} safety-strategy: unprivileged-user codex-user: codex allow-bot-users: clawsweeper[bot] - name: Release leaked Telegram proof leases if: ${{ always() }} env: CRABBOX_PROVIDER: ${{ needs.resolve_request.outputs.crabbox_provider }} OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }} OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }} shell: bash run: | set -euo pipefail if [[ ! -d .artifacts/qa-e2e ]]; then exit 0 fi status=0 mapfile -d '' session_files < <(sudo find .artifacts/qa-e2e -name session.json -type f -print0) for session_file in "${session_files[@]}"; do if ! sudo -u codex node -e 'const fs = require("fs"); const session = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.exit(session.command === "telegram-user-crabbox-session" ? 0 : 1);' "$session_file"; then continue fi lease_file="${session_file%/session.json}/.session/lease.json" if [[ ! -f "$lease_file" ]]; then continue fi if ! sudo -u codex env \ OPENCLAW_QA_CONVEX_SECRET_CI="$OPENCLAW_QA_CONVEX_SECRET_CI" \ OPENCLAW_QA_CONVEX_SITE_URL="$OPENCLAW_QA_CONVEX_SITE_URL" \ OPENCLAW_TELEGRAM_USER_CRABBOX_BIN=/usr/local/bin/crabbox \ OPENCLAW_TELEGRAM_USER_CRABBOX_PROVIDER="$CRABBOX_PROVIDER" \ node --import tsx "$GITHUB_WORKSPACE/scripts/e2e/telegram-user-crabbox-proof.ts" \ finish --session "$session_file" --preview-crop telegram-window; then status=1 fi done mapfile -d '' lease_files < <(sudo find .artifacts/qa-e2e -path '*/.session/lease.json' -type f -print0) for lease_file in "${lease_files[@]}"; do if ! sudo -u codex node -e 'const fs = require("fs"); const lease = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.exit(lease.kind === "telegram-user" ? 0 : 1);' "$lease_file"; then continue fi if ! sudo -u codex env \ OPENCLAW_QA_CONVEX_SECRET_CI="$OPENCLAW_QA_CONVEX_SECRET_CI" \ OPENCLAW_QA_CONVEX_SITE_URL="$OPENCLAW_QA_CONVEX_SITE_URL" \ node --import tsx "$GITHUB_WORKSPACE/scripts/e2e/telegram-user-credential.ts" \ release --lease-file "$lease_file"; then status=1 fi done exit "$status" - name: Inspect Mantis evidence manifest id: inspect if: ${{ always() }} shell: bash run: | set -euo pipefail output_dir="$MANTIS_OUTPUT_DIR" echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT" manifest="$output_dir/mantis-evidence.json" if [[ ! -f "$manifest" ]]; then echo "Mantis agent did not produce ${manifest}." >&2 exit 1 fi comparison_status="$(jq -r 'if .comparison.pass then "pass" else "fail" end' "$manifest")" echo "comparison_status=${comparison_status}" >> "$GITHUB_OUTPUT" - name: Upload Mantis Telegram desktop artifacts id: upload_artifact if: ${{ always() && steps.inspect.outputs.output_dir != '' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: mantis-telegram-desktop-proof-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ steps.inspect.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.inspect.outputs.output_dir != '' }} env: ARTIFACT_URL: ${{ steps.upload_artifact.outputs.artifact-url }} GH_TOKEN: ${{ steps.mantis_app_token.outputs.token }} 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.inspect.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-desktop/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 desktop proof failed if: ${{ always() && steps.inspect.outputs.output_dir != '' && steps.inspect.outputs.comparison_status != 'pass' }} env: COMPARISON_STATUS: ${{ steps.inspect.outputs.comparison_status }} run: | echo "Mantis Telegram desktop proof failed: comparison=${COMPARISON_STATUS:-unset}." >&2 exit 1 publish_existing_telegram_desktop_proof: name: Publish existing native Telegram proof needs: resolve_request if: needs.resolve_request.outputs.should_run == 'true' && needs.resolve_request.outputs.publish_artifact_name != '' runs-on: ubuntu-24.04 environment: qa-live-shared steps: - name: Checkout harness ref uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: persist-credentials: false - name: Setup Node environment uses: ./.github/actions/setup-node-env with: node-version: ${{ env.NODE_VERSION }} install-bun: "true" - name: Download existing proof artifact env: GH_TOKEN: ${{ github.token }} PUBLISH_ARTIFACT_NAME: ${{ needs.resolve_request.outputs.publish_artifact_name }} PUBLISH_RUN_ID: ${{ needs.resolve_request.outputs.publish_run_id }} shell: bash run: | set -euo pipefail if [[ -z "${PUBLISH_RUN_ID:-}" ]]; then echo "publish_run_id is required when publish_artifact_name is set." >&2 exit 1 fi run_id="$PUBLISH_RUN_ID" gh run download "$run_id" \ --repo "$GITHUB_REPOSITORY" \ --name "$PUBLISH_ARTIFACT_NAME" \ --dir "$MANTIS_OUTPUT_DIR" artifacts_json="$( gh api \ -H "Accept: application/vnd.github+json" \ "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/artifacts" )" artifact_id="$(jq -r --arg name "$PUBLISH_ARTIFACT_NAME" '.artifacts[] | select(.name == $name) | .id' <<<"$artifacts_json" | head -n 1)" if [[ -z "$artifact_id" || "$artifact_id" == "null" ]]; then echo "Could not resolve artifact id for '${PUBLISH_ARTIFACT_NAME}' in run ${run_id}." >&2 exit 1 fi echo "PUBLISH_RUN_ID=${run_id}" >> "$GITHUB_ENV" echo "PUBLISH_ARTIFACT_URL=https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}/artifacts/${artifact_id}" >> "$GITHUB_ENV" - name: Create Mantis GitHub App token id: mantis_app_token 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 env: GH_TOKEN: ${{ steps.mantis_app_token.outputs.token }} 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="$MANTIS_OUTPUT_DIR" if [[ ! -f "$root/mantis-evidence.json" ]]; then echo "Downloaded artifact does not contain ${root}/mantis-evidence.json." >&2 exit 1 fi node scripts/mantis/publish-pr-evidence.mjs \ --manifest "$root/mantis-evidence.json" \ --target-pr "$TARGET_PR" \ --artifact-root "mantis/telegram-desktop/pr-${TARGET_PR}/published-${PUBLISH_RUN_ID}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \ --marker "" \ --artifact-url "$PUBLISH_ARTIFACT_URL" \ --run-url "https://github.com/${GITHUB_REPOSITORY}/actions/runs/${PUBLISH_RUN_ID}" \ --request-source "$REQUEST_SOURCE" clear_issue_comment_reaction: name: Clear Mantis command reaction needs: [resolve_request, validate_refs, run_telegram_desktop_proof] if: ${{ always() && github.event_name == 'issue_comment' && needs.resolve_request.outputs.request_source == 'issue_comment' }} runs-on: ubuntu-24.04 permissions: issues: write steps: - name: Remove workflow eyes reaction uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | const { owner, repo } = context.repo; const commentId = context.payload.comment?.id; if (!commentId) { core.info("No issue comment id found; skipping reaction cleanup."); return; } const reactions = await github.paginate(github.rest.reactions.listForIssueComment, { owner, repo, comment_id: commentId, per_page: 100, }); const eyes = reactions.filter( (reaction) => reaction.content === "eyes" && reaction.user?.login === "github-actions[bot]", ); for (const reaction of eyes) { await github.rest.reactions.deleteForIssueComment({ owner, repo, comment_id: commentId, reaction_id: reaction.id, }); core.info(`Removed eyes reaction ${reaction.id} from comment ${commentId}.`); } if (eyes.length === 0) { core.info(`No workflow eyes reaction found on comment ${commentId}.`); }