name: Mantis Slack Desktop Smoke on: workflow_dispatch: inputs: candidate_ref: description: Ref, tag, or SHA to run inside the VNC desktop required: true default: main type: string pr_number: description: Optional PR number to receive the QA evidence comment required: false type: string scenario_id: description: Slack QA scenario id required: true default: slack-canary type: string approval_checkpoints: description: Run native Slack approval checkpoint mode instead of gateway setup required: false default: false type: boolean keep_vm: description: Keep the desktop lease open after a passing run required: false default: false type: boolean crabbox_provider: description: Crabbox provider for the desktop lease required: false default: aws type: choice options: - aws - hetzner crabbox_market: description: Crabbox capacity market for AWS leases required: false default: on-demand type: choice options: - on-demand - spot crabbox_lease_id: description: Optional existing Crabbox desktop/browser lease id or slug to reuse required: false type: string hydrate_mode: description: Remote workspace hydrate mode required: false default: source type: choice options: - source - prehydrated permissions: contents: read issues: write pull-requests: write concurrency: group: mantis-slack-desktop-smoke-${{ inputs.pr_number || inputs.candidate_ref || github.run_id }}-${{ github.run_attempt }} cancel-in-progress: false 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 jobs: authorize_actor: name: Authorize workflow actor 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"); validate_ref: name: Validate candidate ref needs: authorize_actor if: needs.authorize_actor.outputs.authorized == 'true' runs-on: ubuntu-24.04 outputs: candidate_revision: ${{ steps.validate.outputs.candidate_revision }} steps: - name: Checkout harness ref uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: persist-credentials: false fetch-depth: 0 - name: Validate ref is trusted id: validate env: GH_TOKEN: ${{ github.token }} CANDIDATE_REF: ${{ inputs.candidate_ref }} shell: bash run: | set -euo pipefail git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main revision="$(git rev-parse "${CANDIDATE_REF}^{commit}")" reason="" if git merge-base --is-ancestor "$revision" refs/remotes/origin/main; then reason="main-ancestor" elif git tag --points-at "$revision" | grep -Eq '^v'; then reason="release-tag" else pr_head_count="$( gh api \ -H "Accept: application/vnd.github+json" \ "repos/${GITHUB_REPOSITORY}/commits/${revision}/pulls" \ --jq '[.[] | select(.state == "open" and .head.repo.full_name == "'"${GITHUB_REPOSITORY}"'" and .head.sha == "'"${revision}"'")] | length' )" if [[ "$pr_head_count" != "0" ]]; then reason="open-pr-head" fi fi if [[ -z "$reason" ]]; then echo "Candidate ref '${CANDIDATE_REF}' resolved to ${revision}, which is not trusted for this secret-bearing Mantis run." >&2 exit 1 fi echo "candidate_revision=${revision}" >> "$GITHUB_OUTPUT" { echo "candidate: \`${CANDIDATE_REF}\`" echo "candidate SHA: \`${revision}\`" echo "candidate trust reason: \`${reason}\`" } >> "$GITHUB_STEP_SUMMARY" run_slack_desktop: name: Run Slack desktop smoke needs: validate_ref runs-on: ubuntu-24.04 timeout-minutes: 180 environment: qa-live-shared steps: - 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: Build Mantis harness run: pnpm build - name: Cache Mantis candidate pnpm store uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: | ~/.local/share/pnpm/store ~/.cache/pnpm key: mantis-slack-pnpm-${{ runner.os }}-${{ env.NODE_VERSION }}-${{ hashFiles('pnpm-lock.yaml') }} restore-keys: | mantis-slack-pnpm-${{ runner.os }}-${{ env.NODE_VERSION }}- - 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" "$HOME/.local/bin" 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 "$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/slack-desktop-smoke-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 Slack desktop scenario id: run_mantis env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENCLAW_LIVE_OPENAI_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_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_LEASE_ID: ${{ inputs.crabbox_lease_id }} CRABBOX_PROVIDER: ${{ inputs.crabbox_provider }} CRABBOX_MARKET: ${{ inputs.crabbox_market }} KEEP_VM: ${{ inputs.keep_vm }} HYDRATE_MODE: ${{ inputs.hydrate_mode }} SCENARIO_ID: ${{ inputs.scenario_id }} APPROVAL_CHECKPOINTS: ${{ inputs.approval_checkpoints }} 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_LIVE_OPENAI_KEY require_var OPENCLAW_QA_CONVEX_SITE_URL require_var OPENCLAW_QA_CONVEX_SECRET_CI require_var CRABBOX_COORDINATOR_TOKEN if [[ -z "${CRABBOX_LEASE_ID:-}" && "$CRABBOX_PROVIDER" == "aws" ]]; then runner_ip="$(curl -fsS https://checkip.amazonaws.com | tr -d '[:space:]')" if [[ -z "$runner_ip" ]]; then echo "Could not resolve GitHub runner public IPv4 for AWS SSH ingress." >&2 exit 1 fi export CRABBOX_AWS_SSH_CIDRS="${runner_ip}/32" echo "Using AWS SSH CIDR ${CRABBOX_AWS_SSH_CIDRS}" fi candidate_repo="$(pwd)/.artifacts/qa-e2e/mantis/slack-desktop-smoke-worktrees/candidate" output_rel=".artifacts/qa-e2e/mantis/slack-desktop-smoke" root="$candidate_repo/$output_rel" echo "output_dir=${root}" >> "$GITHUB_OUTPUT" lease_args=() if [[ -n "${CRABBOX_LEASE_ID:-}" ]]; then lease_args=(--lease-id "$CRABBOX_LEASE_ID") fi keep_args=() if [[ "$KEEP_VM" == "true" ]]; then keep_args=(--keep-lease) else keep_args=(--no-keep-lease) fi market_args=() if [[ -n "${CRABBOX_MARKET:-}" ]]; then market_args=(--market "$CRABBOX_MARKET") fi gateway_args=(--gateway-setup) approval_args=() scenario_args=(--scenario "$SCENARIO_ID") scenario_label="$SCENARIO_ID" if [[ "$APPROVAL_CHECKPOINTS" == "true" ]]; then approval_args=(--approval-checkpoints) gateway_args=() if [[ -z "${SCENARIO_ID:-}" || "$SCENARIO_ID" == "slack-canary" || "$SCENARIO_ID" == "approval-checkpoints" ]]; then scenario_args=() scenario_label="approval-checkpoints" fi fi set +e pnpm openclaw qa mantis slack-desktop-smoke \ --repo-root "$candidate_repo" \ --output-dir "$output_rel" \ --provider "$CRABBOX_PROVIDER" \ --class standard \ --idle-timeout 45m \ --ttl 120m \ "${gateway_args[@]}" \ --credential-source convex \ --credential-role ci \ --provider-mode live-frontier \ --hydrate-mode "$HYDRATE_MODE" \ --model openai/gpt-5.5 \ --alt-model openai/gpt-5.5 \ --fast \ "${scenario_args[@]}" \ "${approval_args[@]}" \ "${market_args[@]}" \ "${keep_args[@]}" \ "${lease_args[@]}" mantis_exit=$? set -e if [[ ! -f "$root/mantis-slack-desktop-smoke-summary.json" ]]; then echo "Mantis Slack desktop smoke did not produce a summary." >&2 exit "$mantis_exit" fi if [[ -f "$root/slack-desktop-smoke.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-slack-ffmpeg-apt.log 2>&1 || true sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ffmpeg >>/tmp/mantis-slack-ffmpeg-apt.log 2>&1 || true fi if ! crabbox media preview \ --input "$root/slack-desktop-smoke.mp4" \ --output "$root/slack-desktop-smoke-preview.gif" \ --trimmed-video-output "$root/slack-desktop-smoke-change.mp4" \ --json > "$root/slack-desktop-smoke-preview.json"; then rm -f "$root/slack-desktop-smoke-preview.gif" rm -f "$root/slack-desktop-smoke-change.mp4" rm -f "$root/slack-desktop-smoke-preview.json" echo "::warning::Could not generate Slack motion-trimmed desktop preview." fi fi status="$(jq -r '.status' "$root/mantis-slack-desktop-smoke-summary.json")" screenshot_required=false desktop_capture_inline=true if [[ "$status" == "pass" ]]; then screenshot_required=true fi evidence_summary="Mantis ran Slack QA inside a Crabbox Linux VNC desktop, started an OpenClaw Slack gateway in that VM, opened Slack Web in the visible browser, and captured screenshot/video evidence." expected_result="Slack QA and VM gateway setup pass" checkpoint_artifacts='[]' checkpoint_required=false if [[ "$APPROVAL_CHECKPOINTS" == "true" ]]; then evidence_summary="Mantis ran Slack native approval QA inside a Crabbox Linux VNC desktop, rendered pending/resolved approval checkpoints from the Slack API messages, and stored Slack QA artifacts." expected_result="Slack native exec and plugin approval checkpoints pass" screenshot_required=false desktop_capture_inline=false if [[ "$status" == "pass" ]]; then checkpoint_required=true fi checkpoint_scenarios=() if [[ "$scenario_label" == "approval-checkpoints" ]]; then checkpoint_scenarios=("slack-approval-exec-native" "slack-approval-plugin-native") else checkpoint_scenarios=("$scenario_label") fi checkpoint_scenarios_json="$(printf '%s\n' "${checkpoint_scenarios[@]}" | jq -R . | jq -s .)" checkpoint_artifacts="$( jq -n \ --argjson checkpoint_required "$checkpoint_required" \ --argjson scenario_ids "$checkpoint_scenarios_json" \ ' def scenario_kind($id): if $id == "slack-approval-exec-native" then "exec" elif $id == "slack-approval-plugin-native" then "plugin" else error("unsupported approval checkpoint scenario: \($id)") end; def scenario_title($id): if scenario_kind($id) == "exec" then "Exec" else "Plugin" end; [ $scenario_ids[] as $id | ["pending", "resolved"][] as $state | { kind: "desktopScreenshot", lane: "candidate", label: "\(scenario_title($id)) approval \($state) checkpoint", path: "approval-checkpoints/\($id)-\($state).png", targetPath: "approval-checkpoints/\($id)-\($state).png", alt: "Rendered Slack \(scenario_kind($id)) approval \($state) checkpoint", width: 720, inline: true, required: $checkpoint_required } ] ' )" fi jq -n \ --arg status "$status" \ --arg candidate_sha "${{ needs.validate_ref.outputs.candidate_revision }}" \ --arg scenario "$scenario_label" \ --arg summary "$evidence_summary" \ --arg expected "$expected_result" \ --argjson checkpoint_artifacts "$checkpoint_artifacts" \ --argjson screenshot_required "$screenshot_required" \ --argjson desktop_capture_inline "$desktop_capture_inline" \ '{ schemaVersion: 1, id: "slack-desktop-smoke", title: "Mantis Slack Desktop Smoke QA", summary: $summary, scenario: $scenario, comparison: { candidate: { sha: $candidate_sha, expected: $expected, status: $status, fixed: ($status == "pass") }, pass: ($status == "pass") }, artifacts: ([ { kind: "desktopScreenshot", lane: "candidate", label: "Slack desktop/VNC browser", path: "slack-desktop-smoke.png", targetPath: "slack-desktop.png", alt: "Slack Web desktop screenshot from the Mantis VM", width: 720, inline: $desktop_capture_inline, required: $screenshot_required }, { kind: "motionPreview", lane: "candidate", label: "Slack motion preview", path: "slack-desktop-smoke-preview.gif", targetPath: "slack-desktop-preview.gif", alt: "Animated Slack desktop preview", width: 720, inline: $desktop_capture_inline, required: false }, { kind: "motionClip", lane: "candidate", label: "Slack change MP4", path: "slack-desktop-smoke-change.mp4", targetPath: "slack-desktop-change.mp4", required: false }, { kind: "fullVideo", lane: "candidate", label: "Slack desktop MP4", path: "slack-desktop-smoke.mp4", targetPath: "slack-desktop.mp4", required: false }, { kind: "metadata", lane: "run", label: "Slack desktop summary", path: "mantis-slack-desktop-smoke-summary.json", targetPath: "summary.json" }, { kind: "report", lane: "run", label: "Slack desktop report", path: "mantis-slack-desktop-smoke-report.md", targetPath: "report.md" }, { kind: "metadata", lane: "run", label: "Slack command log", path: "slack-desktop-command.log", targetPath: "slack-desktop-command.log", required: false }, { kind: "metadata", lane: "run", label: "Slack preview metadata", path: "slack-desktop-smoke-preview.json", targetPath: "slack-desktop-preview.json", required: false }, { kind: "metadata", lane: "run", label: "Slack error", path: "error.txt", targetPath: "error.txt", required: false } ] + $checkpoint_artifacts) }' > "$root/mantis-evidence.json" cat "$root/mantis-slack-desktop-smoke-report.md" >> "$GITHUB_STEP_SUMMARY" if [[ "$status" != "pass" ]]; then echo "Slack desktop smoke failed." >&2 exit 1 fi if [[ "$mantis_exit" -ne 0 ]]; then echo "Slack desktop smoke exited with $mantis_exit after reporting status $status." >&2 exit "$mantis_exit" fi - name: Upload Mantis Slack desktop artifacts id: upload_artifact if: ${{ always() && steps.run_mantis.outputs.output_dir != '' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: mantis-slack-desktop-smoke-${{ 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() && inputs.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() && inputs.pr_number != '' && steps.run_mantis.outputs.output_dir != '' && steps.upload_artifact.outputs.artifact-url != '' }} 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: workflow_dispatch TARGET_PR: ${{ inputs.pr_number }} shell: bash run: | set -euo pipefail root="${{ steps.run_mantis.outputs.output_dir }}" node scripts/mantis/publish-pr-evidence.mjs \ --manifest "$root/mantis-evidence.json" \ --target-pr "$TARGET_PR" \ --artifact-root "mantis/slack-desktop-smoke/pr-${TARGET_PR}/run-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \ --marker "" \ --artifact-url "$ARTIFACT_URL" \ --run-url "https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ --request-source "$REQUEST_SOURCE"