name: ClawSweeper Dispatch on: issues: types: [opened, reopened, edited, labeled, unlabeled] issue_comment: types: [created, edited] push: branches: [main] 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] pull_request_review: types: [submitted, edited, dismissed] pull_request_review_comment: types: [created, edited] permissions: contents: read concurrency: group: ${{ github.event_name == 'push' && format('clawsweeper-dispatch-{0}-{1}', github.repository, github.ref) || format('clawsweeper-dispatch-{0}-{1}', github.repository, github.event.issue.number || github.event.pull_request.number || github.run_id) }} cancel-in-progress: ${{ github.event_name == 'push' || github.event.action == 'edited' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review' }} jobs: dispatch: runs-on: ubuntu-latest if: >- ${{ (github.event_name != 'issue_comment' || (github.actor != 'clawsweeper[bot]' && github.actor != 'openclaw-clawsweeper[bot]')) && !( endsWith(github.actor, '[bot]') && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && !( github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'stale' && contains(github.event.issue.labels.*.name, 'bug') && (github.actor_id == '257215752' || github.actor_id == '264559031') ) ) }} 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: Debounce main push dispatch if: ${{ github.event_name == 'push' }} run: sleep 45 - name: Install GitHub API backoff helper run: | cat > "$RUNNER_TEMP/github-api-backoff.sh" <<'BASH' gh_api_with_retry() { local attempt output status lower_output for attempt in 1 2 3 4 5; do if output="$(gh api "$@" 2>&1)"; then printf '%s\n' "$output" return 0 fi status=$? lower_output="${output,,}" if [[ "$lower_output" != *"rate limit"* && "$output" != *"HTTP 429"* ]]; then printf '%s\n' "$output" >&2 return "$status" fi echo "::warning::GitHub API throttled ClawSweeper dispatch on attempt ${attempt}; retrying after backoff." >&2 sleep $((attempt * attempt * 5)) done printf '%s\n' "$output" >&2 return "$status" } BASH - 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: Create target PR acknowledgement token id: pr_ack_token if: >- ${{ github.event_name == 'pull_request_target' && ( github.event.action == 'ready_for_review' || (github.event.action == 'opened' && github.event.pull_request.draft == false) ) && env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true' }} continue-on-error: 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 - name: Acknowledge received pull request if: >- ${{ github.event_name == 'pull_request_target' && ( github.event.action == 'ready_for_review' || (github.event.action == 'opened' && github.event.pull_request.draft == false) ) && env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true' }} continue-on-error: true env: ACK_TOKEN: ${{ steps.pr_ack_token.outputs.token }} TARGET_REPO: ${{ github.repository }} ITEM_NUMBER: ${{ github.event.pull_request.number }} SOURCE_ACTION: ${{ github.event.action }} run: | set -euo pipefail if [ -z "$ACK_TOKEN" ]; then echo "::notice::Skipping ClawSweeper pull request acknowledgement because no target credential is configured." exit 0 fi has_ack_marker() { jq -e \ --arg marker_prefix "clawsweeper-pr-ack:" \ --arg marker_suffix " item=$ITEM_NUMBER -->" \ 'any(.[]; (.body // "") as $body | ($body | contains($marker_prefix)) and ($body | contains($marker_suffix)))' \ <<< "$1" >/dev/null } comments="$(GH_TOKEN="$ACK_TOKEN" gh api \ "repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments?per_page=100")" if has_ack_marker "$comments"; then echo "ClawSweeper pull request acknowledgement already exists." exit 0 fi # opened and ready_for_review can fire seconds apart for the same # pull request, and both runs can list comments before either # acknowledgement is visible. Wait, then recheck right before # posting; a superseding run cancels this one while it sleeps. sleep 15 comments="$(GH_TOKEN="$ACK_TOKEN" gh api \ "repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments?per_page=100")" if has_ack_marker "$comments"; then echo "ClawSweeper pull request acknowledgement already exists." exit 0 fi ack_body="$(printf '%s\n' \ "" \ "🦞👀" \ "ClawSweeper picked this up." \ "" \ "Pull request received. I will update this pull request when review starts.")" ack_payload="$(jq -nc --arg body "$ack_body" '{body:$body}')" GH_TOKEN="$ACK_TOKEN" gh api \ "repos/$TARGET_REPO/issues/$ITEM_NUMBER/comments" \ --method POST \ --input - <<< "$ack_payload" - name: Dispatch GitHub activity to ClawSweeper env: GH_TOKEN: ${{ steps.token.outputs.token }} TARGET_REPO: ${{ github.repository }} SOURCE_EVENT: ${{ github.event_name }} SOURCE_ACTION: ${{ github.event.action }} ACTOR: ${{ github.actor }} run: | set -euo pipefail if [ -z "$GH_TOKEN" ]; then echo "::notice::Skipping GitHub activity dispatch because no ClawSweeper app token is configured." exit 0 fi . "$RUNNER_TEMP/github-api-backoff.sh" activity="$(jq -c \ --arg target_repo "$TARGET_REPO" \ --arg event_name "$SOURCE_EVENT" \ --arg source_action "$SOURCE_ACTION" \ --arg actor "$ACTOR" \ ' def body_excerpt(value): if (value // "" | type) == "string" then ((value // "") | gsub("\\s+"; " ") | .[0:1200]) else null end; { type: $event_name, repo: $target_repo, action: $source_action, actor: $actor, subject: ( if .pull_request then { kind: "pull_request", number: .pull_request.number, title: .pull_request.title, url: .pull_request.html_url, state: (if .pull_request.merged == true then "merged" else .pull_request.state end) } elif .issue then { kind: (if .issue.pull_request then "pull_request" else "issue" end), number: .issue.number, title: .issue.title, url: .issue.html_url, state: .issue.state } elif $event_name == "push" then { kind: "push", title: (.head_commit.message // .after // "push"), url: (.head_commit.url // .compare), state: .ref } else { kind: $event_name } end), comment: (if .comment then { id: .comment.id, url: .comment.html_url, body_excerpt: body_excerpt(.comment.body) } else null end), review: (if .review then { id: .review.id, state: .review.state, url: .review.html_url, body_excerpt: body_excerpt(.review.body) } else null end), review_comment: (if .comment and $event_name == "pull_request_review_comment" then { id: .comment.id, path: .comment.path, line: (.comment.line // .comment.original_line), url: .comment.html_url, body_excerpt: body_excerpt(.comment.body) } else null end), push: (if $event_name == "push" then { before: .before, after: .after, ref: .ref, compare: .compare, head_commit: .head_commit.id } else null end), delivery_id: (.comment.id // .review.id // .pull_request.head.sha // .issue.updated_at // .after // env.GITHUB_RUN_ID) } | del(.. | nulls) ' "$GITHUB_EVENT_PATH")" payload="$(jq -nc --argjson activity "$activity" \ '{event_type:"github_activity",client_payload:{activity:$activity}}')" if gh_api_with_retry repos/openclaw/clawsweeper/dispatches \ --method POST \ --input - <<< "$payload"; then echo "Dispatched GitHub activity to ClawSweeper." else echo "::warning::Skipping GitHub activity dispatch because the configured credential could not dispatch to openclaw/clawsweeper." fi - name: Dispatch exact ClawSweeper review if: ${{ github.event_name == 'issues' || github.event_name == 'pull_request_target' }} env: GH_TOKEN: ${{ steps.token.outputs.token }} TARGET_REPO: ${{ github.repository }} TARGET_BRANCH: ${{ github.event.repository.default_branch }} 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 ClawSweeper app token is configured. Not falling back to a maintainer token." exit 0 fi . "$RUNNER_TEMP/github-api-backoff.sh" 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" \ --arg target_branch "$TARGET_BRANCH" \ --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,target_branch:$target_branch,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))}')" if gh_api_with_retry repos/openclaw/clawsweeper/dispatches \ --method POST \ --input - <<< "$payload"; then echo "Dispatched ClawSweeper review." else echo "::warning::Skipping ClawSweeper dispatch because the configured credential could not dispatch to openclaw/clawsweeper." fi - 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: | set -euo pipefail if [ -z "$DISPATCH_TOKEN" ]; then echo "::notice::Skipping ClawSweeper comment dispatch because no ClawSweeper app token is configured." exit 0 fi . "$RUNNER_TEMP/github-api-backoff.sh" body_file="$RUNNER_TEMP/clawsweeper-comment-body.txt" printf '%s\n' "$COMMENT_BODY" > "$body_file" if [ -n "$TARGET_TOKEN" ]; then err="$(mktemp)" if GH_TOKEN="$TARGET_TOKEN" gh_api_with_retry -X POST \ -H "Accept: application/vnd.github+json" \ "repos/$TARGET_REPO/issues/comments/$COMMENT_ID/reactions" \ -f content="eyes" 2>"$err" >/dev/null; then echo "Acknowledged ClawSweeper command comment." elif grep -qi "HTTP 422\\|already exists" "$err"; then echo "ClawSweeper command comment already acknowledged." else cat "$err" >&2 echo "::warning::Could not acknowledge ClawSweeper command comment." fi rm -f "$err" else echo "::notice::Skipping ClawSweeper comment acknowledgement because no target token is configured." fi status_comment_id="" if [ -n "$TARGET_TOKEN" ]; then case "$AUTHOR_ASSOCIATION" in OWNER|MEMBER|COLLABORATOR) status_body="$(printf '%s\n' \ "" \ "🦞👀" \ "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_with_retry \ "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))}')" if GH_TOKEN="$DISPATCH_TOKEN" gh_api_with_retry repos/openclaw/clawsweeper/dispatches \ --method POST \ --input - <<< "$payload"; then echo "Dispatched ClawSweeper comment router." else echo "::warning::Skipping ClawSweeper comment dispatch because the configured credential could not dispatch to openclaw/clawsweeper." fi