Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/actions/issue-auto-implement/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Commit message file written by implement script (never commit)
# Workflow artifacts written by implement script (read by action, never commit)
.commit_msg
.pr_title
.pr_body

# Local env (secrets); do not commit
.env
18 changes: 10 additions & 8 deletions .github/actions/issue-auto-implement/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ runs:
cd .github/actions/issue-auto-implement/assess && npx tsx src/implement.ts
cd "$GITHUB_WORKSPACE"
git add -A
git reset -- "$COMMIT_MSG_FILE" 2>/dev/null || true
git reset -- "$COMMIT_MSG_FILE" ".github/actions/issue-auto-implement/.pr_title" ".github/actions/issue-auto-implement/.pr_body" 2>/dev/null || true
if ! git diff --staged --quiet; then
git commit -F "$COMMIT_MSG_FILE"
rm -f "$COMMIT_MSG_FILE"
Expand All @@ -230,6 +230,10 @@ runs:
fi
VERIFY_OUTPUT=$(eval "$VERIFY_CMDS" 2>&1); VERIFY_EXIT=$?
if [ "$VERIFY_EXIT" -eq 0 ]; then
# Pass PR meta to Create PR step via outputs (files are Claude handoff only; do not re-read in next step)
PR_DIR=".github/actions/issue-auto-implement"
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
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
echo "verified=true" >> $GITHUB_OUTPUT
exit 0
fi
Expand Down Expand Up @@ -285,15 +289,13 @@ runs:
ISSUE_NUMBER: ${{ steps.assess.outputs.issue_number }}
LABEL_PREFIX: ${{ inputs.label_prefix }}
POST_PR_COMMENT: ${{ inputs.post_pr_comment }}
PR_TITLE: ${{ steps.implement_verify_loop.outputs.pr_title }}
PR_BODY: ${{ steps.implement_verify_loop.outputs.pr_body }}
run: |
BRANCH="auto-implement-issue-${ISSUE_NUMBER}"
PR_DIR=".github/actions/issue-auto-implement"
if [ -f "$PR_DIR/.pr_title" ]; then TITLE=$(cat "$PR_DIR/.pr_title"); else TITLE="Implement issue #${ISSUE_NUMBER}"; fi
if [ -f "$PR_DIR/.pr_body" ]; then
PAYLOAD=$(jq -n --arg t "$TITLE" --rawfile b "$PR_DIR/.pr_body" --arg h "$BRANCH" '{title: $t, body: $b, head: $h, base: "main"}')
else
PAYLOAD=$(jq -n --arg t "$TITLE" --arg b "Closes #${ISSUE_NUMBER}" --arg h "$BRANCH" '{title: $t, body: $b, head: $h, base: "main"}')
fi
TITLE="${PR_TITLE:-Implement issue #${ISSUE_NUMBER}}"
BODY="${PR_BODY:-Closes #${ISSUE_NUMBER}}"
PAYLOAD=$(jq -n --arg t "$TITLE" --arg b "$BODY" --arg h "$BRANCH" '{title: $t, body: $b, head: $h, base: "main"}')
PR_JSON=$(curl -s -X POST \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ function buildClaudeCliPrompt(
` 1. ${metaDir}/.commit_msg — one line, conventional commit message (e.g. "fix: correct version comparison for beta").`,
` 2. ${metaDir}/.pr_title — one-line PR title.`,
` 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).`,
` 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.`,
'',
'Issue title:',
issueTitle,
Expand Down
40 changes: 36 additions & 4 deletions .github/actions/issue-auto-implement/assess/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { readFileSync } from 'fs';
import { resolve } from 'path';
import { config } from 'dotenv';
import Anthropic from '@anthropic-ai/sdk';
import { normalizeEvent } from './normalize.js';
import { normalizeEvent, issueNumberFromPrPayload } from './normalize.js';

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

/** Fetch PR by number; returns issue number derived from head ref or body (Closes #N). Used when issue_comment is on a PR. */
async function issueNumberFromPrNumber(
owner: string,
repo: string,
prNumber: number,
token: string
): Promise<number | null> {
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`;
const res = await fetch(url, {
headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' },
});
if (!res.ok) return null;
const pr = (await res.json()) as { body?: string | null; head?: { ref?: string } };
return issueNumberFromPrPayload(pr);
}

type IssueComment = { body?: string; user?: { login?: string }; created_at?: string };

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

/** When issue_comment is on a PR, we fetch comments for the PR (this number); implement uses resolved issue number. */
let commentTargetIssueNumber: number | undefined;

if (eventName === 'issue_comment' && opts.repo && opts.token) {
const [owner, repo] = opts.repo.split('/');
if (owner && repo) {
const existing = await checkExistingPr(owner, repo, normalized.issueNumber);
if (existing) return { action: 'redirect_to_pr', issue_number: normalized.issueNumber, pr_url: existing.pr_url };
const p = payload as Record<string, unknown>;
const issue = p.issue as { pull_request?: unknown } | undefined;
const commentOnPr = Boolean(issue?.pull_request);
let issueNumber = normalized.issueNumber;
if (commentOnPr) {
commentTargetIssueNumber = normalized.issueNumber;
const resolved = await issueNumberFromPrNumber(owner, repo, normalized.issueNumber, opts.token);
if (resolved != null) issueNumber = resolved;
// Comment was on the PR: do not redirect — run assess and iterate on existing branch (same issue number).
} else {
const existing = await checkExistingPr(owner, repo, issueNumber);
if (existing) return { action: 'redirect_to_pr', issue_number: issueNumber, pr_url: existing.pr_url };
}
normalized.issueNumber = issueNumber;
}
}

Expand All @@ -220,7 +251,8 @@ export async function assess(
if ((eventName === 'issues' || eventName === 'issue_comment') && opts.repo && opts.token) {
const [owner, repo] = opts.repo.split('/');
if (owner && repo) {
issueComments = await fetchIssueComments(owner, repo, normalized.issueNumber, opts.token);
const fetchCommentsFor = commentTargetIssueNumber ?? normalized.issueNumber;
issueComments = await fetchIssueComments(owner, repo, fetchCommentsFor, opts.token);
}
}
const prompt = buildAssessmentPrompt(payload, eventName, referenceIssue, contextBlock, issueComments);
Expand Down
Loading