diff --git a/.github/workflows/claude-autofix.yml b/.github/workflows/claude-autofix.yml index f820c9350..24c747076 100644 --- a/.github/workflows/claude-autofix.yml +++ b/.github/workflows/claude-autofix.yml @@ -158,9 +158,13 @@ jobs: jq -s ' def trusted: (.author.__typename == "Bot") or (.authorAssociation | IN("OWNER", "MEMBER", "COLLABORATOR")); + # A thread whose last comment is an autofix reply was answered in an earlier round; + # it comes back once a reviewer replies to it. [ .[] | select(.isResolved | not) | select(.comments.nodes[0] | trusted) + | select(.comments.nodes[-1] + | (.author.__typename == "Bot" and (.body | startswith("**Claude autofix ("))) | not) | { id, path, line: (.line // .originalLine), outdated: .isOutdated, comments: [ .comments.nodes[] | select(trusted) | { author: (.author.login // "ghost"), body } ] } ] @@ -431,7 +435,7 @@ jobs: # Replies go out only for threads that were staged, whatever ids the report holds. A thread # Claude calls fixed is resolved only when a fix commit was pushed. - - name: Reply to and resolve the threads + - name: Reply to the threads if: needs.fix.outputs.threads != '0' env: GH_TOKEN: ${{ github.token }} @@ -439,6 +443,7 @@ jobs: LANDED: ${{ needs.fix.outputs.fixed == 'true' && steps.push.outputs.pushed == 'true' }} run: | set -euo pipefail + : > "$RUNNER_TEMP/to-resolve" jq -c --slurpfile t "$DIR/threads.json" \ '($t[0] | map(.id)) as $ids | .[] | select(.id as $i | $ids | index($i)) | select(.status | IN("fixed", "declined", "unclear"))' \ @@ -458,15 +463,31 @@ jobs: comment { id } } }' --silent || echo "::warning::Couldn't reply to $id." - if [ "$status" = fixed ]; then - gh api graphql -f thread="$id" -f query=' - mutation($thread: ID!) { - resolveReviewThread(input: {threadId: $thread}) { thread { id } } - }' --silent || echo "::warning::Couldn't resolve $id." - fi + # The loop runs in a pipe's subshell, so the threads to resolve go to a file. + if [ "$status" = fixed ]; then echo "$id" >> "$RUNNER_TEMP/to-resolve"; fi done + # Its own step so only these calls see the maintainer's token, which resolving takes: + # GITHUB_TOKEN can reply to a thread but not resolve it ("Resource not accessible by + # integration"). A fixed thread left open is staged again next round and answered twice, so + # a failed resolve fails the step. + - name: Resolve the fixed threads + if: needs.fix.outputs.threads != '0' + env: + GH_TOKEN: ${{ secrets.AUTOMERGE_TOKEN }} + run: | + failed=0 + while read -r id; do + gh api graphql -f thread="$id" -f query=' + mutation($thread: ID!) { + resolveReviewThread(input: {threadId: $thread}) { thread { id } } + }' --silent || { echo "::error::Couldn't resolve $id."; failed=1; } + done < "$RUNNER_TEMP/to-resolve" + exit "$failed" + + # Posted once the push went through, even when a reply or a resolve failed after it. - name: Summarise + if: ${{ !cancelled() && steps.push.outcome == 'success' }} env: GH_TOKEN: ${{ github.token }} PR: ${{ github.event.pull_request.html_url }}