Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 194 additions & 0 deletions .github/workflows/claude-fix-issue.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
name: "Claude Fix Issue"

on:
workflow_dispatch:
inputs:
issue-number:
description: "Issue number from phpstan/phpstan repository"
required: true
type: string
workflow_call:
inputs:
issue-number:
description: "Issue number from phpstan/phpstan repository"
required: true
type: string

permissions:
contents: read

jobs:
fix:
name: "Fix #${{ inputs.issue-number }}"
runs-on: "ubuntu-latest"
timeout-minutes: 60
permissions:
contents: read
issues: read

Check warning

Code scanning / zizmor

permissions without explanatory comments Warning

permissions without explanatory comments
pull-requests: write

steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit

- name: "Checkout"
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: 2.1.x

Check failure

Code scanning / octoscan

Use of 'actions/checkout' with a custom ref. Error

Use of 'actions/checkout' with a custom ref.
repository: phpstan/phpstan-src
fetch-depth: 0
Comment on lines +36 to +41

Check warning

Code scanning / zizmor

credential persistence through GitHub Actions artifacts Warning

credential persistence through GitHub Actions artifacts

- name: "Install PHP"
uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2
with:
coverage: "none"
php-version: "8.4"
ini-file: development
extensions: mbstring

- uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3

- name: "Install Claude Code"
run: npm install -g @anthropic-ai/claude-code

- name: "Fetch issue details"
id: issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_NUMBER: ${{ inputs.issue-number }}
run: |
ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" \
--repo phpstan/phpstan \
--json title,body,url)

Check failure

Code scanning / octoscan

Write to "$GITHUB_OUTPUT" in a bash script. Error

Write to "$GITHUB_OUTPUT" in a bash script.
TITLE=$(echo "$ISSUE_JSON" | jq -r '.title')

Check failure

Code scanning / octoscan

Write to "$GITHUB_OUTPUT" in a bash script. Error

Write to "$GITHUB_OUTPUT" in a bash script.
URL=$(echo "$ISSUE_JSON" | jq -r '.url')
echo "title=$TITLE" >> "$GITHUB_OUTPUT"
echo "url=$URL" >> "$GITHUB_OUTPUT"
echo "$ISSUE_JSON" | jq -r '.body' > /tmp/issue-body.txt

- name: "Run Claude Code"
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
GH_TOKEN: ${{ secrets.PHPSTAN_BOT_FORK_TOKEN }}
run: |
git config user.name "phpstan-bot"

Check failure

Code scanning / octoscan

Expression injection, "steps..outputs." is potentially untrusted. Error

Expression injection, "steps.**.outputs.**" is potentially untrusted.
git config user.email "ondrej+phpstanbot@mirtes.cz"

claude --model claude-opus-4-6 \
--dangerously-skip-permissions \
-p "$(cat << 'PROMPT_EOF'
You are working on phpstan/phpstan-src, the source code of PHPStan - a PHP static analysis tool.

Your task is to fix the following GitHub issue from the phpstan/phpstan repository:
Issue phpstan/phpstan#${{ inputs.issue-number }}: ${{ steps.issue.outputs.title }}

Check failure

Code scanning / zizmor

code injection via template expansion Error

code injection via template expansion

Check warning

Code scanning / zizmor

code injection via template expansion Warning

code injection via template expansion

Check warning

Code scanning / zizmor

code injection via template expansion Warning

code injection via template expansion

Check notice

Code scanning / zizmor

code injection via template expansion Note

code injection via template expansion
URL: ${{ steps.issue.outputs.url }}

Check warning

Code scanning / zizmor

code injection via template expansion Warning

code injection via template expansion

Check notice

Code scanning / zizmor

code injection via template expansion Note

code injection via template expansion

Issue body is in the file /tmp/issue-body.txt — read it before proceeding.

## Step 1: Write a regression test

Read .claude/skills/regression-test/SKILL.md for detailed guidance on writing regression tests for PHPStan bugs.

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.

Skip Steps 5-6 of the skill (reverting fix and committing) — those are not needed here.

The regression test should fail without the fix — verify this by running it before implementing the fix.

## Step 2: Fix the bug

Implement the fix in the source code under src/. Common areas to look:
- src/Analyser/NodeScopeResolver.php - AST traversal and scope management
- src/Analyser/MutatingScope.php - Type tracking
- src/Analyser/TypeSpecifier.php - Type narrowing from conditions
- src/Type/ - Type system implementations
- src/Rules/ - Rule implementations
- src/Reflection/ - Reflection layer

Read CLAUDE.md for important guidelines about the codebase architecture and common patterns.

## Step 3: Verify the fix

1. Run the regression test to confirm it passes now
2. Run the full test suite: make tests
3. Run PHPStan self-analysis: make phpstan
4. Fix any failures that come up
5. Run make cs-fix to fix any coding standard violations
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.

Do not create a branch, push, or create a PR - this will be handled automatically.

## Step 4: Write a summary

After completing the fix, write two files:

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:
Fix array_key_exists narrowing for template types

- Added handling for TemplateType in TypeSpecifier when processing array_key_exists
- New regression test in tests/PHPStan/Analyser/nsrt/bug-12345.php
- The root cause was that TypeSpecifier did not unwrap template bounds before narrowing

2. /tmp/pr-description.md - A pull request description in this format:
## Summary
Brief description of what the issue was about and what the fix does.

## Changes
- Bullet points of specific code changes made
- Reference file paths where changes were made

## Root cause
Explain why the bug happened and how the fix addresses it.

## Test
Describe the regression test that was added.

Fixes phpstan/phpstan#${{ inputs.issue-number }}

Check failure

Code scanning / zizmor

code injection via template expansion Error

code injection via template expansion

Check warning

Code scanning / zizmor

code injection via template expansion Warning

code injection via template expansion

These files are critical - they will be used for the commit message and PR description.
PROMPT_EOF
)"

- name: "Read Claude's summary"

Check failure

Code scanning / octoscan

Write to "$GITHUB_OUTPUT" in a bash script. Error

Write to "$GITHUB_OUTPUT" in a bash script.
id: claude-summary
env:
ISSUE_NUMBER: ${{ inputs.issue-number }}
run: |
if [ -f /tmp/commit-message.txt ]; then
delimiter="EOF_$(openssl rand -hex 16)"
{
echo "commit_message<<${delimiter}"
cat /tmp/commit-message.txt
echo "${delimiter}"

Check failure

Code scanning / octoscan

Write to "$GITHUB_OUTPUT" in a bash script. Error

Write to "$GITHUB_OUTPUT" in a bash script.
} >> "$GITHUB_OUTPUT"
else

Check failure

Code scanning / octoscan

Write to "$GITHUB_OUTPUT" in a bash script. Error

Write to "$GITHUB_OUTPUT" in a bash script.
echo "commit_message=Fix #$ISSUE_NUMBER" >> "$GITHUB_OUTPUT"

Check failure

Code scanning / octoscan

Write to "$GITHUB_OUTPUT" in a bash script. Error

Write to "$GITHUB_OUTPUT" in a bash script.
fi

if [ -f /tmp/pr-description.md ]; then
delimiter="EOF_$(openssl rand -hex 16)"
{
echo "pr_body<<${delimiter}"
cat /tmp/pr-description.md
echo "${delimiter}"
} >> "$GITHUB_OUTPUT"
else
echo "pr_body=Fixes phpstan/phpstan#$ISSUE_NUMBER" >> "$GITHUB_OUTPUT"
fi
Comment on lines +1 to +180

Check warning

Code scanning / zizmor

insufficient job-level concurrency limits Warning

insufficient job-level concurrency limits

- name: "Create Pull Request"
id: create-pr
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
branch-token: ${{ secrets.PHPSTAN_BOT_FORK_TOKEN }}
token: ${{ secrets.PHPSTAN_BOT_PR_TOKEN }}
push-to-fork: phpstan-bot/phpstan-src
branch-suffix: random
delete-branch: true
title: "Fix phpstan/phpstan#${{ inputs.issue-number }}: ${{ steps.issue.outputs.title }}"
body: ${{ steps.claude-summary.outputs.pr_body }}
committer: "phpstan-bot <ondrej+phpstanbot@mirtes.cz>"
commit-message: ${{ steps.claude-summary.outputs.commit_message }}
26 changes: 26 additions & 0 deletions .github/workflows/claude-random-easy-fixes-scheduled.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: "Claude Random Easy Fixes (Scheduled)"

on:
schedule:
# Run every day, 4 times, once an hour at :15, from 2pm CET (13:00 UTC) to 5pm CET (16:00 UTC)
- cron: '15 13-16 * * *'

permissions:
contents: read

jobs:
trigger:

Check notice

Code scanning / zizmor

workflow or action definition without a name Note

workflow or action definition without a name
runs-on: ubuntu-latest
permissions:
contents: read
actions: write

Check warning

Code scanning / zizmor

permissions without explanatory comments Warning

permissions without explanatory comments
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit

- name: Trigger Claude Random Easy Fixes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh workflow run claude-random-easy-fixes.yml -f issue_count=5 --repo ${{ github.repository }}

Check warning

Code scanning / zizmor

code injection via template expansion Warning

code injection via template expansion
91 changes: 91 additions & 0 deletions .github/workflows/claude-random-easy-fixes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: "Claude Random Easy Fixes"

on:
workflow_dispatch:
inputs:
issue_count:
description: "Number of issues to pick and fix in parallel"
required: false
default: "1"
type: string

jobs:
pick-issues:
name: "Pick easy fix issues"
runs-on: ubuntu-latest
timeout-minutes: 5

outputs:
matrix: ${{ steps.pick-issues.outputs.matrix }}

permissions:
contents: read
issues: read

Check warning

Code scanning / zizmor

permissions without explanatory comments Warning

permissions without explanatory comments

steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2
with:
egress-policy: audit

- name: "Pick random Easy fix issues"
id: pick-issues
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_COUNT: ${{ inputs.issue_count || '1' }}
run: |
# Look up milestone number for "Easy fixes"
MILESTONE_NUMBER=$(gh api "repos/phpstan/phpstan/milestones?per_page=100" \
--jq '.[] | select(.title == "Easy fixes") | .number')

if [ -z "$MILESTONE_NUMBER" ]; then
echo "Could not find 'Easy fixes' milestone"
exit 1
fi

# Fetch all open issues in the milestone using pagination
ISSUE_JSON=$(gh api --paginate \
"repos/phpstan/phpstan/issues?state=open&milestone=${MILESTONE_NUMBER}&per_page=100" \
--jq '[.[] | {number: .number, title: .title}]' \
| jq -s 'add // []')

TOTAL=$(echo "$ISSUE_JSON" | jq 'length')
if [ "$TOTAL" -eq 0 ]; then
echo "No issues found in Easy fixes milestone"
exit 1
fi

COUNT=$ISSUE_COUNT
if [ "$COUNT" -gt "$TOTAL" ]; then
COUNT=$TOTAL
fi

# Pick COUNT random unique issues
SELECTED=$(echo "$ISSUE_JSON" | python3 -c "
import json, sys, random
issues = json.load(sys.stdin)
random.shuffle(issues)
count = min(int('$COUNT'), len(issues))
print(json.dumps(issues[:count]))
")

echo "Selected $COUNT issue(s) for fixing"

for NUMBER in $(echo "$SELECTED" | jq -r '.[].number'); do
TITLE=$(echo "$SELECTED" | jq -r --argjson n "$NUMBER" '.[] | select(.number == $n) | .title')
echo "### Selected issue: #$NUMBER - $TITLE" >> "$GITHUB_STEP_SUMMARY"
done

echo "matrix=$(echo "$SELECTED" | jq -c '.')" >> "$GITHUB_OUTPUT"

Check failure

Code scanning / octoscan

Write to "$GITHUB_OUTPUT" in a bash script. Error

Write to "$GITHUB_OUTPUT" in a bash script.

easy-fix:
name: "Fix #${{ matrix.issue.number }}: ${{ matrix.issue.title }}"
needs: pick-issues
strategy:
fail-fast: false
matrix:
issue: ${{ fromJson(needs.pick-issues.outputs.matrix) }}
uses: ./.github/workflows/claude-fix-issue.yml

Check warning

Code scanning / zizmor

secrets unconditionally inherited by called workflow Warning

secrets unconditionally inherited by called workflow

Check failure

Code scanning / octoscan

Use of local workflow "./.github/workflows/claude-fix-issue.yml" Error

Use of local workflow "./.github/workflows/claude-fix-issue.yml"
with:
issue-number: ${{ matrix.issue.number }}
secrets: inherit
Loading