77 types : [opened]
88
99jobs :
10- # First job runs on GitHub-hosted runner to set pending status immediately
11- setup :
12- # Run on PR comments with commands OR PR creation with commands in body
10+ pr-automation :
1311 if : |
1412 (github.event_name == 'issue_comment' &&
1513 github.event.issue.pull_request &&
@@ -20,317 +18,9 @@ jobs:
2018 (contains(github.event.pull_request.body, '[action]') ||
2119 contains(github.event.pull_request.body, '[fix]') ||
2220 contains(github.event.pull_request.body, '[debug]')))
23-
24- runs-on : ubuntu-latest
25-
26- permissions :
27- statuses : write
28- pull-requests : read
29-
30- outputs :
31- branch : ${{ steps.pr.outputs.branch }}
32- sha : ${{ steps.pr.outputs.sha }}
33- pr_number : ${{ steps.pr.outputs.pr_number }}
34- command : ${{ steps.pr.outputs.command }}
35- instructions : ${{ steps.pr.outputs.instructions }}
36-
37- steps :
38- - name : Get PR details and set pending status
39- id : pr
40- uses : actions/github-script@v7
41- with :
42- script : |
43- let prData, body, prNumber;
44-
45- if (context.eventName === 'pull_request') {
46- // PR creation event - data is directly available
47- prData = context.payload.pull_request;
48- body = prData.body || '';
49- prNumber = prData.number;
50- } else {
51- // Comment event - need to fetch PR data
52- const pr = await github.rest.pulls.get({
53- owner: context.repo.owner,
54- repo: context.repo.repo,
55- pull_number: context.issue.number
56- });
57- prData = pr.data;
58- body = context.payload.comment.body;
59- prNumber = context.issue.number;
60- }
61-
62- const sha = prData.head.sha;
63- core.setOutput('branch', prData.head.ref);
64- core.setOutput('sha', sha);
65- core.setOutput('pr_number', prNumber);
66-
67- // Extract command - for PR body, search anywhere; for comments, must start with command
68- let command = 'unknown';
69- let instructions = '';
70-
71- if (context.eventName === 'pull_request') {
72- // For PR body, find command anywhere in text
73- if (body.includes('[action]')) {
74- command = 'action';
75- const match = body.match(/\[action\](.*?)(?=\[(?:action|fix|debug)\]|$)/s);
76- instructions = match ? match[1].trim() : '';
77- } else if (body.includes('[fix]')) {
78- command = 'fix';
79- const match = body.match(/\[fix\](.*?)(?=\[(?:action|fix|debug)\]|$)/s);
80- instructions = match ? match[1].trim() : '';
81- } else if (body.includes('[debug]')) {
82- command = 'debug';
83- }
84- } else {
85- // For comments, must start with command
86- if (body.startsWith('[action]')) {
87- command = 'action';
88- instructions = body.slice('[action]'.length).trim();
89- } else if (body.startsWith('[fix]')) {
90- command = 'fix';
91- instructions = body.slice('[fix]'.length).trim();
92- } else if (body.startsWith('[debug]')) {
93- command = 'debug';
94- }
95- }
96-
97- core.setOutput('command', command);
98- core.setOutput('instructions', instructions);
99-
100- // Set pending status immediately
101- await github.rest.repos.createCommitStatus({
102- owner: context.repo.owner,
103- repo: context.repo.repo,
104- sha: sha,
105- state: 'pending',
106- context: `PR Automation / ${command}`,
107- description: 'Waiting for runner...',
108- target_url: `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
109- });
110-
111- execute :
112- needs : setup
113- runs-on : ${{ vars.RUNNER_TYPE || 'ubuntu-latest' }}
114-
21+ uses : GiggleLiu/pr-resolver/.github/workflows/pr-automation.yml@main
22+ secrets : inherit
11523 permissions :
11624 contents : write
11725 pull-requests : write
11826 statuses : write
119-
120- steps :
121- - name : Update status to running
122- uses : actions/github-script@v7
123- with :
124- script : |
125- await github.rest.repos.createCommitStatus({
126- owner: context.repo.owner,
127- repo: context.repo.repo,
128- sha: '${{ needs.setup.outputs.sha }}',
129- state: 'pending',
130- context: 'PR Automation / ${{ needs.setup.outputs.command }}',
131- description: 'Running...',
132- target_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
133- });
134-
135- - name : Checkout PR branch
136- uses : actions/checkout@v4
137- with :
138- ref : ${{ needs.setup.outputs.branch }}
139- fetch-depth : 0
140-
141- - name : Setup Node.js (GitHub-hosted only)
142- if : ${{ vars.RUNNER_TYPE != 'self-hosted' }}
143- uses : actions/setup-node@v4
144- with :
145- node-version : ' 20'
146-
147- - name : Install Claude Code CLI (GitHub-hosted only)
148- if : ${{ vars.RUNNER_TYPE != 'self-hosted' }}
149- run : npm install -g @anthropic-ai/claude-code
150-
151- - name : Install superpowers plugin (GitHub-hosted only)
152- if : ${{ vars.RUNNER_TYPE != 'self-hosted' }}
153- run : claude plugin install anthropics/claude-code-superpowers
154-
155- - name : Find plan file
156- id : plan
157- if : needs.setup.outputs.command == 'action'
158- run : |
159- for f in PLAN.md plan.md .claude/plan.md docs/plan.md; do
160- [ -f "$f" ] && echo "file=$f" >> $GITHUB_OUTPUT && exit 0
161- done
162- latest=$(ls -t docs/plans/*.md 2>/dev/null | head -1)
163- [ -n "$latest" ] && echo "file=$latest" >> $GITHUB_OUTPUT && exit 0
164- echo "No plan file found" && exit 1
165-
166- - name : Execute plan
167- if : needs.setup.outputs.command == 'action'
168- env :
169- ANTHROPIC_API_KEY_SECRET : ${{ secrets.ANTHROPIC_API_KEY }}
170- GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
171- run : |
172- set -eo pipefail
173- # Use secret if set, otherwise use runner's env (or OAuth from keychain)
174- [ -n "$ANTHROPIC_API_KEY_SECRET" ] && export ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY_SECRET"
175-
176- # For GitHub-hosted runners, API key is required
177- # For self-hosted runners, OAuth from keychain is also supported
178- if [ -z "$ANTHROPIC_API_KEY" ] && [ "${{ vars.RUNNER_TYPE }}" != "self-hosted" ]; then
179- echo "Error: ANTHROPIC_API_KEY not set (required for GitHub-hosted runners)" | tee claude-output.txt
180- exit 1
181- fi
182-
183- INSTRUCTIONS='${{ needs.setup.outputs.instructions }}'
184- claude --dangerously-skip-permissions --max-turns 100 -p "
185- Execute the plan in '${{ steps.plan.outputs.file }}'.
186- ${INSTRUCTIONS:+
187- ## Additional Instructions
188- $INSTRUCTIONS
189- }
190- ## Process
191- 1. Read the plan file
192- 2. Use /subagent-driven-development to execute tasks
193- 3. Push: git push origin ${{ needs.setup.outputs.branch }}
194- 4. Post summary: gh pr comment ${{ needs.setup.outputs.pr_number }} --repo ${{ github.repository }} --body 'SUMMARY'
195-
196- ## Rules
197- - Tests should be strong enough to catch regressions.
198- - Do not modify tests to make them pass.
199- - Test failure must be reported.
200- " 2>&1 | tee claude-output.txt
201-
202- # Check for authentication errors in output
203- if grep -qiE "authenticat(e|ion)|unauthorized|forbidden|invalid.*key|api.*key.*invalid|API Error: 40[13]" claude-output.txt; then
204- echo "Error: Authentication failure detected"
205- exit 1
206- fi
207-
208- # Check for max turns exhaustion
209- if grep -q "Reached max turns" claude-output.txt; then
210- echo "Error: Claude exhausted max turns without completing"
211- exit 1
212- fi
213-
214- - name : Fix issues
215- if : needs.setup.outputs.command == 'fix'
216- env :
217- ANTHROPIC_API_KEY_SECRET : ${{ secrets.ANTHROPIC_API_KEY }}
218- GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
219- run : |
220- set -eo pipefail
221- # Use secret if set, otherwise use runner's env (or OAuth from keychain)
222- [ -n "$ANTHROPIC_API_KEY_SECRET" ] && export ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY_SECRET"
223-
224- # For GitHub-hosted runners, API key is required
225- # For self-hosted runners, OAuth from keychain is also supported
226- if [ -z "$ANTHROPIC_API_KEY" ] && [ "${{ vars.RUNNER_TYPE }}" != "self-hosted" ]; then
227- echo "Error: ANTHROPIC_API_KEY not set (required for GitHub-hosted runners)" | tee claude-output.txt
228- exit 1
229- fi
230-
231- # Gather all feedback sources
232- INLINE=$(gh api repos/${{ github.repository }}/pulls/${{ needs.setup.outputs.pr_number }}/comments --jq '.[].body' 2>/dev/null || echo "")
233- REVIEWS=$(gh api repos/${{ github.repository }}/pulls/${{ needs.setup.outputs.pr_number }}/reviews --jq '.[] | select(.body != "") | .body' 2>/dev/null || echo "")
234- PR_COMMENTS=$(gh api repos/${{ github.repository }}/issues/${{ needs.setup.outputs.pr_number }}/comments --jq '.[].body' 2>/dev/null || echo "")
235-
236- # Gather CI status
237- CI_STATUS=$(gh pr checks ${{ needs.setup.outputs.pr_number }} --repo ${{ github.repository }} 2>/dev/null || echo "")
238- FAILED_RUNS=$(gh run list --branch ${{ needs.setup.outputs.branch }} --status failure --limit 3 --json databaseId,name --jq '.[] | "\(.databaseId) \(.name)"' 2>/dev/null || echo "")
239-
240- # Get failed run logs if any
241- CI_LOGS=""
242- for run_id in $(echo "$FAILED_RUNS" | cut -d' ' -f1); do
243- [ -n "$run_id" ] && CI_LOGS="$CI_LOGS
244- --- Run $run_id ---
245- $(gh run view $run_id --log-failed 2>/dev/null | tail -100 || echo 'Could not fetch logs')"
246- done
247-
248- INSTRUCTIONS='${{ needs.setup.outputs.instructions }}'
249- claude --dangerously-skip-permissions --max-turns 100 -p "
250- Fix all issues with this PR: review comments AND CI failures.
251- ${INSTRUCTIONS:+
252- ## Additional Instructions
253- $INSTRUCTIONS
254- }
255- === Inline Code Comments ===
256- $INLINE
257-
258- === PR Reviews ===
259- $REVIEWS
260-
261- === PR Conversation ===
262- $PR_COMMENTS
263-
264- === CI Status ===
265- $CI_STATUS
266-
267- === Failed CI Logs (last 100 lines each) ===
268- $CI_LOGS
269-
270- ## Process
271- 1. Use /systematic-debugging if CI failed
272- 2. Fix review comments and CI issues
273- 3. Run tests to verify: make test, cargo test, npm test, etc.
274- 4. Commit: git commit -am 'Fix review feedback and CI issues'
275- 5. Push: git push origin ${{ needs.setup.outputs.branch }}
276- 6. Post summary: gh pr comment ${{ needs.setup.outputs.pr_number }} --repo ${{ github.repository }} --body 'SUMMARY'
277-
278- ## Rules
279- - All CI failures must be fixed.
280- - All change requests must be either addressed or explained.
281- " 2>&1 | tee claude-output.txt
282-
283- # Check for authentication errors in output
284- if grep -qiE "authenticat(e|ion)|unauthorized|forbidden|invalid.*key|api.*key.*invalid|API Error: 40[13]" claude-output.txt; then
285- echo "Error: Authentication failure detected"
286- exit 1
287- fi
288-
289- # Check for max turns exhaustion
290- if grep -q "Reached max turns" claude-output.txt; then
291- echo "Error: Claude exhausted max turns without completing"
292- exit 1
293- fi
294-
295- - name : Debug test
296- if : needs.setup.outputs.command == 'debug'
297- run : |
298- echo "Debug test passed - workflow is working" | tee claude-output.txt
299-
300- - name : Upload logs
301- if : always()
302- uses : actions/upload-artifact@v4
303- with :
304- name : claude-output
305- path : claude-output.txt
306- retention-days : 7
307-
308- - name : Set success status
309- if : success()
310- uses : actions/github-script@v7
311- with :
312- script : |
313- await github.rest.repos.createCommitStatus({
314- owner: context.repo.owner,
315- repo: context.repo.repo,
316- sha: '${{ needs.setup.outputs.sha }}',
317- state: 'success',
318- context: 'PR Automation / ${{ needs.setup.outputs.command }}',
319- description: 'Completed successfully',
320- target_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
321- });
322-
323- - name : Set failure status
324- if : failure()
325- uses : actions/github-script@v7
326- with :
327- script : |
328- await github.rest.repos.createCommitStatus({
329- owner: context.repo.owner,
330- repo: context.repo.repo,
331- sha: '${{ needs.setup.outputs.sha }}',
332- state: 'failure',
333- context: 'PR Automation / ${{ needs.setup.outputs.command }}',
334- description: 'Failed - check logs',
335- target_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
336- });
0 commit comments