|
| 1 | +name: "Claude Random 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 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 issues" |
| 32 | + id: pick-issues |
| 33 | + env: |
| 34 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 35 | + ISSUE_COUNT: ${{ inputs.issue_count || '1' }} |
| 36 | + run: | |
| 37 | + # Fetch all milestones once |
| 38 | + MILESTONES=$(gh api "repos/phpstan/phpstan/milestones?per_page=100&state=all" \ |
| 39 | + --jq '[.[] | {title: .title, number: .number}]') |
| 40 | +
|
| 41 | + ISSUE_JSON="[]" |
| 42 | +
|
| 43 | + # Easy fixes - all issues |
| 44 | + MILESTONE_NUM=$(echo "$MILESTONES" | jq -r '.[] | select(.title == "Easy fixes") | .number') |
| 45 | + if [ -n "$MILESTONE_NUM" ]; then |
| 46 | + ISSUES=$(gh api --paginate \ |
| 47 | + "repos/phpstan/phpstan/issues?state=open&milestone=${MILESTONE_NUM}&per_page=100" \ |
| 48 | + --jq '[.[] | {number: .number, title: .title}]' \ |
| 49 | + | jq -s 'add // []') |
| 50 | + ISSUE_JSON=$(echo "[$ISSUE_JSON, $ISSUES]" | jq '.[0] + .[1]') |
| 51 | + echo "Fetched $(echo "$ISSUES" | jq 'length') issues from Easy fixes" |
| 52 | + else |
| 53 | + echo "Warning: Could not find 'Easy fixes' milestone" |
| 54 | + fi |
| 55 | +
|
| 56 | + # Dependent types - Bug label only |
| 57 | + MILESTONE_NUM=$(echo "$MILESTONES" | jq -r '.[] | select(.title == "Dependent types") | .number') |
| 58 | + if [ -n "$MILESTONE_NUM" ]; then |
| 59 | + ISSUES=$(gh api --paginate \ |
| 60 | + "repos/phpstan/phpstan/issues?state=open&milestone=${MILESTONE_NUM}&labels=Bug&per_page=100" \ |
| 61 | + --jq '[.[] | {number: .number, title: .title}]' \ |
| 62 | + | jq -s 'add // []') |
| 63 | + ISSUE_JSON=$(echo "[$ISSUE_JSON, $ISSUES]" | jq '.[0] + .[1]') |
| 64 | + echo "Fetched $(echo "$ISSUES" | jq 'length') Bug issues from Dependent types" |
| 65 | + else |
| 66 | + echo "Warning: Could not find 'Dependent types' milestone" |
| 67 | + fi |
| 68 | +
|
| 69 | + # Generics - Bug label only |
| 70 | + MILESTONE_NUM=$(echo "$MILESTONES" | jq -r '.[] | select(.title == "Generics") | .number') |
| 71 | + if [ -n "$MILESTONE_NUM" ]; then |
| 72 | + ISSUES=$(gh api --paginate \ |
| 73 | + "repos/phpstan/phpstan/issues?state=open&milestone=${MILESTONE_NUM}&labels=Bug&per_page=100" \ |
| 74 | + --jq '[.[] | {number: .number, title: .title}]' \ |
| 75 | + | jq -s 'add // []') |
| 76 | + ISSUE_JSON=$(echo "[$ISSUE_JSON, $ISSUES]" | jq '.[0] + .[1]') |
| 77 | + echo "Fetched $(echo "$ISSUES" | jq 'length') Bug issues from Generics" |
| 78 | + else |
| 79 | + echo "Warning: Could not find 'Generics' milestone" |
| 80 | + fi |
| 81 | +
|
| 82 | + # Deduplicate |
| 83 | + ISSUE_JSON=$(echo "$ISSUE_JSON" | jq 'unique_by(.number)') |
| 84 | +
|
| 85 | + TOTAL=$(echo "$ISSUE_JSON" | jq 'length') |
| 86 | + if [ "$TOTAL" -eq 0 ]; then |
| 87 | + echo "No issues found across milestones" |
| 88 | + exit 1 |
| 89 | + fi |
| 90 | +
|
| 91 | + COUNT=$ISSUE_COUNT |
| 92 | + if [ "$COUNT" -gt "$TOTAL" ]; then |
| 93 | + COUNT=$TOTAL |
| 94 | + fi |
| 95 | +
|
| 96 | + # Pick COUNT random unique issues |
| 97 | + SELECTED=$(echo "$ISSUE_JSON" | python3 -c " |
| 98 | + import json, sys, random |
| 99 | + issues = json.load(sys.stdin) |
| 100 | + random.shuffle(issues) |
| 101 | + count = min(int('$COUNT'), len(issues)) |
| 102 | + print(json.dumps(issues[:count])) |
| 103 | + ") |
| 104 | +
|
| 105 | + echo "Selected $COUNT issue(s) from $TOTAL total candidates" |
| 106 | +
|
| 107 | + for NUMBER in $(echo "$SELECTED" | jq -r '.[].number'); do |
| 108 | + TITLE=$(echo "$SELECTED" | jq -r --argjson n "$NUMBER" '.[] | select(.number == $n) | .title') |
| 109 | + echo "### Selected issue: #$NUMBER - $TITLE" >> "$GITHUB_STEP_SUMMARY" |
| 110 | + done |
| 111 | +
|
| 112 | + echo "matrix=$(echo "$SELECTED" | jq -c '.')" >> "$GITHUB_OUTPUT" |
| 113 | +
|
| 114 | + fix: |
| 115 | + name: "Fix #${{ matrix.issue.number }}: ${{ matrix.issue.title }}" |
| 116 | + needs: pick-issues |
| 117 | + strategy: |
| 118 | + fail-fast: false |
| 119 | + matrix: |
| 120 | + issue: ${{ fromJson(needs.pick-issues.outputs.matrix) }} |
| 121 | + uses: ./.github/workflows/claude-fix-issue.yml |
| 122 | + with: |
| 123 | + issue-number: ${{ matrix.issue.number }} |
| 124 | + secrets: inherit |
0 commit comments