diff --git a/.github/workflows/clawsweeper-dispatch.yml b/.github/workflows/clawsweeper-dispatch.yml index 5af378a9..bf7bea08 100644 --- a/.github/workflows/clawsweeper-dispatch.yml +++ b/.github/workflows/clawsweeper-dispatch.yml @@ -12,33 +12,25 @@ permissions: contents: read concurrency: - group: clawsweeper-dispatch-${{ github.repository }}-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }} + group: clawsweeper-dispatch-${{ github.repository }}-${{ github.event_name }}-${{ github.event.comment.id || github.event.issue.number || github.event.pull_request.number || github.run_id }}-${{ endsWith(github.actor, '[bot]') && (github.event.action == 'labeled' || github.event.action == 'unlabeled') && github.actor || 'dispatchable' }} cancel-in-progress: ${{ github.event.action == 'edited' || github.event.action == 'synchronize' || github.event.action == 'ready_for_review' }} jobs: dispatch: runs-on: ubuntu-latest + # id-token proves this repository, event, branch, and run to the queue. + # The job never checks out or executes pull request code. + permissions: + contents: read + id-token: write 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' }} @@ -46,157 +38,222 @@ jobs: 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 + if [[ "$COMMENT_BODY" == *")' "$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 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 \ + set -euo pipefail + client_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" + '{target_repo:$target_repo,item_number:$item_number,comment_id:$comment_id,source_event:"issue_comment",source_action:$source_action}')" + # The App webhook remains an independent delivery route. + if [ -n "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ] && + [ -n "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] && + oidc_token="$(curl --fail --silent --show-error --connect-timeout 5 --max-time 15 \ + --header "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ + --get --data-urlencode "audience=$TARGET_DISPATCH_URL" \ + "$ACTIONS_ID_TOKEN_REQUEST_URL" | jq -er '.value | strings | select(length > 0)')" && + [ -n "$oidc_token" ]; then + echo "::add-mask::$oidc_token" + direct_body="$RUNNER_TEMP/clawsweeper-direct-intake.json" + if direct_status="$(curl --silent --show-error --connect-timeout 5 --max-time 20 \ + --request POST \ + --header "authorization: Bearer $oidc_token" \ + --header "content-type: application/json" \ + --data "$client_payload" \ + --output "$direct_body" \ + --write-out '%{http_code}' \ + "$TARGET_DISPATCH_URL")" && + [[ "$direct_status" == 2[0-9][0-9] ]]; then + echo "Queued ClawSweeper intake directly: $(jq -c . "$direct_body" 2>/dev/null || echo '{}')" + exit 0 + fi + # Only non-transient 4xx is a deliberate refusal; the App webhook covers it. + # Timeouts, throttling, and verifier/Worker failures must stay re-runnable. + if [[ "${direct_status:-}" == 4[0-9][0-9] && "$direct_status" != 408 && "$direct_status" != 429 ]]; then + echo "::warning::ClawSweeper intake declined this event (HTTP $direct_status); the App webhook route still applies." + exit 0 + fi + echo "::error::ClawSweeper intake unavailable (HTTP ${direct_status:-none}); re-run this job after the Worker recovers." + else + echo "::error::ClawSweeper OIDC token unavailable; the dispatch job needs id-token: write." + fi + exit 1