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';
describe('buildPrBody', () => {
it('should build body with issue and report', () => {
it('should build body with single issue and report', () => {
const issue: GitHubIssue = {
number: 99,
title: 'Add login feature',
@ -19,7 +19,7 @@ describe('buildPrBody', () => {
comments: [],
};
const result = buildPrBody(issue, 'Piece `default` completed.');
const result = buildPrBody([issue], 'Piece `default` completed.');
expect(result).toContain('## Summary');
expect(result).toContain('Implement username/password authentication.');
@ -37,7 +37,7 @@ describe('buildPrBody', () => {
comments: [],
};
const result = buildPrBody(issue, 'Done.');
const result = buildPrBody([issue], 'Done.');
expect(result).toContain('Fix bug');
expect(result).toContain('Closes #10');
@ -51,4 +51,30 @@ describe('buildPrBody', () => {
expect(result).toContain('Task completed.');
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 { 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 { executePipeline } from '../../features/pipeline/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;
if (issueFromOption) {
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);
} catch (e) {
error(getErrorMessage(e));
@ -75,17 +81,27 @@ export async function executeDefaultAction(task?: string): Promise<void> {
}
if (task && isDirectTask(task)) {
// isDirectTask() returns true only for issue references
let resolvedTask: string;
// isDirectTask() returns true only for issue references (e.g., "#6" or "#1 #2")
try {
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) {
error(getErrorMessage(e));
process.exit(1);
}
await selectAndExecuteTask(resolvedCwd, resolvedTask, selectOptions, agentOverrides);
return;
}

View File

@ -96,7 +96,7 @@ function buildPipelinePrBody(
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);
if (shouldCreatePr) {
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, {
branch,
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 { ProviderType } from '../../../infra/providers/index.js';
import type { GitHubIssue } from '../../../infra/github/index.js';
/** Result of piece execution */
export interface PieceExecutionResult {
@ -93,4 +94,6 @@ export interface SelectAndExecuteOptions {
interactiveUserInput?: boolean;
/** Interactive mode result metadata for NDJSON logging */
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[] = [];
parts.push('## Summary');
if (issue) {
if (issues && issues.length > 0) {
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('');
@ -86,9 +88,9 @@ export function buildPrBody(issue: GitHubIssue | undefined, report: string): str
parts.push('');
parts.push(report);
if (issue) {
if (issues && issues.length > 0) {
parts.push('');
parts.push(`Closes #${issue.number}`);
parts.push(issues.map((issue) => `Closes #${issue.number}`).join('\n'));
}
return parts.join('\n');