diff --git a/.gitattributes b/.gitattributes index ed98b8a4c8..ba520f3418 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,5 @@ *.stub linguist-language=PHP tests/PHPStan/Command/ErrorFormatter/data/WindowsNewlines.php eol=crlf + +.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file diff --git a/.github/workflows/backward-compatibility.yml b/.github/workflows/backward-compatibility.yml index f928b68c51..cb5ff5f0fa 100644 --- a/.github/workflows/backward-compatibility.yml +++ b/.github/workflows/backward-compatibility.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "2.1.x" + - "2.2.x" paths: - 'src/**' - '.github/workflows/backward-compatibility.yml' diff --git a/.github/workflows/build-issue-bot.yml b/.github/workflows/build-issue-bot.yml index 899c5b9b09..f634d4e975 100644 --- a/.github/workflows/build-issue-bot.yml +++ b/.github/workflows/build-issue-bot.yml @@ -9,7 +9,7 @@ on: - '.github/workflows/build-issue-bot.yml' push: branches: - - "2.1.x" + - "2.2.x" paths: - 'issue-bot/**' - '.github/workflows/build-issue-bot.yml' diff --git a/.github/workflows/changelog-generator.yml b/.github/workflows/changelog-generator.yml index 19e406007f..2dd24c145b 100644 --- a/.github/workflows/changelog-generator.yml +++ b/.github/workflows/changelog-generator.yml @@ -9,7 +9,7 @@ on: - '.github/workflows/changelog-generator.yml' push: branches: - - "2.1.x" + - "2.2.x" paths: - 'changelog-generator/**' - '.github/workflows/changelog-generator.yml' diff --git a/.github/workflows/claude-fix-issue.yml b/.github/workflows/claude-fix-issue.yml new file mode 100644 index 0000000000..dc81ad55e0 --- /dev/null +++ b/.github/workflows/claude-fix-issue.yml @@ -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 + 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 + repository: phpstan/phpstan-src + fetch-depth: 0 + + - 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) + + TITLE=$(echo "$ISSUE_JSON" | jq -r '.title') + 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" + 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 }} + URL: ${{ steps.issue.outputs.url }} + + 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 }} + + These files are critical - they will be used for the commit message and PR description. + PROMPT_EOF + )" + + - name: "Read Claude's summary" + 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}" + } >> "$GITHUB_OUTPUT" + else + echo "commit_message=Fix #$ISSUE_NUMBER" >> "$GITHUB_OUTPUT" + 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 + + - 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 #${{ inputs.issue-number }}: ${{ steps.issue.outputs.title }}" + body: ${{ steps.claude-summary.outputs.pr_body }} + committer: "phpstan-bot " + commit-message: ${{ steps.claude-summary.outputs.commit_message }} diff --git a/.github/workflows/claude-random-easy-fixes-scheduled.yml b/.github/workflows/claude-random-easy-fixes-scheduled.yml new file mode 100644 index 0000000000..86c720b917 --- /dev/null +++ b/.github/workflows/claude-random-easy-fixes-scheduled.yml @@ -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: + runs-on: ubuntu-latest + permissions: + contents: read + actions: write + 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 }} diff --git a/.github/workflows/claude-random-easy-fixes.yml b/.github/workflows/claude-random-easy-fixes.yml new file mode 100644 index 0000000000..7ab8186735 --- /dev/null +++ b/.github/workflows/claude-random-easy-fixes.yml @@ -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 + + 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" + + 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 + with: + issue-number: ${{ matrix.issue.number }} + secrets: inherit diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 9e8ca391b0..7a378cbb30 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -11,7 +11,7 @@ on: - 'issue-bot/**' push: branches: - - "2.1.x" + - "2.2.x" paths-ignore: - 'compiler/**' - 'apigen/**' diff --git a/.github/workflows/lint-workflows.yml b/.github/workflows/lint-workflows.yml index 5dd4964d5e..94b083a9da 100644 --- a/.github/workflows/lint-workflows.yml +++ b/.github/workflows/lint-workflows.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "2.1.x" + - "2.2.x" permissions: {} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0dcd57fc11..a48cc8087d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "2.1.x" + - "2.2.x" concurrency: group: lint-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index abc8cc9416..9813250201 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -6,9 +6,9 @@ on: pull_request: push: branches: - - "2.1.x" + - "2.2.x" tags: - - '2.1.*' + - '2.2.*' concurrency: group: phar-${{ github.ref }} # will be canceled on subsequent pushes in both branches and pull requests @@ -95,14 +95,14 @@ jobs: - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 env: - COMPOSER_ROOT_VERSION: "2.1.x-dev" + COMPOSER_ROOT_VERSION: "2.2.x-dev" - name: "Compile PHAR for checksum" working-directory: "compiler/build" run: "php ../box/vendor/bin/box compile --no-parallel --sort-compiled-files" env: PHAR_CHECKSUM: "1" - COMPOSER_ROOT_VERSION: "2.1.x-dev" + COMPOSER_ROOT_VERSION: "2.2.x-dev" - name: "Re-sign PHAR" run: "php compiler/build/resign.php tmp/phpstan.phar" @@ -134,25 +134,25 @@ jobs: integration-tests: if: github.event_name == 'pull_request' needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/integration-tests.yml@2.1.x + uses: phpstan/phpstan/.github/workflows/integration-tests.yml@2.2.x with: - ref: 2.1.x + ref: 2.2.x phar-checksum: ${{needs.compiler-tests.outputs.checksum}} extension-tests: if: github.event_name == 'pull_request' needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/extension-tests.yml@2.1.x + uses: phpstan/phpstan/.github/workflows/extension-tests.yml@2.2.x with: - ref: 2.1.x + ref: 2.2.x phar-checksum: ${{needs.compiler-tests.outputs.checksum}} other-tests: if: github.event_name == 'pull_request' needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/other-tests.yml@2.1.x + uses: phpstan/phpstan/.github/workflows/other-tests.yml@2.2.x with: - ref: 2.1.x + ref: 2.2.x phar-checksum: ${{needs.compiler-tests.outputs.checksum}} download-base-sha-phar: @@ -298,7 +298,7 @@ jobs: commit: name: "Commit PHAR" - if: "github.repository_owner == 'phpstan' && (github.ref == 'refs/heads/2.1.x' || startsWith(github.ref, 'refs/tags/'))" + if: "github.repository_owner == 'phpstan' && (github.ref == 'refs/heads/2.2.x' || startsWith(github.ref, 'refs/tags/'))" needs: compiler-tests runs-on: "ubuntu-latest" timeout-minutes: 60 @@ -325,7 +325,7 @@ jobs: repository: phpstan/phpstan path: phpstan-dist token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - ref: 2.1.x + ref: 2.2.x - name: "Get previous pushed dist commit" id: previous-commit diff --git a/.github/workflows/reflection-golden-test.yml b/.github/workflows/reflection-golden-test.yml index faa9329cc6..4ca96ccde2 100644 --- a/.github/workflows/reflection-golden-test.yml +++ b/.github/workflows/reflection-golden-test.yml @@ -11,7 +11,7 @@ on: - 'issue-bot/**' push: branches: - - "2.1.x" + - "2.2.x" paths-ignore: - 'compiler/**' - 'apigen/**' diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml index a8c84e0981..442cd7347e 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "2.1.x" + - "2.2.x" permissions: contents: read diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 8162d58468..2f79a0d337 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -9,7 +9,7 @@ on: - 'apigen/**' push: branches: - - "2.1.x" + - "2.2.x" paths-ignore: - 'compiler/**' - 'apigen/**' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 80474700d4..f70f9c0c32 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ on: - 'issue-bot/**' push: branches: - - "2.1.x" + - "2.2.x" paths-ignore: - 'compiler/**' - 'apigen/**'