takt: fix-pr-issue-number

This commit is contained in:
nrslib 2026-02-06 12:04:24 +09:00
parent 52c927e6f1
commit 73db206c9a
6 changed files with 65 additions and 18 deletions

View File

@ -10,7 +10,7 @@ import { buildPrBody } from '../infra/github/pr.js';
import type { GitHubIssue } from '../infra/github/types.js'; import type { GitHubIssue } from '../infra/github/types.js';
describe('buildPrBody', () => { describe('buildPrBody', () => {
it('should build body with issue and report', () => { it('should build body with single issue and report', () => {
const issue: GitHubIssue = { const issue: GitHubIssue = {
number: 99, number: 99,
title: 'Add login feature', title: 'Add login feature',
@ -19,7 +19,7 @@ describe('buildPrBody', () => {
comments: [], comments: [],
}; };
const result = buildPrBody(issue, 'Piece `default` completed.'); const result = buildPrBody([issue], 'Piece `default` completed.');
expect(result).toContain('## Summary'); expect(result).toContain('## Summary');
expect(result).toContain('Implement username/password authentication.'); expect(result).toContain('Implement username/password authentication.');
@ -37,7 +37,7 @@ describe('buildPrBody', () => {
comments: [], comments: [],
}; };
const result = buildPrBody(issue, 'Done.'); const result = buildPrBody([issue], 'Done.');
expect(result).toContain('Fix bug'); expect(result).toContain('Fix bug');
expect(result).toContain('Closes #10'); expect(result).toContain('Closes #10');
@ -51,4 +51,30 @@ describe('buildPrBody', () => {
expect(result).toContain('Task completed.'); expect(result).toContain('Task completed.');
expect(result).not.toContain('Closes'); expect(result).not.toContain('Closes');
}); });
it('should support multiple issues', () => {
const issues: GitHubIssue[] = [
{
number: 1,
title: 'First issue',
body: 'First issue body.',
labels: [],
comments: [],
},
{
number: 2,
title: 'Second issue',
body: 'Second issue body.',
labels: [],
comments: [],
},
];
const result = buildPrBody(issues, 'Done.');
expect(result).toContain('## Summary');
expect(result).toContain('First issue body.');
expect(result).toContain('Closes #1');
expect(result).toContain('Closes #2');
});
}); });

View File

@ -7,7 +7,7 @@
import { info, error } from '../../shared/ui/index.js'; import { info, error } from '../../shared/ui/index.js';
import { getErrorMessage } from '../../shared/utils/index.js'; import { getErrorMessage } from '../../shared/utils/index.js';
import { resolveIssueTask } from '../../infra/github/index.js'; import { fetchIssue, formatIssueAsTask, checkGhCli, parseIssueNumbers } from '../../infra/github/index.js';
import { selectAndExecuteTask, determinePiece, saveTaskFromInteractive, createIssueFromTask, type SelectAndExecuteOptions } from '../../features/tasks/index.js'; import { selectAndExecuteTask, determinePiece, saveTaskFromInteractive, createIssueFromTask, type SelectAndExecuteOptions } from '../../features/tasks/index.js';
import { executePipeline } from '../../features/pipeline/index.js'; import { executePipeline } from '../../features/pipeline/index.js';
import { interactiveMode } from '../../features/interactive/index.js'; import { interactiveMode } from '../../features/interactive/index.js';
@ -65,7 +65,13 @@ export async function executeDefaultAction(task?: string): Promise<void> {
const issueFromOption = opts.issue as number | undefined; const issueFromOption = opts.issue as number | undefined;
if (issueFromOption) { if (issueFromOption) {
try { try {
const resolvedTask = resolveIssueTask(`#${issueFromOption}`); const ghStatus = checkGhCli();
if (!ghStatus.available) {
throw new Error(ghStatus.error);
}
const issue = fetchIssue(issueFromOption);
const resolvedTask = formatIssueAsTask(issue);
selectOptions.issues = [issue];
await selectAndExecuteTask(resolvedCwd, resolvedTask, selectOptions, agentOverrides); await selectAndExecuteTask(resolvedCwd, resolvedTask, selectOptions, agentOverrides);
} catch (e) { } catch (e) {
error(getErrorMessage(e)); error(getErrorMessage(e));
@ -75,17 +81,27 @@ export async function executeDefaultAction(task?: string): Promise<void> {
} }
if (task && isDirectTask(task)) { if (task && isDirectTask(task)) {
// isDirectTask() returns true only for issue references // isDirectTask() returns true only for issue references (e.g., "#6" or "#1 #2")
let resolvedTask: string;
try { try {
info('Fetching GitHub Issue...'); info('Fetching GitHub Issue...');
resolvedTask = resolveIssueTask(task); const ghStatus = checkGhCli();
if (!ghStatus.available) {
throw new Error(ghStatus.error);
}
// Parse all issue numbers from task (supports "#6" and "#1 #2")
const tokens = task.trim().split(/\s+/);
const issueNumbers = parseIssueNumbers(tokens);
if (issueNumbers.length === 0) {
throw new Error(`Invalid issue reference: ${task}`);
}
const issues = issueNumbers.map((n) => fetchIssue(n));
const resolvedTask = issues.map(formatIssueAsTask).join('\n\n---\n\n');
selectOptions.issues = issues;
await selectAndExecuteTask(resolvedCwd, resolvedTask, selectOptions, agentOverrides);
} catch (e) { } catch (e) {
error(getErrorMessage(e)); error(getErrorMessage(e));
process.exit(1); process.exit(1);
} }
await selectAndExecuteTask(resolvedCwd, resolvedTask, selectOptions, agentOverrides);
return; return;
} }

View File

@ -96,7 +96,7 @@ function buildPipelinePrBody(
report, report,
}); });
} }
return buildPrBody(issue, report); return buildPrBody(issue ? [issue] : undefined, report);
} }
/** /**

View File

@ -168,7 +168,7 @@ export async function selectAndExecuteTask(
const shouldCreatePr = options?.autoPr === true || await confirm('Create pull request?', false); const shouldCreatePr = options?.autoPr === true || await confirm('Create pull request?', false);
if (shouldCreatePr) { if (shouldCreatePr) {
info('Creating pull request...'); info('Creating pull request...');
const prBody = buildPrBody(undefined, `Piece \`${pieceIdentifier}\` completed successfully.`); const prBody = buildPrBody(options?.issues, `Piece \`${pieceIdentifier}\` completed successfully.`);
const prResult = createPullRequest(execCwd, { const prResult = createPullRequest(execCwd, {
branch, branch,
title: task.length > 100 ? `${task.slice(0, 97)}...` : task, title: task.length > 100 ? `${task.slice(0, 97)}...` : task,

View File

@ -4,6 +4,7 @@
import type { Language } from '../../../core/models/index.js'; import type { Language } from '../../../core/models/index.js';
import type { ProviderType } from '../../../infra/providers/index.js'; import type { ProviderType } from '../../../infra/providers/index.js';
import type { GitHubIssue } from '../../../infra/github/index.js';
/** Result of piece execution */ /** Result of piece execution */
export interface PieceExecutionResult { export interface PieceExecutionResult {
@ -93,4 +94,6 @@ export interface SelectAndExecuteOptions {
interactiveUserInput?: boolean; interactiveUserInput?: boolean;
/** Interactive mode result metadata for NDJSON logging */ /** Interactive mode result metadata for NDJSON logging */
interactiveMetadata?: InteractiveMetadata; interactiveMetadata?: InteractiveMetadata;
/** GitHub Issues to associate with the PR (adds "Closes #N" for each issue) */
issues?: GitHubIssue[];
} }

View File

@ -70,15 +70,17 @@ export function createPullRequest(cwd: string, options: CreatePrOptions): Create
} }
/** /**
* Build PR body from issue and execution report. * Build PR body from issues and execution report.
* Supports multiple issues (adds "Closes #N" for each).
*/ */
export function buildPrBody(issue: GitHubIssue | undefined, report: string): string { export function buildPrBody(issues: GitHubIssue[] | undefined, report: string): string {
const parts: string[] = []; const parts: string[] = [];
parts.push('## Summary'); parts.push('## Summary');
if (issue) { if (issues && issues.length > 0) {
parts.push(''); parts.push('');
parts.push(issue.body || issue.title); // Use the first issue's body/title for summary
parts.push(issues[0]!.body || issues[0]!.title);
} }
parts.push(''); parts.push('');
@ -86,9 +88,9 @@ export function buildPrBody(issue: GitHubIssue | undefined, report: string): str
parts.push(''); parts.push('');
parts.push(report); parts.push(report);
if (issue) { if (issues && issues.length > 0) {
parts.push(''); parts.push('');
parts.push(`Closes #${issue.number}`); parts.push(issues.map((issue) => `Closes #${issue.number}`).join('\n'));
} }
return parts.join('\n'); return parts.join('\n');