@@ -3,50 +3,97 @@ name: PR Automation
33on :
44 issue_comment :
55 types : [created]
6+ pull_request :
7+ types : [opened]
68
79jobs :
8- execute :
9- # Only run on PR comments with recognized commands
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
1013 if : |
11- github.event.issue.pull_request &&
12- (startsWith(github.event.comment.body, '[action]') ||
13- startsWith(github.event.comment.body, '[fix]') ||
14- startsWith(github.event.comment.body, '[debug]'))
14+ (github.event_name == 'issue_comment' &&
15+ github.event.issue.pull_request &&
16+ (startsWith(github.event.comment.body, '[action]') ||
17+ startsWith(github.event.comment.body, '[fix]') ||
18+ startsWith(github.event.comment.body, '[debug]'))) ||
19+ (github.event_name == 'pull_request' &&
20+ (contains(github.event.pull_request.body, '[action]') ||
21+ contains(github.event.pull_request.body, '[fix]') ||
22+ contains(github.event.pull_request.body, '[debug]')))
1523
16- runs-on : ${{ vars.RUNNER_TYPE || ' ubuntu-latest' }}
24+ runs-on : ubuntu-latest
1725
1826 permissions :
19- contents : write
20- pull-requests : write
2127 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 }}
2236
2337 steps :
2438 - name : Get PR details and set pending status
2539 id : pr
2640 uses : actions/github-script@v7
2741 with :
2842 script : |
29- const pr = await github.rest.pulls.get({
30- owner: context.repo.owner,
31- repo: context.repo.repo,
32- pull_number: context.issue.number
33- });
34- const sha = pr.data.head.sha;
35- core.setOutput('branch', pr.data.head.ref);
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);
3664 core.setOutput('sha', sha);
65+ core.setOutput('pr_number', prNumber);
3766
38- const body = context.payload.comment. body;
67+ // Extract command - for PR body, search anywhere; for comments, must start with command
3968 let command = 'unknown';
4069 let instructions = '';
41- if (body.startsWith('[action]')) {
42- command = 'action';
43- instructions = body.slice('[action]'.length).trim();
44- } else if (body.startsWith('[fix]')) {
45- command = 'fix';
46- instructions = body.slice('[fix]'.length).trim();
47- } else if (body.startsWith('[debug]')) {
48- command = 'debug';
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+ }
4995 }
96+
5097 core.setOutput('command', command);
5198 core.setOutput('instructions', instructions);
5299
@@ -57,14 +104,38 @@ jobs:
57104 sha: sha,
58105 state: 'pending',
59106 context: `PR Automation / ${command}`,
60- description: 'Running ...',
107+ description: 'Waiting for runner ...',
61108 target_url: `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
62109 });
63110
111+ execute :
112+ needs : setup
113+ runs-on : ${{ vars.RUNNER_TYPE || 'ubuntu-latest' }}
114+
115+ permissions :
116+ contents : write
117+ pull-requests : write
118+ 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+
64135 - name : Checkout PR branch
65136 uses : actions/checkout@v4
66137 with :
67- ref : ${{ steps.pr .outputs.branch }}
138+ ref : ${{ needs.setup .outputs.branch }}
68139 fetch-depth : 0
69140
70141 - name : Setup Node.js (GitHub-hosted only)
83154
84155 - name : Find plan file
85156 id : plan
86- if : steps.pr .outputs.command == 'action'
157+ if : needs.setup .outputs.command == 'action'
87158 run : |
88159 for f in PLAN.md plan.md .claude/plan.md docs/plan.md; do
89160 [ -f "$f" ] && echo "file=$f" >> $GITHUB_OUTPUT && exit 0
@@ -93,14 +164,22 @@ jobs:
93164 echo "No plan file found" && exit 1
94165
95166 - name : Execute plan
96- if : steps.pr .outputs.command == 'action'
167+ if : needs.setup .outputs.command == 'action'
97168 env :
98169 ANTHROPIC_API_KEY_SECRET : ${{ secrets.ANTHROPIC_API_KEY }}
99170 GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
100171 run : |
172+ set -eo pipefail
101173 # Use secret if set, otherwise use runner's env
102174 [ -n "$ANTHROPIC_API_KEY_SECRET" ] && export ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY_SECRET"
103- INSTRUCTIONS='${{ steps.pr.outputs.instructions }}'
175+
176+ # Check API key is available
177+ if [ -z "$ANTHROPIC_API_KEY" ]; then
178+ echo "Error: ANTHROPIC_API_KEY not set" | tee claude-output.txt
179+ exit 1
180+ fi
181+
182+ INSTRUCTIONS='${{ needs.setup.outputs.instructions }}'
104183 claude --dangerously-skip-permissions --max-turns 100 -p "
105184 Execute the plan in '${{ steps.plan.outputs.file }}'.
106185 ${INSTRUCTIONS:+
@@ -110,32 +189,45 @@ jobs:
110189 ## Process
111190 1. Read the plan file
112191 2. Use /subagent-driven-development to execute tasks
113- 3. Push: git push origin ${{ steps.pr .outputs.branch }}
114- 4. Post summary: gh pr comment ${{ github.event.issue.number }} --repo ${{ github.repository }} --body 'SUMMARY'
192+ 3. Push: git push origin ${{ needs.setup .outputs.branch }}
193+ 4. Post summary: gh pr comment ${{ needs.setup.outputs.pr_number }} --repo ${{ github.repository }} --body 'SUMMARY'
115194
116195 ## Rules
117196 - Tests should be strong enough to catch regressions.
118197 - Do not modify tests to make them pass.
119198 - Test failure must be reported.
120199 " 2>&1 | tee claude-output.txt
121200
201+ # Check for authentication errors in output
202+ if grep -qiE "authenticat(e|ion)|unauthorized|forbidden|invalid.*key|api.*key.*invalid|API Error: 40[13]" claude-output.txt; then
203+ echo "Error: Authentication failure detected"
204+ exit 1
205+ fi
206+
122207 - name : Fix issues
123- if : steps.pr .outputs.command == 'fix'
208+ if : needs.setup .outputs.command == 'fix'
124209 env :
125210 ANTHROPIC_API_KEY_SECRET : ${{ secrets.ANTHROPIC_API_KEY }}
126211 GH_TOKEN : ${{ secrets.GITHUB_TOKEN }}
127212 run : |
213+ set -eo pipefail
128214 # Use secret if set, otherwise use runner's env
129215 [ -n "$ANTHROPIC_API_KEY_SECRET" ] && export ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY_SECRET"
130216
217+ # Check API key is available
218+ if [ -z "$ANTHROPIC_API_KEY" ]; then
219+ echo "Error: ANTHROPIC_API_KEY not set" | tee claude-output.txt
220+ exit 1
221+ fi
222+
131223 # Gather all feedback sources
132- INLINE=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}/comments --jq '.[].body' 2>/dev/null || echo "")
133- REVIEWS=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}/reviews --jq '.[] | select(.body != "") | .body' 2>/dev/null || echo "")
134- PR_COMMENTS=$(gh api repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments --jq '.[].body' 2>/dev/null || echo "")
224+ INLINE=$(gh api repos/${{ github.repository }}/pulls/${{ needs.setup.outputs.pr_number }}/comments --jq '.[].body' 2>/dev/null || echo "")
225+ REVIEWS=$(gh api repos/${{ github.repository }}/pulls/${{ needs.setup.outputs.pr_number }}/reviews --jq '.[] | select(.body != "") | .body' 2>/dev/null || echo "")
226+ PR_COMMENTS=$(gh api repos/${{ github.repository }}/issues/${{ needs.setup.outputs.pr_number }}/comments --jq '.[].body' 2>/dev/null || echo "")
135227
136228 # Gather CI status
137- CI_STATUS=$(gh pr checks ${{ github.event.issue.number }} --repo ${{ github.repository }} 2>/dev/null || echo "")
138- FAILED_RUNS=$(gh run list --branch ${{ steps.pr .outputs.branch }} --status failure --limit 3 --json databaseId,name --jq '.[] | "\(.databaseId) \(.name)"' 2>/dev/null || echo "")
229+ CI_STATUS=$(gh pr checks ${{ needs.setup.outputs.pr_number }} --repo ${{ github.repository }} 2>/dev/null || echo "")
230+ FAILED_RUNS=$(gh run list --branch ${{ needs.setup .outputs.branch }} --status failure --limit 3 --json databaseId,name --jq '.[] | "\(.databaseId) \(.name)"' 2>/dev/null || echo "")
139231
140232 # Get failed run logs if any
141233 CI_LOGS=""
@@ -145,7 +237,7 @@ jobs:
145237 $(gh run view $run_id --log-failed 2>/dev/null | tail -100 || echo 'Could not fetch logs')"
146238 done
147239
148- INSTRUCTIONS='${{ steps.pr .outputs.instructions }}'
240+ INSTRUCTIONS='${{ needs.setup .outputs.instructions }}'
149241 claude --dangerously-skip-permissions --max-turns 100 -p "
150242 Fix all issues with this PR: review comments AND CI failures.
151243 ${INSTRUCTIONS:+
@@ -172,16 +264,22 @@ jobs:
172264 2. Fix review comments and CI issues
173265 3. Run tests to verify: make test, cargo test, npm test, etc.
174266 4. Commit: git commit -am 'Fix review feedback and CI issues'
175- 5. Push: git push origin ${{ steps.pr .outputs.branch }}
176- 6. Post summary: gh pr comment ${{ github.event.issue.number }} --repo ${{ github.repository }} --body 'SUMMARY'
267+ 5. Push: git push origin ${{ needs.setup .outputs.branch }}
268+ 6. Post summary: gh pr comment ${{ needs.setup.outputs.pr_number }} --repo ${{ github.repository }} --body 'SUMMARY'
177269
178270 ## Rules
179271 - All CI failures must be fixed.
180272 - All change requests must be either addressed or explained.
181273 " 2>&1 | tee claude-output.txt
182274
275+ # Check for authentication errors in output
276+ if grep -qiE "authenticat(e|ion)|unauthorized|forbidden|invalid.*key|api.*key.*invalid|API Error: 40[13]" claude-output.txt; then
277+ echo "Error: Authentication failure detected"
278+ exit 1
279+ fi
280+
183281 - name : Debug test
184- if : steps.pr .outputs.command == 'debug'
282+ if : needs.setup .outputs.command == 'debug'
185283 run : |
186284 echo "Debug test passed - workflow is working" | tee claude-output.txt
187285
@@ -201,9 +299,9 @@ jobs:
201299 await github.rest.repos.createCommitStatus({
202300 owner: context.repo.owner,
203301 repo: context.repo.repo,
204- sha: '${{ steps.pr .outputs.sha }}',
302+ sha: '${{ needs.setup .outputs.sha }}',
205303 state: 'success',
206- context: 'PR Automation / ${{ steps.pr .outputs.command }}',
304+ context: 'PR Automation / ${{ needs.setup .outputs.command }}',
207305 description: 'Completed successfully',
208306 target_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
209307 });
@@ -216,9 +314,9 @@ jobs:
216314 await github.rest.repos.createCommitStatus({
217315 owner: context.repo.owner,
218316 repo: context.repo.repo,
219- sha: '${{ steps.pr .outputs.sha }}',
317+ sha: '${{ needs.setup .outputs.sha }}',
220318 state: 'failure',
221- context: 'PR Automation / ${{ steps.pr .outputs.command }}',
319+ context: 'PR Automation / ${{ needs.setup .outputs.command }}',
222320 description: 'Failed - check logs',
223321 target_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
224322 });
0 commit comments