From bbf129e13d1c02cf556c2e4a1d556ffcc9e9df23 Mon Sep 17 00:00:00 2001 From: Forge0x Date: Thu, 25 Jun 2026 17:05:54 -0700 Subject: [PATCH] chore: add ClawSweeper dispatch workflow --- .github/workflows/clawsweeper-dispatch.yml | 167 +++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 .github/workflows/clawsweeper-dispatch.yml diff --git a/.github/workflows/clawsweeper-dispatch.yml b/.github/workflows/clawsweeper-dispatch.yml new file mode 100644 index 000000000..90f872fca --- /dev/null +++ b/.github/workflows/clawsweeper-dispatch.yml @@ -0,0 +1,167 @@ +name: ClawSweeper Dispatch + +on: + issues: + types: [opened, reopened, edited, labeled, unlabeled] + issue_comment: + types: [created, edited] + pull_request_target: + 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]') }} + env: + HAS_CLAWSWEEPER_APP_PRIVATE_KEY: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY != '' }} + CLAWSWEEPER_APP_CLIENT_ID: Iv23liA9VELDe2oy0feU + 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@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + with: + client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }} + private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }} + owner: lnflash + 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|lnflash-clawsweeper)(\[bot\])?|(^|[[:space:]])/(clawsweeper|review|autoclose|auto([[:space:]]+|-)?merge)' <<< "$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@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + 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 + 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" \ + --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}}')" + gh api repos/lnflash/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 +' "$COMMENT_BODY" > "$body_file" + if grep -Eiq ')' "$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 +' \ + "" \ + "🦞👀" \ + "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/lnflash/clawsweeper/dispatches \ + --method POST \ + --input - <<< "$payload"