|
| 1 | +name: PR Fixer |
| 2 | + |
| 3 | +on: |
| 4 | + # Immediate: when a PR gets the agent-managed label |
| 5 | + pull_request: |
| 6 | + types: [labeled] |
| 7 | + |
| 8 | + # Immediate: when someone comments @ambient-fix on a PR |
| 9 | + issue_comment: |
| 10 | + types: [created] |
| 11 | + |
| 12 | + # Cadence: every 4 hours including overnight |
| 13 | + schedule: |
| 14 | + - cron: '0 */4 * * 1-5' |
| 15 | + |
| 16 | + # Manual: for one-off fixes |
| 17 | + workflow_dispatch: |
| 18 | + inputs: |
| 19 | + pr_number: |
| 20 | + description: 'PR number to fix' |
| 21 | + required: true |
| 22 | + type: number |
| 23 | + |
| 24 | +permissions: |
| 25 | + contents: read |
| 26 | + pull-requests: read |
| 27 | + |
| 28 | +jobs: |
| 29 | + # -- Single PR: triggered by label, comment, or manual dispatch -- |
| 30 | + fix-single: |
| 31 | + if: >- |
| 32 | + (github.event_name == 'pull_request' |
| 33 | + && github.event.label.name == 'agent-managed' |
| 34 | + && !github.event.pull_request.head.repo.fork) |
| 35 | + || github.event_name == 'workflow_dispatch' |
| 36 | + || (github.event_name == 'issue_comment' |
| 37 | + && github.event.issue.pull_request |
| 38 | + && contains(github.event.comment.body, '@ambient-fix') |
| 39 | + && (github.event.comment.author_association == 'MEMBER' |
| 40 | + || github.event.comment.author_association == 'OWNER' |
| 41 | + || github.event.comment.author_association == 'COLLABORATOR')) |
| 42 | + runs-on: ubuntu-latest |
| 43 | + timeout-minutes: 30 |
| 44 | + steps: |
| 45 | + - name: Resolve PR number |
| 46 | + id: pr |
| 47 | + run: | |
| 48 | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then |
| 49 | + echo "number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT |
| 50 | + elif [ "${{ github.event_name }}" = "issue_comment" ]; then |
| 51 | + echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT |
| 52 | + else |
| 53 | + echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT |
| 54 | + fi |
| 55 | +
|
| 56 | + - name: Check PR is not a fork (issue_comment) |
| 57 | + if: github.event_name == 'issue_comment' |
| 58 | + id: fork_check |
| 59 | + env: |
| 60 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 61 | + run: | |
| 62 | + IS_FORK=$(gh pr view ${{ steps.pr.outputs.number }} --repo "${{ github.repository }}" --json isCrossRepository --jq '.isCrossRepository') |
| 63 | + if [ "$IS_FORK" = "true" ]; then |
| 64 | + echo "Skipping fork PR" |
| 65 | + echo "skip=true" >> $GITHUB_OUTPUT |
| 66 | + else |
| 67 | + echo "skip=false" >> $GITHUB_OUTPUT |
| 68 | + fi |
| 69 | +
|
| 70 | + - name: Fix PR |
| 71 | + if: steps.fork_check.outputs.skip != 'true' |
| 72 | + id: session |
| 73 | + uses: ambient-code/ambient-action@v0.0.2 |
| 74 | + with: |
| 75 | + api-url: ${{ secrets.AMBIENT_API_URL }} |
| 76 | + api-token: ${{ secrets.AMBIENT_BOT_TOKEN }} |
| 77 | + project: ${{ secrets.AMBIENT_PROJECT }} |
| 78 | + prompt: >- |
| 79 | + Fix PR #${{ steps.pr.outputs.number }} in https://github.com/${{ github.repository }}. |
| 80 | + Fetch all PR data, rebase to resolve conflicts, evaluate reviewer |
| 81 | + comments (fix valid issues, respond to invalid ones), run lints |
| 82 | + and tests, and push the fixes. |
| 83 | + repos: >- |
| 84 | + [{"url": "https://github.com/${{ github.repository }}", "branch": "main"}] |
| 85 | + workflow: >- |
| 86 | + {"gitUrl": "https://github.com/ambient-code/workflows", "branch": "main", "path": "internal-workflows/pr-fixer"} |
| 87 | + model: claude-sonnet-4-5 |
| 88 | + wait: 'true' |
| 89 | + timeout: '25' |
| 90 | + |
| 91 | + - name: Session summary |
| 92 | + if: always() && steps.fork_check.outputs.skip != 'true' |
| 93 | + env: |
| 94 | + SESSION_NAME: ${{ steps.session.outputs.session-name }} |
| 95 | + SESSION_UID: ${{ steps.session.outputs.session-uid }} |
| 96 | + SESSION_PHASE: ${{ steps.session.outputs.session-phase }} |
| 97 | + run: | |
| 98 | + echo "### PR Fixer — #${{ steps.pr.outputs.number }}" >> $GITHUB_STEP_SUMMARY |
| 99 | + echo "" >> $GITHUB_STEP_SUMMARY |
| 100 | + if [ -n "$SESSION_NAME" ]; then |
| 101 | + echo "- **Session**: \`$SESSION_NAME\`" >> $GITHUB_STEP_SUMMARY |
| 102 | + echo "- **UID**: \`$SESSION_UID\`" >> $GITHUB_STEP_SUMMARY |
| 103 | + echo "- **Phase**: \`$SESSION_PHASE\`" >> $GITHUB_STEP_SUMMARY |
| 104 | + else |
| 105 | + echo "- **Status**: Failed to create session" >> $GITHUB_STEP_SUMMARY |
| 106 | + fi |
| 107 | +
|
| 108 | + # -- Batch: scheduled cadence for all agent-managed PRs -- |
| 109 | + fix-batch: |
| 110 | + if: github.event_name == 'schedule' |
| 111 | + runs-on: ubuntu-latest |
| 112 | + timeout-minutes: 10 |
| 113 | + outputs: |
| 114 | + pr_numbers: ${{ steps.find.outputs.pr_numbers }} |
| 115 | + steps: |
| 116 | + - name: Find agent-managed PRs that need work |
| 117 | + id: find |
| 118 | + env: |
| 119 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 120 | + run: | |
| 121 | + # Get all open PRs with agent-managed label, including last committer |
| 122 | + PRS=$(gh pr list --repo "${{ github.repository }}" \ |
| 123 | + --label "agent-managed" \ |
| 124 | + --state open \ |
| 125 | + --json number,updatedAt,isCrossRepository \ |
| 126 | + --limit 20) |
| 127 | +
|
| 128 | + # Filter: updated in the last 4 hours, not a fork, |
| 129 | + # and last update wasn't by the fixer bot itself |
| 130 | + NEEDS_WORK=$(echo "$PRS" | python3 -c " |
| 131 | + import json, sys |
| 132 | + from datetime import datetime, timezone, timedelta |
| 133 | +
|
| 134 | + prs = json.load(sys.stdin) |
| 135 | + cutoff = datetime.now(timezone.utc) - timedelta(hours=4) |
| 136 | + active = [] |
| 137 | + for pr in prs: |
| 138 | + # Skip fork PRs |
| 139 | + if pr.get('isCrossRepository', False): |
| 140 | + continue |
| 141 | + updated = pr.get('updatedAt', '') |
| 142 | + if updated: |
| 143 | + try: |
| 144 | + dt = datetime.fromisoformat(updated.replace('Z', '+00:00')) |
| 145 | + if dt > cutoff: |
| 146 | + active.append(pr['number']) |
| 147 | + except Exception: |
| 148 | + active.append(pr['number']) |
| 149 | + print(json.dumps(active)) |
| 150 | + ") |
| 151 | +
|
| 152 | + echo "pr_numbers=$NEEDS_WORK" >> $GITHUB_OUTPUT |
| 153 | + COUNT=$(echo "$NEEDS_WORK" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))") |
| 154 | + echo "Found $COUNT agent-managed PRs with recent activity" |
| 155 | +
|
| 156 | + fix-each: |
| 157 | + needs: fix-batch |
| 158 | + if: needs.fix-batch.outputs.pr_numbers != '[]' |
| 159 | + runs-on: ubuntu-latest |
| 160 | + timeout-minutes: 30 |
| 161 | + strategy: |
| 162 | + matrix: |
| 163 | + pr_number: ${{ fromJson(needs.fix-batch.outputs.pr_numbers) }} |
| 164 | + steps: |
| 165 | + - name: Skip if last update was by the fixer |
| 166 | + id: churn_check |
| 167 | + env: |
| 168 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 169 | + run: | |
| 170 | + # Check if the most recent comment is from the fixer bot |
| 171 | + LAST_AUTHOR=$(gh api "repos/${{ github.repository }}/issues/${{ matrix.pr_number }}/comments" \ |
| 172 | + --jq 'last | .body // ""' 2>/dev/null || echo "") |
| 173 | + if echo "$LAST_AUTHOR" | grep -q '<!-- pr-fixer-bot -->'; then |
| 174 | + echo "Last activity was fixer bot — skipping" |
| 175 | + echo "skip=true" >> $GITHUB_OUTPUT |
| 176 | + else |
| 177 | + echo "skip=false" >> $GITHUB_OUTPUT |
| 178 | + fi |
| 179 | +
|
| 180 | + - name: Fix PR #${{ matrix.pr_number }} |
| 181 | + if: steps.churn_check.outputs.skip != 'true' |
| 182 | + id: session |
| 183 | + uses: ambient-code/ambient-action@v0.0.2 |
| 184 | + with: |
| 185 | + api-url: ${{ secrets.AMBIENT_API_URL }} |
| 186 | + api-token: ${{ secrets.AMBIENT_BOT_TOKEN }} |
| 187 | + project: ${{ secrets.AMBIENT_PROJECT }} |
| 188 | + prompt: >- |
| 189 | + Fix PR #${{ matrix.pr_number }} in https://github.com/${{ github.repository }}. |
| 190 | + Fetch all PR data, rebase to resolve conflicts, evaluate reviewer |
| 191 | + comments (fix valid issues, respond to invalid ones), run lints |
| 192 | + and tests, and push the fixes. |
| 193 | + repos: >- |
| 194 | + [{"url": "https://github.com/${{ github.repository }}", "branch": "main"}] |
| 195 | + workflow: >- |
| 196 | + {"gitUrl": "https://github.com/ambient-code/workflows", "branch": "main", "path": "internal-workflows/pr-fixer"} |
| 197 | + model: claude-sonnet-4-5 |
| 198 | + wait: 'false' |
| 199 | + |
| 200 | + - name: Session summary |
| 201 | + if: always() && steps.churn_check.outputs.skip != 'true' |
| 202 | + env: |
| 203 | + SESSION_NAME: ${{ steps.session.outputs.session-name }} |
| 204 | + SESSION_UID: ${{ steps.session.outputs.session-uid }} |
| 205 | + SESSION_PHASE: ${{ steps.session.outputs.session-phase }} |
| 206 | + run: | |
| 207 | + echo "### PR Fixer — #${{ matrix.pr_number }}" >> $GITHUB_STEP_SUMMARY |
| 208 | + echo "" >> $GITHUB_STEP_SUMMARY |
| 209 | + if [ -n "$SESSION_NAME" ]; then |
| 210 | + echo "- **Session**: \`$SESSION_NAME\`" >> $GITHUB_STEP_SUMMARY |
| 211 | + echo "- **UID**: \`$SESSION_UID\`" >> $GITHUB_STEP_SUMMARY |
| 212 | + echo "- **Phase**: \`$SESSION_PHASE\`" >> $GITHUB_STEP_SUMMARY |
| 213 | + else |
| 214 | + echo "- **Status**: Failed to create session" >> $GITHUB_STEP_SUMMARY |
| 215 | + fi |
0 commit comments