ClawSweeper Dispatch #80
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: ClawSweeper Dispatch | |
| on: | |
| issues: | |
| types: [opened, reopened, edited, labeled, unlabeled] | |
| issue_comment: | |
| types: [created, edited] | |
| pull_request_target: # zizmor: ignore[dangerous-triggers] maintainer-owned external dispatch; no checkout or untrusted PR code execution | |
| types: [opened, reopened, synchronize, ready_for_review, edited, labeled, unlabeled] | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: clawsweeper-dispatch-${{ github.repository }}-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }} | |
| cancel-in-progress: ${{ github.event.action == 'edited' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review' }} | |
| jobs: | |
| dispatch: | |
| runs-on: ubuntu-latest | |
| if: ${{ !(endsWith(github.actor, '[bot]') && (github.event.action == 'labeled' || github.event.action == 'unlabeled')) }} | |
| env: | |
| HAS_CLAWSWEEPER_APP_PRIVATE_KEY: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY != '' }} | |
| CLAWSWEEPER_APP_CLIENT_ID: Iv23liOECG0slfuhz093 | |
| SUPERSEDES_IN_PROGRESS: ${{ (github.event.action == 'edited' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review') && 'true' || 'false' }} | |
| steps: | |
| - name: Debounce bursty metadata events | |
| if: ${{ github.event.action == 'labeled' || github.event.action == 'unlabeled' }} | |
| run: sleep 20 | |
| - name: Create ClawSweeper dispatch token | |
| id: token | |
| if: ${{ env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true' }} | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| owner: openclaw | |
| repositories: clawsweeper | |
| permission-contents: write | |
| - name: Pre-filter ClawSweeper comment | |
| id: comment_filter | |
| if: ${{ github.event_name == 'issue_comment' }} | |
| env: | |
| COMMENT_BODY: ${{ github.event.comment.body }} | |
| run: | | |
| set -euo pipefail | |
| if grep -Eiq '(^|[[:space:]])@(clawsweeper|openclaw-clawsweeper)\b(\[bot\])?|(^|[[:space:]])/(clawsweeper|review|autoclose|auto([[:space:]]+|-)?merge)\b' <<< "$COMMENT_BODY"; then | |
| echo "is_command=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "is_command=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Create target comment token | |
| id: target_token | |
| if: >- | |
| ${{ | |
| github.event_name == 'issue_comment' && | |
| steps.comment_filter.outputs.is_command == 'true' && | |
| env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true' | |
| }} | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} | |
| owner: ${{ github.repository_owner }} | |
| repositories: ${{ github.event.repository.name }} | |
| permission-issues: write | |
| permission-pull-requests: read | |
| - name: Dispatch exact ClawSweeper review | |
| if: ${{ github.event_name != 'issue_comment' }} | |
| env: | |
| GH_TOKEN: ${{ steps.token.outputs.token }} | |
| TARGET_REPO: ${{ github.repository }} | |
| ITEM_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} | |
| ITEM_KIND: ${{ github.event_name == 'pull_request_target' && 'pull_request' || 'issue' }} | |
| SOURCE_EVENT: ${{ github.event_name }} | |
| SOURCE_ACTION: ${{ github.event.action }} | |
| run: | | |
| if [ -z "$GH_TOKEN" ]; then | |
| echo "::notice::Skipping ClawSweeper dispatch because no dispatch credential is configured." | |
| exit 0 | |
| fi | |
| ingress_fingerprint="$(node <<'NODE' | |
| const crypto = require("node:crypto"); | |
| const fs = require("node:fs"); | |
| const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")); | |
| const pullRequest = event.pull_request && typeof event.pull_request === "object" | |
| ? event.pull_request | |
| : {}; | |
| const headSha = String(pullRequest.head?.sha || "").trim().toLowerCase(); | |
| const updatedAt = String(pullRequest.updated_at || "").trim(); | |
| if ( | |
| process.env.ITEM_KIND !== "pull_request" || | |
| !/^[0-9a-f]{40}$/.test(headSha) || | |
| !updatedAt | |
| ) { | |
| process.stdout.write(""); | |
| } else { | |
| process.stdout.write( | |
| crypto | |
| .createHash("sha256") | |
| .update( | |
| JSON.stringify({ | |
| version: 1, | |
| target_repo: String(process.env.TARGET_REPO || "").toLowerCase(), | |
| item_number: Number(process.env.ITEM_NUMBER), | |
| action: String(process.env.SOURCE_ACTION || ""), | |
| head_sha: headSha, | |
| updated_at: updatedAt, | |
| body: typeof pullRequest.body === "string" ? pullRequest.body : "", | |
| label: String(event.label?.name || ""), | |
| }), | |
| ) | |
| .digest("hex"), | |
| ); | |
| } | |
| NODE | |
| )" | |
| payload="$(jq -nc \ | |
| --arg target_repo "$TARGET_REPO" \ | |
| --argjson item_number "$ITEM_NUMBER" \ | |
| --arg item_kind "$ITEM_KIND" \ | |
| --arg source_event "$SOURCE_EVENT" \ | |
| --arg source_action "$SOURCE_ACTION" \ | |
| --arg ingress_fingerprint "$ingress_fingerprint" \ | |
| --argjson supersedes_in_progress "$SUPERSEDES_IN_PROGRESS" \ | |
| '{event_type:"clawsweeper_item",client_payload:({target_repo:$target_repo,item_number:$item_number,item_kind:$item_kind,source_event:$source_event,source_action:$source_action,supersedes_in_progress:$supersedes_in_progress} + (if $ingress_fingerprint != "" then {ingress_route:"target_dispatcher",ingress_fingerprint:$ingress_fingerprint} else {} end))}')" | |
| gh api repos/openclaw/clawsweeper/dispatches \ | |
| --method POST \ | |
| --input - <<< "$payload" | |
| - name: Acknowledge and dispatch ClawSweeper comment | |
| if: >- | |
| ${{ | |
| github.event_name == 'issue_comment' && | |
| steps.comment_filter.outputs.is_command == 'true' | |
| }} | |
| env: | |
| DISPATCH_TOKEN: ${{ steps.token.outputs.token }} | |
| TARGET_TOKEN: ${{ steps.target_token.outputs.token }} | |
| TARGET_REPO: ${{ github.repository }} | |
| ITEM_NUMBER: ${{ github.event.issue.number }} | |
| COMMENT_ID: ${{ github.event.comment.id }} | |
| COMMENT_BODY: ${{ github.event.comment.body }} | |
| AUTHOR_ASSOCIATION: ${{ github.event.comment.author_association }} | |
| SOURCE_ACTION: ${{ github.event.action }} | |
| run: | | |
| if [ -z "$DISPATCH_TOKEN" ]; then | |
| echo "::notice::Skipping ClawSweeper dispatch because no dispatch credential is configured." | |
| exit 0 | |
| fi | |
| body_file="$RUNNER_TEMP/clawsweeper-comment-body.txt" | |
| printf '%s\n' "$COMMENT_BODY" > "$body_file" | |
| if grep -Eiq '<!--[[:space:]]*clawsweeper-proof-nudge([[:space:]]|-->)' "$body_file"; then | |
| echo "Ignoring ClawSweeper proof-nudge comment." | |
| exit 0 | |
| fi | |
| if [ -n "$TARGET_TOKEN" ]; then | |
| GH_TOKEN="$TARGET_TOKEN" gh api -X POST \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "repos/$TARGET_REPO/issues/comments/$COMMENT_ID/reactions" \ | |
| -f content="eyes" >/dev/null || true | |
| fi | |
| status_comment_id="" | |
| if [ -n "$TARGET_TOKEN" ]; then | |
| case "$AUTHOR_ASSOCIATION" in | |
| OWNER|MEMBER|COLLABORATOR) | |
| status_body="$(printf '%s\n' \ | |
| "<!-- clawsweeper-command-ack:$COMMENT_ID -->" \ | |
| "🦞👀" \ | |
| "ClawSweeper picked this up." \ | |
| "" \ | |
| "Command router queued. I will update this comment with the next step.")" | |
| status_payload="$(jq -nc --arg body "$status_body" '{body:$body}')" | |
| status_err="$(mktemp)" | |
| if status_response="$(GH_TOKEN="$TARGET_TOKEN" gh api \ | |
| "repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments" \ | |
| --method POST \ | |
| --input - <<< "$status_payload" 2>"$status_err")"; then | |
| status_comment_id="$(jq -r '.id // empty' <<< "$status_response")" | |
| else | |
| cat "$status_err" >&2 | |
| echo "::warning::Could not create ClawSweeper queued status comment; dispatching command router without one." | |
| fi | |
| rm -f "$status_err" | |
| ;; | |
| esac | |
| fi | |
| payload="$(jq -nc \ | |
| --arg target_repo "$TARGET_REPO" \ | |
| --argjson item_number "$ITEM_NUMBER" \ | |
| --argjson comment_id "$COMMENT_ID" \ | |
| --arg status_comment_id "$status_comment_id" \ | |
| --arg source_event "issue_comment" \ | |
| --arg source_action "$SOURCE_ACTION" \ | |
| '{event_type:"clawsweeper_comment",client_payload:({target_repo:$target_repo,item_number:$item_number,comment_id:$comment_id,source_event:$source_event,source_action:$source_action,max_comments:"1"} + (if $status_comment_id != "" then {status_comment_id:($status_comment_id|tonumber)} else {} end))}')" | |
| GH_TOKEN="$DISPATCH_TOKEN" gh api repos/openclaw/clawsweeper/dispatches \ | |
| --method POST \ | |
| --input - <<< "$payload" |