Skip to content

Commit b403cd2

Browse files
authored
fix(issue-auto-implement): iterate on PR when comment is on PR; avoid committing .pr_*; pass PR meta via outputs (#249)
- Treat issue_comment on a PR as iteration: resolve issue number from PR head/body, skip redirect, use existing branch - Unstage .pr_title and .pr_body before commit; add to action .gitignore - Pass PR title/body to Create PR step via step outputs instead of re-reading files - Prompt: clarify that .commit_msg/.pr_title/.pr_body are workflow-only and must not be committed Made-with: Cursor
1 parent 1d1871f commit b403cd2

4 files changed

Lines changed: 50 additions & 13 deletions

File tree

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
# Commit message file written by implement script (never commit)
1+
# Workflow artifacts written by implement script (read by action, never commit)
22
.commit_msg
3+
.pr_title
4+
.pr_body
35

46
# Local env (secrets); do not commit
57
.env

.github/actions/issue-auto-implement/action.yml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ runs:
220220
cd .github/actions/issue-auto-implement/assess && npx tsx src/implement.ts
221221
cd "$GITHUB_WORKSPACE"
222222
git add -A
223-
git reset -- "$COMMIT_MSG_FILE" 2>/dev/null || true
223+
git reset -- "$COMMIT_MSG_FILE" ".github/actions/issue-auto-implement/.pr_title" ".github/actions/issue-auto-implement/.pr_body" 2>/dev/null || true
224224
if ! git diff --staged --quiet; then
225225
git commit -F "$COMMIT_MSG_FILE"
226226
rm -f "$COMMIT_MSG_FILE"
@@ -230,6 +230,10 @@ runs:
230230
fi
231231
VERIFY_OUTPUT=$(eval "$VERIFY_CMDS" 2>&1); VERIFY_EXIT=$?
232232
if [ "$VERIFY_EXIT" -eq 0 ]; then
233+
# Pass PR meta to Create PR step via outputs (files are Claude handoff only; do not re-read in next step)
234+
PR_DIR=".github/actions/issue-auto-implement"
235+
if [ -f "$PR_DIR/.pr_title" ]; then echo "pr_title<<EOF" >> $GITHUB_OUTPUT; cat "$PR_DIR/.pr_title" >> $GITHUB_OUTPUT; echo "EOF" >> $GITHUB_OUTPUT; else echo "pr_title=Implement issue #${ISSUE_NUMBER}" >> $GITHUB_OUTPUT; fi
236+
if [ -f "$PR_DIR/.pr_body" ]; then echo "pr_body<<EOF" >> $GITHUB_OUTPUT; cat "$PR_DIR/.pr_body" >> $GITHUB_OUTPUT; echo "EOF" >> $GITHUB_OUTPUT; else echo "pr_body=Closes #${ISSUE_NUMBER}" >> $GITHUB_OUTPUT; fi
233237
echo "verified=true" >> $GITHUB_OUTPUT
234238
exit 0
235239
fi
@@ -285,15 +289,13 @@ runs:
285289
ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }}
286290
LABEL_PREFIX: ${{ inputs.label_prefix }}
287291
POST_PR_COMMENT: ${{ inputs.post_pr_comment }}
292+
PR_TITLE: ${{ steps.implement_verify_loop.outputs.pr_title }}
293+
PR_BODY: ${{ steps.implement_verify_loop.outputs.pr_body }}
288294
run: |
289295
BRANCH="auto-implement-issue-${ISSUE_NUMBER}"
290-
PR_DIR=".github/actions/issue-auto-implement"
291-
if [ -f "$PR_DIR/.pr_title" ]; then TITLE=$(cat "$PR_DIR/.pr_title"); else TITLE="Implement issue #${ISSUE_NUMBER}"; fi
292-
if [ -f "$PR_DIR/.pr_body" ]; then
293-
PAYLOAD=$(jq -n --arg t "$TITLE" --rawfile b "$PR_DIR/.pr_body" --arg h "$BRANCH" '{title: $t, body: $b, head: $h, base: "main"}')
294-
else
295-
PAYLOAD=$(jq -n --arg t "$TITLE" --arg b "Closes #${ISSUE_NUMBER}" --arg h "$BRANCH" '{title: $t, body: $b, head: $h, base: "main"}')
296-
fi
296+
TITLE="${PR_TITLE:-Implement issue #${ISSUE_NUMBER}}"
297+
BODY="${PR_BODY:-Closes #${ISSUE_NUMBER}}"
298+
PAYLOAD=$(jq -n --arg t "$TITLE" --arg b "$BODY" --arg h "$BRANCH" '{title: $t, body: $b, head: $h, base: "main"}')
297299
PR_JSON=$(curl -s -X POST \
298300
-H "Authorization: Bearer $GITHUB_TOKEN" \
299301
-H "Accept: application/vnd.github+json" \

.github/actions/issue-auto-implement/assess/src/implement.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ function buildClaudeCliPrompt(
7272
` 1. ${metaDir}/.commit_msg — one line, conventional commit message (e.g. "fix: correct version comparison for beta").`,
7373
` 2. ${metaDir}/.pr_title — one-line PR title.`,
7474
` 3. ${metaDir}/.pr_body — markdown body: brief problem summary, then "How it was solved" or "Solution". Do NOT include "Closes #N" (it will be appended).`,
75+
` These files are workflow-only inputs (consumed by the action to create the commit and PR). Do NOT add or commit them to the repository.`,
7576
'',
7677
'Issue title:',
7778
issueTitle,

.github/actions/issue-auto-implement/assess/src/index.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { readFileSync } from 'fs';
99
import { resolve } from 'path';
1010
import { config } from 'dotenv';
1111
import Anthropic from '@anthropic-ai/sdk';
12-
import { normalizeEvent } from './normalize.js';
12+
import { normalizeEvent, issueNumberFromPrPayload } from './normalize.js';
1313

1414
// Load .env from action root then cwd (cwd is assess/ when run from there). No-op if files missing.
1515
config({ path: resolve(process.cwd(), '../.env') });
@@ -44,6 +44,22 @@ async function checkExistingPr(owner: string, repo: string, issueNumber: number)
4444
return pr?.html_url ? { pr_url: pr.html_url } : null;
4545
}
4646

47+
/** Fetch PR by number; returns issue number derived from head ref or body (Closes #N). Used when issue_comment is on a PR. */
48+
async function issueNumberFromPrNumber(
49+
owner: string,
50+
repo: string,
51+
prNumber: number,
52+
token: string
53+
): Promise<number | null> {
54+
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`;
55+
const res = await fetch(url, {
56+
headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' },
57+
});
58+
if (!res.ok) return null;
59+
const pr = (await res.json()) as { body?: string | null; head?: { ref?: string } };
60+
return issueNumberFromPrPayload(pr);
61+
}
62+
4763
type IssueComment = { body?: string; user?: { login?: string }; created_at?: string };
4864

4965
async function fetchIssueComments(
@@ -206,11 +222,26 @@ export async function assess(
206222
const normalized = normalizeEvent(eventName, payload);
207223
if (!normalized) throw new Error('Could not normalize event');
208224

225+
/** When issue_comment is on a PR, we fetch comments for the PR (this number); implement uses resolved issue number. */
226+
let commentTargetIssueNumber: number | undefined;
227+
209228
if (eventName === 'issue_comment' && opts.repo && opts.token) {
210229
const [owner, repo] = opts.repo.split('/');
211230
if (owner && repo) {
212-
const existing = await checkExistingPr(owner, repo, normalized.issueNumber);
213-
if (existing) return { action: 'redirect_to_pr', issue_number: normalized.issueNumber, pr_url: existing.pr_url };
231+
const p = payload as Record<string, unknown>;
232+
const issue = p.issue as { pull_request?: unknown } | undefined;
233+
const commentOnPr = Boolean(issue?.pull_request);
234+
let issueNumber = normalized.issueNumber;
235+
if (commentOnPr) {
236+
commentTargetIssueNumber = normalized.issueNumber;
237+
const resolved = await issueNumberFromPrNumber(owner, repo, normalized.issueNumber, opts.token);
238+
if (resolved != null) issueNumber = resolved;
239+
// Comment was on the PR: do not redirect — run assess and iterate on existing branch (same issue number).
240+
} else {
241+
const existing = await checkExistingPr(owner, repo, issueNumber);
242+
if (existing) return { action: 'redirect_to_pr', issue_number: issueNumber, pr_url: existing.pr_url };
243+
}
244+
normalized.issueNumber = issueNumber;
214245
}
215246
}
216247

@@ -220,7 +251,8 @@ export async function assess(
220251
if ((eventName === 'issues' || eventName === 'issue_comment') && opts.repo && opts.token) {
221252
const [owner, repo] = opts.repo.split('/');
222253
if (owner && repo) {
223-
issueComments = await fetchIssueComments(owner, repo, normalized.issueNumber, opts.token);
254+
const fetchCommentsFor = commentTargetIssueNumber ?? normalized.issueNumber;
255+
issueComments = await fetchIssueComments(owner, repo, fetchCommentsFor, opts.token);
224256
}
225257
}
226258
const prompt = buildAssessmentPrompt(payload, eventName, referenceIssue, contextBlock, issueComments);

0 commit comments

Comments
 (0)