Skip to content

Commit 76da17b

Browse files
committed
Proper Claude Fix Issue workflow (for the fork)
1 parent b054374 commit 76da17b

File tree

3 files changed

+311
-0
lines changed

3 files changed

+311
-0
lines changed
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
name: "Claude Fix Issue"
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
issue-number:
7+
description: "Issue number from phpstan/phpstan repository"
8+
required: true
9+
type: string
10+
workflow_call:
11+
inputs:
12+
issue-number:
13+
description: "Issue number from phpstan/phpstan repository"
14+
required: true
15+
type: string
16+
17+
permissions:
18+
contents: read
19+
20+
jobs:
21+
fix:
22+
name: "Fix #${{ inputs.issue-number }}"
23+
runs-on: "ubuntu-latest"
24+
timeout-minutes: 60
25+
permissions:
26+
contents: read
27+
issues: read
28+
pull-requests: write
29+
30+
steps:
31+
- name: Harden the runner (Audit all outbound calls)
32+
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
33+
with:
34+
egress-policy: audit
35+
36+
- name: "Checkout"
37+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
38+
with:
39+
ref: 2.1.x
40+
repository: phpstan/phpstan-src
41+
fetch-depth: 0
42+
43+
- name: "Install PHP"
44+
uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2
45+
with:
46+
coverage: "none"
47+
php-version: "8.4"
48+
ini-file: development
49+
extensions: mbstring
50+
51+
- uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3
52+
53+
- name: "Install Claude Code"
54+
run: npm install -g @anthropic-ai/claude-code
55+
56+
- name: "Fetch issue details"
57+
id: issue
58+
env:
59+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
60+
ISSUE_NUMBER: ${{ inputs.issue-number }}
61+
run: |
62+
ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" \
63+
--repo phpstan/phpstan \
64+
--json title,body,url)
65+
66+
TITLE=$(echo "$ISSUE_JSON" | jq -r '.title')
67+
URL=$(echo "$ISSUE_JSON" | jq -r '.url')
68+
echo "title=$TITLE" >> "$GITHUB_OUTPUT"
69+
echo "url=$URL" >> "$GITHUB_OUTPUT"
70+
echo "$ISSUE_JSON" | jq -r '.body' > /tmp/issue-body.txt
71+
72+
- name: "Run Claude Code"
73+
env:
74+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
75+
GH_TOKEN: ${{ secrets.PHPSTAN_BOT_FORK_TOKEN }}
76+
run: |
77+
git config user.name "phpstan-bot"
78+
git config user.email "ondrej+phpstanbot@mirtes.cz"
79+
80+
claude --model claude-opus-4-6 \
81+
--dangerously-skip-permissions \
82+
-p "$(cat << 'PROMPT_EOF'
83+
You are working on phpstan/phpstan-src, the source code of PHPStan - a PHP static analysis tool.
84+
85+
Your task is to fix the following GitHub issue from the phpstan/phpstan repository:
86+
Issue phpstan/phpstan#${{ inputs.issue-number }}: ${{ steps.issue.outputs.title }}
87+
URL: ${{ steps.issue.outputs.url }}
88+
89+
Issue body is in the file /tmp/issue-body.txt — read it before proceeding.
90+
91+
## Step 1: Write a regression test
92+
93+
Read .claude/skills/regression-test/SKILL.md for detailed guidance on writing regression tests for PHPStan bugs.
94+
95+
The issue body is already provided above — start from Step 2 of the skill (deciding test type). For Step 1 (gathering context), you only need to fetch the playground samples from any playground links found in the issue body.
96+
97+
Skip Steps 5-6 of the skill (reverting fix and committing) — those are not needed here.
98+
99+
The regression test should fail without the fix — verify this by running it before implementing the fix.
100+
101+
## Step 2: Fix the bug
102+
103+
Implement the fix in the source code under src/. Common areas to look:
104+
- src/Analyser/NodeScopeResolver.php - AST traversal and scope management
105+
- src/Analyser/MutatingScope.php - Type tracking
106+
- src/Analyser/TypeSpecifier.php - Type narrowing from conditions
107+
- src/Type/ - Type system implementations
108+
- src/Rules/ - Rule implementations
109+
- src/Reflection/ - Reflection layer
110+
111+
Read CLAUDE.md for important guidelines about the codebase architecture and common patterns.
112+
113+
## Step 3: Verify the fix
114+
115+
1. Run the regression test to confirm it passes now
116+
2. Run the full test suite: make tests
117+
3. Run PHPStan self-analysis: make phpstan
118+
4. Fix any failures that come up
119+
5. Run make cs-fix to fix any coding standard violations
120+
6. Run make name-collision and fix violations - add different tests in unique namespaces. If the function and class declarations are exactly the same, you can reuse them across files instead of duplicating them.
121+
122+
Do not create a branch, push, or create a PR - this will be handled automatically.
123+
124+
## Step 4: Write a summary
125+
126+
After completing the fix, write two files:
127+
128+
1. /tmp/commit-message.txt - A concise commit message (first line: short summary under 72 chars, then a blank line, then a few bullet points describing key changes). Example:
129+
Fix array_key_exists narrowing for template types
130+
131+
- Added handling for TemplateType in TypeSpecifier when processing array_key_exists
132+
- New regression test in tests/PHPStan/Analyser/nsrt/bug-12345.php
133+
- The root cause was that TypeSpecifier did not unwrap template bounds before narrowing
134+
135+
2. /tmp/pr-description.md - A pull request description in this format:
136+
## Summary
137+
Brief description of what the issue was about and what the fix does.
138+
139+
## Changes
140+
- Bullet points of specific code changes made
141+
- Reference file paths where changes were made
142+
143+
## Root cause
144+
Explain why the bug happened and how the fix addresses it.
145+
146+
## Test
147+
Describe the regression test that was added.
148+
149+
Fixes phpstan/phpstan#${{ inputs.issue-number }}
150+
151+
These files are critical - they will be used for the commit message and PR description.
152+
PROMPT_EOF
153+
)"
154+
155+
- name: "Read Claude's summary"
156+
id: claude-summary
157+
env:
158+
ISSUE_NUMBER: ${{ inputs.issue-number }}
159+
run: |
160+
if [ -f /tmp/commit-message.txt ]; then
161+
delimiter="EOF_$(openssl rand -hex 16)"
162+
{
163+
echo "commit_message<<${delimiter}"
164+
cat /tmp/commit-message.txt
165+
echo "${delimiter}"
166+
} >> "$GITHUB_OUTPUT"
167+
else
168+
echo "commit_message=Fix #$ISSUE_NUMBER" >> "$GITHUB_OUTPUT"
169+
fi
170+
171+
if [ -f /tmp/pr-description.md ]; then
172+
delimiter="EOF_$(openssl rand -hex 16)"
173+
{
174+
echo "pr_body<<${delimiter}"
175+
cat /tmp/pr-description.md
176+
echo "${delimiter}"
177+
} >> "$GITHUB_OUTPUT"
178+
else
179+
echo "pr_body=Fixes phpstan/phpstan#$ISSUE_NUMBER" >> "$GITHUB_OUTPUT"
180+
fi
181+
182+
- name: "Create Pull Request"
183+
id: create-pr
184+
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
185+
with:
186+
branch-token: ${{ secrets.PHPSTAN_BOT_FORK_TOKEN }}
187+
token: ${{ secrets.PHPSTAN_BOT_PR_TOKEN }}
188+
push-to-fork: phpstan-bot/phpstan-src
189+
branch-suffix: random
190+
delete-branch: true
191+
title: "Fix #${{ inputs.issue-number }}: ${{ steps.issue.outputs.title }}"
192+
body: ${{ steps.claude-summary.outputs.pr_body }}
193+
committer: "phpstan-bot <ondrej+phpstanbot@mirtes.cz>"
194+
commit-message: ${{ steps.claude-summary.outputs.commit_message }}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: "Claude Random Easy Fixes (Scheduled)"
2+
3+
on:
4+
schedule:
5+
# Run every day, 4 times, once an hour at :15, from 2pm CET (13:00 UTC) to 5pm CET (16:00 UTC)
6+
- cron: '15 13-16 * * *'
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
trigger:
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: read
16+
actions: write
17+
steps:
18+
- name: Harden the runner (Audit all outbound calls)
19+
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
20+
with:
21+
egress-policy: audit
22+
23+
- name: Trigger Claude Random Easy Fixes
24+
env:
25+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26+
run: gh workflow run claude-random-easy-fixes.yml -f issue_count=5 --repo ${{ github.repository }}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
name: "Claude Random Easy Fixes"
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
issue_count:
7+
description: "Number of issues to pick and fix in parallel"
8+
required: false
9+
default: "1"
10+
type: string
11+
12+
jobs:
13+
pick-issues:
14+
name: "Pick easy fix issues"
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 5
17+
18+
outputs:
19+
matrix: ${{ steps.pick-issues.outputs.matrix }}
20+
21+
permissions:
22+
contents: read
23+
issues: read
24+
25+
steps:
26+
- name: Harden the runner (Audit all outbound calls)
27+
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
28+
with:
29+
egress-policy: audit
30+
31+
- name: "Pick random Easy fix issues"
32+
id: pick-issues
33+
env:
34+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35+
ISSUE_COUNT: ${{ inputs.issue_count || '1' }}
36+
run: |
37+
# Look up milestone number for "Easy fixes"
38+
MILESTONE_NUMBER=$(gh api "repos/phpstan/phpstan/milestones?per_page=100" \
39+
--jq '.[] | select(.title == "Easy fixes") | .number')
40+
41+
if [ -z "$MILESTONE_NUMBER" ]; then
42+
echo "Could not find 'Easy fixes' milestone"
43+
exit 1
44+
fi
45+
46+
# Fetch all open issues in the milestone using pagination
47+
ISSUE_JSON=$(gh api --paginate \
48+
"repos/phpstan/phpstan/issues?state=open&milestone=${MILESTONE_NUMBER}&per_page=100" \
49+
--jq '[.[] | {number: .number, title: .title}]' \
50+
| jq -s 'add // []')
51+
52+
TOTAL=$(echo "$ISSUE_JSON" | jq 'length')
53+
if [ "$TOTAL" -eq 0 ]; then
54+
echo "No issues found in Easy fixes milestone"
55+
exit 1
56+
fi
57+
58+
COUNT=$ISSUE_COUNT
59+
if [ "$COUNT" -gt "$TOTAL" ]; then
60+
COUNT=$TOTAL
61+
fi
62+
63+
# Pick COUNT random unique issues
64+
SELECTED=$(echo "$ISSUE_JSON" | python3 -c "
65+
import json, sys, random
66+
issues = json.load(sys.stdin)
67+
random.shuffle(issues)
68+
count = min(int('$COUNT'), len(issues))
69+
print(json.dumps(issues[:count]))
70+
")
71+
72+
echo "Selected $COUNT issue(s) for fixing"
73+
74+
for NUMBER in $(echo "$SELECTED" | jq -r '.[].number'); do
75+
TITLE=$(echo "$SELECTED" | jq -r --argjson n "$NUMBER" '.[] | select(.number == $n) | .title')
76+
echo "### Selected issue: #$NUMBER - $TITLE" >> "$GITHUB_STEP_SUMMARY"
77+
done
78+
79+
echo "matrix=$(echo "$SELECTED" | jq -c '.')" >> "$GITHUB_OUTPUT"
80+
81+
easy-fix:
82+
name: "Fix #${{ matrix.issue.number }}: ${{ matrix.issue.title }}"
83+
needs: pick-issues
84+
strategy:
85+
fail-fast: false
86+
matrix:
87+
issue: ${{ fromJson(needs.pick-issues.outputs.matrix) }}
88+
uses: ./.github/workflows/claude-fix-issue.yml
89+
with:
90+
issue-number: ${{ matrix.issue.number }}
91+
secrets: inherit

0 commit comments

Comments
 (0)