@@ -43,15 +43,20 @@ jobs:
4343 with :
4444 egress-policy : audit
4545
46- - name : Check if review is from phpstan-bot
46+ - name : Check if review is from a maintainer
4747 id : check
4848 env :
4949 GH_TOKEN : ${{ secrets.PHPSTAN_BOT_PR_TOKEN }}
5050 run : |
51- reviewer=$(gh api "repos/phpstan/phpstan-src/pulls/${{ inputs.pr_number }}/reviews/${{ inputs.review_id }}" --jq '.user.login')
51+ review=$(gh api "repos/phpstan/phpstan-src/pulls/${{ inputs.pr_number }}/reviews/${{ inputs.review_id }}")
52+ reviewer=$(echo "$review" | jq -r '.user.login')
53+ association=$(echo "$review" | jq -r '.author_association')
5254 if [ "$reviewer" = "phpstan-bot" ]; then
5355 echo "Skipping review from phpstan-bot"
5456 echo "skip=true" >> "$GITHUB_OUTPUT"
57+ elif [ "$association" != "OWNER" ] && [ "$association" != "MEMBER" ] && [ "$association" != "COLLABORATOR" ]; then
58+ echo "Skipping review from $reviewer ($association), not a maintainer"
59+ echo "skip=true" >> "$GITHUB_OUTPUT"
5560 else
5661 echo "skip=false" >> "$GITHUB_OUTPUT"
5762 fi
6267 runs-on : ubuntu-latest
6368 timeout-minutes : 120
6469 permissions :
65- contents : write
66- pull-requests : write
70+ contents : read
71+ outputs :
72+ pr_head_ref : ${{ steps.review.outputs.pr_head_ref }}
73+ pr_head_sha : ${{ steps.review.outputs.pr_head_sha }}
74+ claude_outcome : ${{ steps.run-claude.outcome }}
6775 steps :
6876 - name : Harden the runner (Audit all outbound calls)
6977 uses : step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
@@ -140,7 +148,7 @@ jobs:
140148 uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
141149 with :
142150 ref : ${{ steps.review.outputs.pr_head_ref }}
143- token : ${{ secrets.PHPSTAN_BOT_FORK_TOKEN }}
151+ persist-credentials : false
144152 fetch-depth : 0
145153
146154 - name : " Install PHP"
@@ -360,7 +368,7 @@ jobs:
360368 prompt+="Please address this review. Make the requested code changes, then run tests with \`make tests\` and static analysis with \`make phpstan\` to verify."
361369 prompt+=$'\n'
362370 prompt+="Commit each logical change separately with a descriptive message."
363- prompt+="Push the commits in the repo. "
371+ prompt+=" Do not push, the commits will be pushed in a separate step. "
364372 prompt+="The results of this prompt will be posted to the pull request in a separate step."
365373
366374 {
@@ -376,9 +384,9 @@ jobs:
376384 id : run-claude
377385 env :
378386 CLAUDE_CODE_OAUTH_TOKEN : ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
379- GH_TOKEN : ${{ secrets.PHPSTAN_BOT_FORK_TOKEN }}
380387 PROMPT : ${{ steps.prompt.outputs.PROMPT }}
381388 run : |
389+ git rev-parse HEAD > /tmp/base-sha.txt
382390 git config user.name "phpstan-bot"
383391 git config user.email "ondrej+phpstanbot@mirtes.cz"
384392
@@ -388,14 +396,75 @@ jobs:
388396 -p "$PROMPT" \
389397 > /tmp/claude-response.txt
390398
399+ - name : Save Claude output
400+ if : always()
401+ run : |
402+ mkdir -p /tmp/claude-output
403+ if [ -f /tmp/claude-response.txt ]; then
404+ cp /tmp/claude-response.txt /tmp/claude-output/response.txt
405+ fi
406+ base_sha=$(cat /tmp/base-sha.txt)
407+ if [ "$(git rev-list --count "$base_sha..HEAD")" -gt 0 ]; then
408+ git bundle create /tmp/claude-output/changes.bundle "$base_sha..HEAD"
409+ fi
410+
411+ - name : Upload Claude output
412+ if : always()
413+ uses : actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
414+ with :
415+ name : claude-output
416+ path : /tmp/claude-output/
417+ if-no-files-found : ignore
418+
419+ # Runs in a fresh VM so that nothing the agent left behind can read these tokens.
420+ publish :
421+ needs : respond
422+ if : always() && needs.respond.result != 'skipped'
423+ runs-on : ubuntu-latest
424+ timeout-minutes : 10
425+ permissions :
426+ contents : read
427+ steps :
428+ - name : Harden the runner (Audit all outbound calls)
429+ uses : step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
430+ with :
431+ egress-policy : audit
432+
433+ - name : Download Claude output
434+ continue-on-error : true
435+ uses : actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
436+ with :
437+ name : claude-output
438+ path : /tmp/claude-output
439+
440+ - name : " Checkout"
441+ if : needs.respond.outputs.claude_outcome == 'success'
442+ uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
443+ with :
444+ ref : ${{ needs.respond.outputs.pr_head_ref }}
445+ token : ${{ secrets.PHPSTAN_BOT_FORK_TOKEN }}
446+
447+ - name : Push commits
448+ id : push
449+ if : needs.respond.outputs.claude_outcome == 'success'
450+ env :
451+ PR_HEAD_REF : ${{ needs.respond.outputs.pr_head_ref }}
452+ run : |
453+ if [ ! -f /tmp/claude-output/changes.bundle ]; then
454+ echo "No new commits to push"
455+ exit 0
456+ fi
457+ git fetch /tmp/claude-output/changes.bundle HEAD
458+ git push origin "FETCH_HEAD:refs/heads/$PR_HEAD_REF"
459+
391460 - name : Reply to review
392461 if : always()
393462 env :
394463 GH_TOKEN : ${{ secrets.PHPSTAN_BOT_PR_TOKEN }}
395464 run : |
396465 body=""
397- if [ -f /tmp/claude-response.txt ]; then
398- body=$(cat /tmp/claude-response.txt)
466+ if [ -f /tmp/claude-output/ response.txt ]; then
467+ body=$(cat /tmp/claude-output/ response.txt)
399468 fi
400469
401470 if [ -z "$body" ]; then
@@ -405,7 +474,7 @@ jobs:
405474 gh api \
406475 "repos/phpstan/phpstan-src/pulls/${{ inputs.pr_number }}/comments" \
407476 -f body="$body" \
408- -F in_reply_to="$(jq -r '.[0].id // empty' /tmp/review-comments.json )" \
477+ -F in_reply_to="$(gh api "repos/phpstan/phpstan-src/pulls/${{ inputs.pr_number }}/reviews/${{ inputs.review_id }}/comments" --jq '.[0].id // empty')" \
409478 2>/dev/null \
410479 || gh pr comment "${{ inputs.pr_number }}" \
411480 --repo "phpstan/phpstan-src" \
@@ -415,11 +484,12 @@ jobs:
415484 if : always()
416485 env :
417486 GH_TOKEN : ${{ secrets.PHPSTAN_BOT_PR_TOKEN }}
418- PR_HEAD_SHA : ${{ steps.review .outputs.pr_head_sha }}
487+ PR_HEAD_SHA : ${{ needs.respond .outputs.pr_head_sha }}
419488 RUN_URL : ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
420- CLAUDE_OUTCOME : ${{ steps.run-claude.outcome }}
489+ CLAUDE_OUTCOME : ${{ needs.respond.outputs.claude_outcome }}
490+ PUSH_OUTCOME : ${{ steps.push.outcome }}
421491 run : |
422- if [ "$CLAUDE_OUTCOME" = "success" ]; then
492+ if [ "$CLAUDE_OUTCOME" = "success" ] && [ "$PUSH_OUTCOME" != "failure" ] ; then
423493 state=success
424494 description="Review addressed"
425495 else
0 commit comments