Skip to content

Commit 3a0a663

Browse files
TomasVotrubaclaude
andauthored
[ci] kick of auto issue fixer (#8004)
* Add CLAUDE.md with project conventions Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * [ci] kick of auto fix issuer --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent be04892 commit 3a0a663

2 files changed

Lines changed: 289 additions & 0 deletions

File tree

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
name: Auto Fix Issue
2+
3+
on:
4+
schedule:
5+
- cron: '0 */12 * * *' # every 12 hours
6+
workflow_dispatch: # allow manual trigger
7+
8+
permissions:
9+
contents: write
10+
pull-requests: write
11+
issues: read
12+
13+
jobs:
14+
pick-and-fix:
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 30
17+
18+
steps:
19+
# 1. Checkout
20+
- name: Checkout repository
21+
uses: actions/checkout@v4
22+
with:
23+
fetch-depth: 0
24+
25+
# 2. Install Claude Code
26+
- name: Install Claude Code
27+
run: npm install -g @anthropic-ai/claude-code
28+
29+
# 3. Install PHP & Composer dependencies
30+
- name: Set up PHP
31+
uses: shivammathur/setup-php@v2
32+
with:
33+
php-version: '8.3'
34+
tools: composer:v2
35+
coverage: none
36+
37+
- name: Install Composer dependencies
38+
uses: ramsey/composer-install@v3
39+
40+
# 4. Pick a random open issue from rectorphp/rector (skip any already covered by a PR)
41+
- name: Pick a fixable issue
42+
id: pick
43+
env:
44+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45+
run: |
46+
set -euo pipefail
47+
48+
ISSUES_REPO="rectorphp/rector"
49+
50+
# Fetch open issues (exclude PRs)
51+
issues=$(gh api "repos/$ISSUES_REPO/issues?state=open&per_page=100" \
52+
--jq '[.[] | select(.pull_request == null) | {number, title, body}]')
53+
54+
# Collect issue numbers already covered by an open PR in this repo
55+
covered=$(gh api "repos/${{ github.repository }}/pulls?state=open&per_page=100" \
56+
--jq '[.[].head.ref]' | grep -oP '(?<=issue-)\d+' | tr '\n' ' ' || true)
57+
58+
echo "Issues covered by open PRs: ${covered:-none}"
59+
60+
# Exclude covered issues and pick one at random
61+
candidates=$(echo "$issues" | jq -c \
62+
--argjson covered "$(echo "$covered" | jq -Rsc 'split(" ") | map(select(. != ""))')" \
63+
'[.[] | select((.number | tostring) as $n | $covered | index($n) == null)]')
64+
65+
count=$(echo "$candidates" | jq 'length')
66+
echo "Candidates: $count"
67+
68+
if [ "$count" -eq 0 ]; then
69+
echo "no_issue=true" >> "$GITHUB_OUTPUT"
70+
exit 0
71+
fi
72+
73+
issue=$(echo "$candidates" | jq -c ".[$(( RANDOM % count ))]")
74+
issue_number=$(echo "$issue" | jq -r '.number')
75+
issue_title=$(echo "$issue" | jq -r '.title')
76+
issue_body=$(echo "$issue" | jq -r '.body // ""')
77+
78+
echo "Picked issue #$issue_number — $issue_title"
79+
80+
branch="fix/issue-${issue_number}-$(echo "$issue_title" | \
81+
tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | cut -c1-50 | sed 's/-$//')"
82+
83+
echo "issue_number=$issue_number" >> "$GITHUB_OUTPUT"
84+
echo "issue_title=$issue_title" >> "$GITHUB_OUTPUT"
85+
echo "issue_body<<EOF" >> "$GITHUB_OUTPUT"
86+
echo "$issue_body" >> "$GITHUB_OUTPUT"
87+
echo "EOF" >> "$GITHUB_OUTPUT"
88+
echo "branch=$branch" >> "$GITHUB_OUTPUT"
89+
echo "no_issue=false" >> "$GITHUB_OUTPUT"
90+
91+
# 5. Early exit if no suitable issue was found
92+
- name: No suitable issue found
93+
if: steps.pick.outputs.no_issue == 'true'
94+
run: echo "::notice::No fixable open issues found — skipping this run."
95+
96+
# 6. Configure git
97+
- name: Configure git identity
98+
if: steps.pick.outputs.no_issue == 'false'
99+
run: |
100+
git config user.name "github-actions[bot]"
101+
git config user.email "github-actions[bot]@users.noreply.github.com"
102+
103+
# 7. Create fix branch
104+
- name: Create fix branch
105+
if: steps.pick.outputs.no_issue == 'false'
106+
run: git checkout -b "${{ steps.pick.outputs.branch }}"
107+
108+
# 8. Ask Claude Code to fix the issue
109+
- name: Run Claude Code to fix the issue
110+
if: steps.pick.outputs.no_issue == 'false'
111+
env:
112+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
113+
CLAUDE_ISSUE_NUMBER: ${{ steps.pick.outputs.issue_number }}
114+
CLAUDE_ISSUE_TITLE: ${{ steps.pick.outputs.issue_title }}
115+
CLAUDE_ISSUE_BODY: ${{ steps.pick.outputs.issue_body }}
116+
run: |
117+
claude --print --dangerously-skip-permissions \
118+
"You are an expert software engineer. Fix the following GitHub issue in this repository.
119+
120+
Issue #${CLAUDE_ISSUE_NUMBER}: ${CLAUDE_ISSUE_TITLE}
121+
122+
Issue description:
123+
${CLAUDE_ISSUE_BODY}
124+
125+
Instructions:
126+
1. Understand the root cause of the issue deeply before making any changes.
127+
2. Make the minimal change that fixes the issue — no unrelated refactoring.
128+
3. Follow the existing code style, naming conventions, and formatting.
129+
4. If the project has a test suite, add or update a test that covers this fix.
130+
5. Run the test suite and make sure all tests pass.
131+
6. Stage all changed files with 'git add'.
132+
7. Commit with the message: fix: <short description> (fixes #${CLAUDE_ISSUE_NUMBER})
133+
8. Do NOT push — only commit locally.
134+
9. If you cannot confidently fix this issue without breaking anything, output exactly:
135+
CANNOT_FIX: <reason>
136+
and make no changes."
137+
138+
# 9. Verify something was committed
139+
- name: Check for new commits
140+
if: steps.pick.outputs.no_issue == 'false'
141+
id: check_commit
142+
run: |
143+
ahead=$(git rev-list --count origin/HEAD..HEAD 2>/dev/null || echo 0)
144+
echo "commits_ahead=$ahead" >> "$GITHUB_OUTPUT"
145+
if [ "$ahead" -eq 0 ]; then
146+
echo "::warning::Claude Code made no commits — issue may not be fixable automatically."
147+
fi
148+
149+
# 10. Push branch and open PR
150+
- name: Push branch and open PR
151+
if: >
152+
steps.pick.outputs.no_issue == 'false' &&
153+
steps.check_commit.outputs.commits_ahead != '0'
154+
env:
155+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
156+
BRANCH: ${{ steps.pick.outputs.branch }}
157+
ISSUE_NUMBER: ${{ steps.pick.outputs.issue_number }}
158+
ISSUE_TITLE: ${{ steps.pick.outputs.issue_title }}
159+
run: |
160+
git push origin "$BRANCH"
161+
162+
gh pr create \
163+
--title "fix: $ISSUE_TITLE (fixes rectorphp/rector#$ISSUE_NUMBER)" \
164+
--body "## Summary
165+
Fixes rectorphp/rector#${ISSUE_NUMBER} — ${ISSUE_TITLE}
166+
167+
## Root cause
168+
See the commit message and diff for details of what was changed and why.
169+
170+
## Testing
171+
- Test suite run inside the workflow — see CI logs for details.
172+
173+
> Opened automatically by the [Auto Fix Issue](.github/workflows/auto-fix-issue.yml) workflow. Please review carefully before merging." \
174+
--head "$BRANCH" \
175+
--label "auto-fix"
176+
177+
# 11. Summary
178+
- name: Write job summary
179+
if: always()
180+
env:
181+
NO_ISSUE: ${{ steps.pick.outputs.no_issue }}
182+
ISSUE_NUMBER: ${{ steps.pick.outputs.issue_number }}
183+
ISSUE_TITLE: ${{ steps.pick.outputs.issue_title }}
184+
BRANCH: ${{ steps.pick.outputs.branch }}
185+
COMMITS_AHEAD: ${{ steps.check_commit.outputs.commits_ahead }}
186+
run: |
187+
if [ "$NO_ISSUE" = "true" ]; then
188+
echo "## ✅ No candidates found" >> "$GITHUB_STEP_SUMMARY"
189+
echo "No open issues without an existing PR were found." >> "$GITHUB_STEP_SUMMARY"
190+
elif [ "${COMMITS_AHEAD:-0}" = "0" ]; then
191+
echo "## ⚠️ Could not fix issue #${ISSUE_NUMBER}" >> "$GITHUB_STEP_SUMMARY"
192+
echo "**${ISSUE_TITLE}** — Claude Code was unable to produce a commit." >> "$GITHUB_STEP_SUMMARY"
193+
else
194+
echo "## 🚀 PR opened for issue #${ISSUE_NUMBER}" >> "$GITHUB_STEP_SUMMARY"
195+
echo "**${ISSUE_TITLE}** — branch: \`${BRANCH}\`" >> "$GITHUB_STEP_SUMMARY"
196+
fi

CLAUDE.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# CLAUDE.md
2+
3+
Project-specific conventions for `rector/rector-src`. See `CONTRIBUTING.md` for the human-facing version.
4+
5+
## Project
6+
7+
- PHP `^8.3` required. Do not use syntax that breaks on 8.3.
8+
- The package is `rector/rector-src`; it `replace`s `rector/rector`.
9+
- Sibling extension packages (`rector-doctrine`, `rector-symfony`, `rector-phpunit`, `rector-downgrade-php`) are pulled in as `dev-main`.
10+
11+
## Layout
12+
13+
- `src/` — core engine (`Rector\` namespace).
14+
- `rules/` — built-in Rector rules, also under `Rector\` namespace (PSR-4 maps both `src/` and `rules/` to `Rector\`).
15+
- `rules-tests/` — tests for `rules/`, namespace `Rector\Tests\`.
16+
- `tests/` — tests for `src/`, same `Rector\Tests\` namespace.
17+
- `utils/` + `utils-tests/` — internal dev tooling (`Rector\Utils\`).
18+
- `config/` — config sets/presets (kept as plain class-string literals; do not let Rector rewrite them).
19+
- `build/target-repository/docs` — documentation lives here, not in repo root.
20+
21+
## Coding style
22+
23+
- `declare(strict_types=1);` at the top of every PHP file.
24+
- Classes are `final` by default; `abstract` only when explicitly intended for extension.
25+
- Constructor property promotion with `private readonly` for dependencies.
26+
- Run `composer fix-cs` (ECS) before committing; the ruleset is symplify + common + psr12.
27+
- Do not add `@author`, `@since`, or change/`@var` tags that ECS would strip.
28+
- No emojis in source.
29+
30+
## Quality gates
31+
32+
Match what `composer complete-check` runs:
33+
34+
```bash
35+
composer check-cs # ECS, read-only
36+
composer phpstan # PHPStan level 8, 512M
37+
vendor/bin/phpunit
38+
```
39+
40+
PHPStan extras enabled in `phpstan.neon`:
41+
- `type-perfect`: `no_mixed`, `null_over_false`, `narrow_param`, `narrow_return` — return/param types must be narrow; prefer `null` over `false` for "no result".
42+
- `unused-public`: public methods/properties/constants must be used somewhere. If you add a new public API, expect to use it or mark it accordingly.
43+
- `symplify/phpstan-rules` + `rector-rules`: forbids `var_dump`, `dd`, `property_exists`, `class_exists`, `@` error suppression, dynamic names, etc., outside the narrowly listed exceptions in `phpstan.neon`. Do not add new exceptions casually — fix the code.
44+
45+
Rector applies to its own source: `composer rector` runs the config in `rector.php`.
46+
47+
## Writing a Rector rule
48+
49+
Required shape (see `rules/Php85/Rector/FuncCall/OrdSingleByteRector.php` as a canonical example):
50+
51+
1. Namespace mirrors the path: `Rector\<Category>\Rector\<NodeType>\<RuleName>`.
52+
2. `final class` extends `Rector\Rector\AbstractRector`.
53+
3. Implement `MinPhpVersionInterface` when the rule targets a specific PHP version; return a `PhpVersionFeature::*` constant from `provideMinPhpVersion()`.
54+
4. Implement three methods:
55+
- `getRuleDefinition(): RuleDefinition` — one-line description + at least one `CodeSample` (before/after).
56+
- `getNodeTypes(): array` — list of `PhpParser\Node\...` classes to subscribe to.
57+
- `refactor(Node $node): ?Node` — return the new node, `null` for no change, or `NodeVisitor::REMOVE_NODE` to delete. Do **not** return integer values except `REMOVE_NODE` (see `rector.noIntegerRefactorReturn`).
58+
5. Add a `@see` PHPDoc pointing to the test class: `@see \Rector\Tests\<...>\<RuleName>Test`.
59+
6. Inject services via constructor promotion (`ValueResolver`, etc.); reuse what `AbstractRector` already exposes (`$this->nodeNameResolver`, `$this->nodeTypeResolver`, `$this->nodeFactory`, `$this->nodeComparator`).
60+
7. Bail out early: check `isFirstClassCallable()`, name match, arg presence, type, **then** transform.
61+
62+
## Tests for a Rector rule
63+
64+
Mirror the rule path under `rules-tests/`:
65+
66+
```
67+
rules-tests/<Category>/Rector/<NodeType>/<RuleName>/
68+
├── <RuleName>Test.php
69+
├── Fixture/
70+
│ ├── some_case.php.inc
71+
│ └── skip_some_case.php.inc
72+
└── config/
73+
└── configured_rule.php
74+
```
75+
76+
- Test class extends `Rector\Testing\PHPUnit\AbstractRectorTestCase`, uses `#[DataProvider('provideData')]`, and returns `self::yieldFilesFromDirectory(__DIR__ . '/Fixture')`.
77+
- `provideConfigFilePath()` returns the config file that registers the rule and pins `phpVersion(PhpVersion::PHP_XX)` when version-bound.
78+
- Fixtures use the `.php.inc` extension. Before/after are separated by a line containing exactly `-----`. A fixture with **no** `-----` separator asserts the file is unchanged; name those `skip_*.php.inc`.
79+
- The fixture's `namespace` must match its directory.
80+
- `Fixture/`, `Source/`, `Expected/` directories are auto-skipped by ECS, PHPStan, and Rector — don't try to make them conformant.
81+
82+
## What not to do
83+
84+
- Don't introduce new abstractions, traits, or helpers beyond what the task needs — the existing `AbstractRector` already exposes most node helpers.
85+
- Don't modify `phpstan.neon` ignore lists or `ecs.php` skip lists to silence a new warning; fix the underlying code instead.
86+
- Don't add `class_exists`/`property_exists`/`function_exists` runtime checks — use `ReflectionProvider` (errors from `symplify/phpstan-rules` will reject the PR).
87+
- Don't bypass `instanceof` rules by adding a new ignore; only existing `Skipper`/internal paths are allowed.
88+
- Don't touch files under `config/` with code-style rewrites — class strings there are intentional.
89+
- Don't push docs into repo root — they belong under `build/target-repository/docs`.
90+
91+
## CI parity
92+
93+
`.github/workflows/code_analysis.yaml`, `tests.yaml`, `rector.yaml`, `e2e*.yaml`, and `phpstan_printer_test.yaml` mirror the local `composer complete-check`. If it passes locally with `composer complete-check && composer rector`, CI usually agrees.

0 commit comments

Comments
 (0)