diff --git a/.github/workflows/mantis-telegram-desktop-proof.yml b/.github/workflows/mantis-telegram-desktop-proof.yml index 4d93e3322f0..7e148ae1710 100644 --- a/.github/workflows/mantis-telegram-desktop-proof.yml +++ b/.github/workflows/mantis-telegram-desktop-proof.yml @@ -3,6 +3,8 @@ name: Mantis Telegram Desktop Proof on: issue_comment: types: [created] + pull_request_target: + types: [labeled] workflow_dispatch: inputs: pr_number: @@ -25,6 +27,14 @@ on: 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 @@ -47,6 +57,11 @@ jobs: 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 && @@ -66,6 +81,12 @@ jobs: uses: actions/github-script@v8 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({ @@ -95,6 +116,8 @@ jobs: 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 }} steps: @@ -112,7 +135,11 @@ jobs: const inputs = context.payload.inputs ?? {}; const prNumber = - eventName === "workflow_dispatch" ? inputs.pr_number : String(context.payload.issue?.number ?? ""); + 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; @@ -124,7 +151,12 @@ jobs: repo, pull_number: Number(prNumber), }); - const body = eventName === "workflow_dispatch" ? inputs.instructions || "" : context.payload.comment?.body || ""; + const body = + eventName === "workflow_dispatch" + ? inputs.instructions || "" + : eventName === "issue_comment" + ? context.payload.comment?.body || "" + : ""; const provider = inputs.crabbox_provider || "aws"; if (!["aws", "hetzner"].includes(provider)) { core.setFailed(`Unsupported Crabbox provider for Mantis Telegram desktop proof: ${provider}`); @@ -137,6 +169,8 @@ jobs: 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") { @@ -151,6 +185,7 @@ jobs: validate_refs: name: Validate selected refs needs: resolve_request + if: needs.resolve_request.outputs.publish_artifact_name == '' runs-on: ubuntu-24.04 outputs: baseline_revision: ${{ steps.validate.outputs.baseline_revision }} @@ -229,6 +264,7 @@ jobs: run_telegram_desktop_proof: name: Run agentic native Telegram proof needs: [resolve_request, validate_refs] + if: needs.resolve_request.outputs.publish_artifact_name == '' runs-on: blacksmith-16vcpu-ubuntu-2404 timeout-minutes: 360 environment: qa-live-shared @@ -473,3 +509,92 @@ jobs: 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.publish_artifact_name != '' + runs-on: ubuntu-24.04 + environment: qa-live-shared + steps: + - name: Checkout harness ref + uses: actions/checkout@v6 + with: + persist-credentials: false + + - 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 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@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" diff --git a/test/scripts/mantis-telegram-desktop-proof-workflow.test.ts b/test/scripts/mantis-telegram-desktop-proof-workflow.test.ts index 3182dd09cc7..1567143ec06 100644 --- a/test/scripts/mantis-telegram-desktop-proof-workflow.test.ts +++ b/test/scripts/mantis-telegram-desktop-proof-workflow.test.ts @@ -14,9 +14,11 @@ type WorkflowStep = { name?: string; run?: string; uses?: string; + with?: Record; }; type WorkflowJob = { + if?: string; steps?: WorkflowStep[]; }; @@ -24,6 +26,20 @@ type Workflow = { concurrency?: unknown; env?: Record; jobs?: Record; + on?: { + pull_request_target?: { + types?: string[]; + }; + workflow_dispatch?: { + inputs?: Record< + string, + { + required?: boolean; + type?: string; + } + >; + }; + }; permissions?: Record; }; @@ -102,6 +118,39 @@ describe("Mantis Telegram Desktop proof workflow", () => { expect(workflow).not.toContain('"/mantis"'); }); + it("runs when ClawSweeper applies the Telegram proof label", () => { + const workflow = parse(readFileSync(WORKFLOW, "utf8")) as Workflow; + const workflowText = readFileSync(WORKFLOW, "utf8"); + + expect(workflow.on?.pull_request_target?.types).toContain("labeled"); + expect(workflowText).toContain("github.event.label.name == 'mantis: telegram-visible-proof'"); + expect(workflowText).toContain('eventName === "pull_request_target"'); + expect(workflowText).toContain("context.payload.pull_request?.number"); + expect(workflowText).toContain("Accepted Mantis label trigger"); + }); + + it("can publish an existing proof artifact without recapturing", () => { + const workflow = parse(readFileSync(WORKFLOW, "utf8")) as Workflow; + const workflowText = readFileSync(WORKFLOW, "utf8"); + const publishJob = workflow.jobs?.publish_existing_telegram_desktop_proof; + const captureJob = workflow.jobs?.run_telegram_desktop_proof; + const validateJob = workflow.jobs?.validate_refs; + + expect(workflow.on?.workflow_dispatch?.inputs?.publish_artifact_name?.required).toBe(false); + expect(workflow.on?.workflow_dispatch?.inputs?.publish_run_id?.required).toBe(false); + expect(captureJob?.if).toBe("needs.resolve_request.outputs.publish_artifact_name == ''"); + expect(validateJob?.if).toBe("needs.resolve_request.outputs.publish_artifact_name == ''"); + expect(publishJob?.if).toBe("needs.resolve_request.outputs.publish_artifact_name != ''"); + expect(workflowText).toContain("publish_run_id is required when publish_artifact_name is set."); + expect(workflowText).toContain('gh run download "$run_id"'); + expect(workflowText).toContain( + '--artifact-root "mantis/telegram-desktop/pr-${TARGET_PR}/published-', + ); + expect(workflowText).toContain( + "PUBLISH_ARTIFACT_URL=https://github.com/${GITHUB_REPOSITORY}/actions/runs/", + ); + }); + it("uses the repo-owned Telegram user driver by default", () => { expect(existsSync(USER_DRIVER)).toBe(true); expect(readFileSync(PROOF_SCRIPT, "utf8")).toContain(