Merge pull request #377 from Yuma-Satake/feature/issue-111
Fix #111 Issue作成時にラベルを選択できるようにする
This commit is contained in:
parent
96ff2ed961
commit
6a175bcb11
@ -40,7 +40,8 @@ vi.mock('../features/tasks/index.js', () => ({
|
|||||||
selectAndExecuteTask: vi.fn(),
|
selectAndExecuteTask: vi.fn(),
|
||||||
determinePiece: vi.fn(),
|
determinePiece: vi.fn(),
|
||||||
saveTaskFromInteractive: vi.fn(),
|
saveTaskFromInteractive: vi.fn(),
|
||||||
createIssueFromTask: vi.fn(),
|
createIssueAndSaveTask: vi.fn(),
|
||||||
|
promptLabelSelection: vi.fn().mockResolvedValue([]),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../features/pipeline/index.js', () => ({
|
vi.mock('../features/pipeline/index.js', () => ({
|
||||||
@ -105,7 +106,7 @@ vi.mock('../app/cli/helpers.js', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import { checkGhCli, fetchIssue, formatIssueAsTask, parseIssueNumbers } from '../infra/github/issue.js';
|
import { checkGhCli, fetchIssue, formatIssueAsTask, parseIssueNumbers } from '../infra/github/issue.js';
|
||||||
import { selectAndExecuteTask, determinePiece, createIssueFromTask, saveTaskFromInteractive } from '../features/tasks/index.js';
|
import { selectAndExecuteTask, determinePiece, createIssueAndSaveTask } from '../features/tasks/index.js';
|
||||||
import { interactiveMode } from '../features/interactive/index.js';
|
import { interactiveMode } from '../features/interactive/index.js';
|
||||||
import { resolveConfigValues, loadPersonaSessions } from '../infra/config/index.js';
|
import { resolveConfigValues, loadPersonaSessions } from '../infra/config/index.js';
|
||||||
import { isDirectTask } from '../app/cli/helpers.js';
|
import { isDirectTask } from '../app/cli/helpers.js';
|
||||||
@ -119,8 +120,7 @@ const mockFormatIssueAsTask = vi.mocked(formatIssueAsTask);
|
|||||||
const mockParseIssueNumbers = vi.mocked(parseIssueNumbers);
|
const mockParseIssueNumbers = vi.mocked(parseIssueNumbers);
|
||||||
const mockSelectAndExecuteTask = vi.mocked(selectAndExecuteTask);
|
const mockSelectAndExecuteTask = vi.mocked(selectAndExecuteTask);
|
||||||
const mockDeterminePiece = vi.mocked(determinePiece);
|
const mockDeterminePiece = vi.mocked(determinePiece);
|
||||||
const mockCreateIssueFromTask = vi.mocked(createIssueFromTask);
|
const mockCreateIssueAndSaveTask = vi.mocked(createIssueAndSaveTask);
|
||||||
const mockSaveTaskFromInteractive = vi.mocked(saveTaskFromInteractive);
|
|
||||||
const mockInteractiveMode = vi.mocked(interactiveMode);
|
const mockInteractiveMode = vi.mocked(interactiveMode);
|
||||||
const mockLoadPersonaSessions = vi.mocked(loadPersonaSessions);
|
const mockLoadPersonaSessions = vi.mocked(loadPersonaSessions);
|
||||||
const mockResolveConfigValues = vi.mocked(resolveConfigValues);
|
const mockResolveConfigValues = vi.mocked(resolveConfigValues);
|
||||||
@ -435,38 +435,22 @@ describe('Issue resolution in routing', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('create_issue action', () => {
|
describe('create_issue action', () => {
|
||||||
it('should create issue first, then delegate final confirmation to saveTaskFromInteractive', async () => {
|
it('should delegate to createIssueAndSaveTask with confirmAtEndMessage', async () => {
|
||||||
// Given
|
// Given
|
||||||
mockInteractiveMode.mockResolvedValue({ action: 'create_issue', task: 'New feature request' });
|
mockInteractiveMode.mockResolvedValue({ action: 'create_issue', task: 'New feature request' });
|
||||||
mockCreateIssueFromTask.mockReturnValue(226);
|
|
||||||
|
|
||||||
// When
|
// When
|
||||||
await executeDefaultAction();
|
await executeDefaultAction();
|
||||||
|
|
||||||
// Then: issue is created first
|
// Then: issue is created first
|
||||||
expect(mockCreateIssueFromTask).toHaveBeenCalledWith('New feature request');
|
expect(mockCreateIssueAndSaveTask).toHaveBeenCalledWith(
|
||||||
// Then: saveTaskFromInteractive receives final confirmation message
|
|
||||||
expect(mockSaveTaskFromInteractive).toHaveBeenCalledWith(
|
|
||||||
'/test/cwd',
|
'/test/cwd',
|
||||||
'New feature request',
|
'New feature request',
|
||||||
'default',
|
'default',
|
||||||
{ issue: 226, confirmAtEndMessage: 'Add this issue to tasks?' },
|
{ confirmAtEndMessage: 'Add this issue to tasks?', labels: [] },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip confirmation and task save when issue creation fails', async () => {
|
|
||||||
// Given
|
|
||||||
mockInteractiveMode.mockResolvedValue({ action: 'create_issue', task: 'New feature request' });
|
|
||||||
mockCreateIssueFromTask.mockReturnValue(undefined);
|
|
||||||
|
|
||||||
// When
|
|
||||||
await executeDefaultAction();
|
|
||||||
|
|
||||||
// Then
|
|
||||||
expect(mockCreateIssueFromTask).toHaveBeenCalledWith('New feature request');
|
|
||||||
expect(mockSaveTaskFromInteractive).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not call selectAndExecuteTask when create_issue action is chosen', async () => {
|
it('should not call selectAndExecuteTask when create_issue action is chosen', async () => {
|
||||||
// Given
|
// Given
|
||||||
mockInteractiveMode.mockResolvedValue({ action: 'create_issue', task: 'New feature request' });
|
mockInteractiveMode.mockResolvedValue({ action: 'create_issue', task: 'New feature request' });
|
||||||
|
|||||||
@ -62,23 +62,42 @@ describe('createIssue', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include labels when provided', () => {
|
it('should include labels when provided and they exist on the repo', () => {
|
||||||
// Given
|
// Given
|
||||||
mockExecFileSync
|
mockExecFileSync
|
||||||
.mockReturnValueOnce(Buffer.from('')) // gh auth status
|
.mockReturnValueOnce(Buffer.from('')) // gh auth status
|
||||||
|
.mockReturnValueOnce('bug\npriority:high\n' as unknown as Buffer) // gh label list
|
||||||
.mockReturnValueOnce('https://github.com/owner/repo/issues/1\n' as unknown as Buffer);
|
.mockReturnValueOnce('https://github.com/owner/repo/issues/1\n' as unknown as Buffer);
|
||||||
|
|
||||||
// When
|
// When
|
||||||
createIssue({ title: 'Bug', body: 'Fix it', labels: ['bug', 'priority:high'] });
|
createIssue({ title: 'Bug', body: 'Fix it', labels: ['bug', 'priority:high'] });
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
const issueCreateCall = mockExecFileSync.mock.calls[1];
|
const issueCreateCall = mockExecFileSync.mock.calls[2];
|
||||||
expect(issueCreateCall?.[1]).toEqual([
|
expect(issueCreateCall?.[1]).toEqual([
|
||||||
'issue', 'create', '--title', 'Bug', '--body', 'Fix it',
|
'issue', 'create', '--title', 'Bug', '--body', 'Fix it',
|
||||||
'--label', 'bug,priority:high',
|
'--label', 'bug,priority:high',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should skip non-existent labels', () => {
|
||||||
|
// Given
|
||||||
|
mockExecFileSync
|
||||||
|
.mockReturnValueOnce(Buffer.from('')) // gh auth status
|
||||||
|
.mockReturnValueOnce('bug\n' as unknown as Buffer) // gh label list (only bug exists)
|
||||||
|
.mockReturnValueOnce('https://github.com/owner/repo/issues/1\n' as unknown as Buffer);
|
||||||
|
|
||||||
|
// When
|
||||||
|
createIssue({ title: 'Bug', body: 'Fix it', labels: ['bug', 'Docs'] });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
const issueCreateCall = mockExecFileSync.mock.calls[2];
|
||||||
|
expect(issueCreateCall?.[1]).toEqual([
|
||||||
|
'issue', 'create', '--title', 'Bug', '--body', 'Fix it',
|
||||||
|
'--label', 'bug',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should not include --label when labels is empty', () => {
|
it('should not include --label when labels is empty', () => {
|
||||||
// Given
|
// Given
|
||||||
mockExecFileSync
|
mockExecFileSync
|
||||||
@ -93,6 +112,21 @@ describe('createIssue', () => {
|
|||||||
expect(issueCreateCall?.[1]).not.toContain('--label');
|
expect(issueCreateCall?.[1]).not.toContain('--label');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not include --label when all labels are non-existent', () => {
|
||||||
|
// Given
|
||||||
|
mockExecFileSync
|
||||||
|
.mockReturnValueOnce(Buffer.from('')) // gh auth status
|
||||||
|
.mockReturnValueOnce('bug\n' as unknown as Buffer) // gh label list
|
||||||
|
.mockReturnValueOnce('https://github.com/owner/repo/issues/1\n' as unknown as Buffer);
|
||||||
|
|
||||||
|
// When
|
||||||
|
createIssue({ title: 'Title', body: 'Body', labels: ['Docs', 'Chore'] });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
const issueCreateCall = mockExecFileSync.mock.calls[2];
|
||||||
|
expect(issueCreateCall?.[1]).not.toContain('--label');
|
||||||
|
});
|
||||||
|
|
||||||
it('should return error when gh CLI is not authenticated', () => {
|
it('should return error when gh CLI is not authenticated', () => {
|
||||||
// Given: auth fails, version succeeds
|
// Given: auth fails, version succeeds
|
||||||
mockExecFileSync
|
mockExecFileSync
|
||||||
|
|||||||
@ -164,4 +164,64 @@ describe('createIssueFromTask', () => {
|
|||||||
body: task,
|
body: task,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('labels option', () => {
|
||||||
|
it('should pass labels to createIssue when provided', () => {
|
||||||
|
// Given
|
||||||
|
mockCreateIssue.mockReturnValue({ success: true, url: 'https://github.com/owner/repo/issues/1' });
|
||||||
|
|
||||||
|
// When
|
||||||
|
createIssueFromTask('Test task', { labels: ['bug'] });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockCreateIssue).toHaveBeenCalledWith({
|
||||||
|
title: 'Test task',
|
||||||
|
body: 'Test task',
|
||||||
|
labels: ['bug'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include labels key when options is undefined', () => {
|
||||||
|
// Given
|
||||||
|
mockCreateIssue.mockReturnValue({ success: true, url: 'https://github.com/owner/repo/issues/1' });
|
||||||
|
|
||||||
|
// When
|
||||||
|
createIssueFromTask('Test task');
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockCreateIssue).toHaveBeenCalledWith({
|
||||||
|
title: 'Test task',
|
||||||
|
body: 'Test task',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include labels key when labels is empty array', () => {
|
||||||
|
// Given
|
||||||
|
mockCreateIssue.mockReturnValue({ success: true, url: 'https://github.com/owner/repo/issues/1' });
|
||||||
|
|
||||||
|
// When
|
||||||
|
createIssueFromTask('Test task', { labels: [] });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockCreateIssue).toHaveBeenCalledWith({
|
||||||
|
title: 'Test task',
|
||||||
|
body: 'Test task',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter out empty string labels', () => {
|
||||||
|
// Given
|
||||||
|
mockCreateIssue.mockReturnValue({ success: true, url: 'https://github.com/owner/repo/issues/1' });
|
||||||
|
|
||||||
|
// When
|
||||||
|
createIssueFromTask('Test task', { labels: ['bug', '', 'enhancement'] });
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(mockCreateIssue).toHaveBeenCalledWith({
|
||||||
|
title: 'Test task',
|
||||||
|
body: 'Test task',
|
||||||
|
labels: ['bug', 'enhancement'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { info, error as logError, withProgress } from '../../shared/ui/index.js'
|
|||||||
import { getErrorMessage } from '../../shared/utils/index.js';
|
import { getErrorMessage } from '../../shared/utils/index.js';
|
||||||
import { getLabel } from '../../shared/i18n/index.js';
|
import { getLabel } from '../../shared/i18n/index.js';
|
||||||
import { fetchIssue, formatIssueAsTask, checkGhCli, parseIssueNumbers, type GitHubIssue } from '../../infra/github/index.js';
|
import { fetchIssue, formatIssueAsTask, checkGhCli, parseIssueNumbers, type GitHubIssue } from '../../infra/github/index.js';
|
||||||
import { selectAndExecuteTask, determinePiece, saveTaskFromInteractive, createIssueFromTask, type SelectAndExecuteOptions } from '../../features/tasks/index.js';
|
import { selectAndExecuteTask, determinePiece, saveTaskFromInteractive, createIssueAndSaveTask, promptLabelSelection, type SelectAndExecuteOptions } from '../../features/tasks/index.js';
|
||||||
import { executePipeline } from '../../features/pipeline/index.js';
|
import { executePipeline } from '../../features/pipeline/index.js';
|
||||||
import {
|
import {
|
||||||
interactiveMode,
|
interactiveMode,
|
||||||
@ -219,13 +219,11 @@ export async function executeDefaultAction(task?: string): Promise<void> {
|
|||||||
await selectAndExecuteTask(resolvedCwd, confirmedTask, selectOptions, agentOverrides);
|
await selectAndExecuteTask(resolvedCwd, confirmedTask, selectOptions, agentOverrides);
|
||||||
},
|
},
|
||||||
create_issue: async ({ task: confirmedTask }) => {
|
create_issue: async ({ task: confirmedTask }) => {
|
||||||
const issueNumber = createIssueFromTask(confirmedTask);
|
const labels = await promptLabelSelection(lang);
|
||||||
if (issueNumber !== undefined) {
|
await createIssueAndSaveTask(resolvedCwd, confirmedTask, pieceId, {
|
||||||
await saveTaskFromInteractive(resolvedCwd, confirmedTask, pieceId, {
|
|
||||||
issue: issueNumber,
|
|
||||||
confirmAtEndMessage: 'Add this issue to tasks?',
|
confirmAtEndMessage: 'Add this issue to tasks?',
|
||||||
|
labels,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
},
|
},
|
||||||
save_task: async ({ task: confirmedTask }) => {
|
save_task: async ({ task: confirmedTask }) => {
|
||||||
await saveTaskFromInteractive(resolvedCwd, confirmedTask, pieceId);
|
await saveTaskFromInteractive(resolvedCwd, confirmedTask, pieceId);
|
||||||
|
|||||||
@ -6,8 +6,10 @@
|
|||||||
|
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import { promptInput, confirm } from '../../../shared/prompt/index.js';
|
import { promptInput, confirm, selectOption } from '../../../shared/prompt/index.js';
|
||||||
import { success, info, error, withProgress } from '../../../shared/ui/index.js';
|
import { success, info, error, withProgress } from '../../../shared/ui/index.js';
|
||||||
|
import { getLabel } from '../../../shared/i18n/index.js';
|
||||||
|
import type { Language } from '../../../core/models/types.js';
|
||||||
import { TaskRunner, type TaskFileData, summarizeTaskName } from '../../../infra/task/index.js';
|
import { TaskRunner, type TaskFileData, summarizeTaskName } from '../../../infra/task/index.js';
|
||||||
import { determinePiece } from '../execute/selectAndExecute.js';
|
import { determinePiece } from '../execute/selectAndExecute.js';
|
||||||
import { createLogger, getErrorMessage, generateReportDir } from '../../../shared/utils/index.js';
|
import { createLogger, getErrorMessage, generateReportDir } from '../../../shared/utils/index.js';
|
||||||
@ -73,14 +75,21 @@ export async function saveTaskFile(
|
|||||||
* Extracts the first line as the issue title (truncated to 100 chars),
|
* Extracts the first line as the issue title (truncated to 100 chars),
|
||||||
* uses the full task as the body, and displays success/error messages.
|
* uses the full task as the body, and displays success/error messages.
|
||||||
*/
|
*/
|
||||||
export function createIssueFromTask(task: string): number | undefined {
|
export function createIssueFromTask(task: string, options?: { labels?: string[] }): number | undefined {
|
||||||
info('Creating GitHub Issue...');
|
info('Creating GitHub Issue...');
|
||||||
const titleLine = task.split('\n')[0] || task;
|
const titleLine = task.split('\n')[0] || task;
|
||||||
const title = titleLine.length > 100 ? `${titleLine.slice(0, 97)}...` : titleLine;
|
const title = titleLine.length > 100 ? `${titleLine.slice(0, 97)}...` : titleLine;
|
||||||
const issueResult = createIssue({ title, body: task });
|
const effectiveLabels = options?.labels?.filter((l) => l.length > 0) ?? [];
|
||||||
|
const labels = effectiveLabels.length > 0 ? effectiveLabels : undefined;
|
||||||
|
|
||||||
|
const issueResult = createIssue({ title, body: task, labels });
|
||||||
if (issueResult.success) {
|
if (issueResult.success) {
|
||||||
|
if (!issueResult.url) {
|
||||||
|
error('Failed to extract issue number from URL');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
success(`Issue created: ${issueResult.url}`);
|
success(`Issue created: ${issueResult.url}`);
|
||||||
const num = Number(issueResult.url!.split('/').pop());
|
const num = Number(issueResult.url.split('/').pop());
|
||||||
if (Number.isNaN(num)) {
|
if (Number.isNaN(num)) {
|
||||||
error('Failed to extract issue number from URL');
|
error('Failed to extract issue number from URL');
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -127,13 +136,46 @@ function displayTaskCreationResult(
|
|||||||
* Combines issue creation and task saving into a single workflow.
|
* Combines issue creation and task saving into a single workflow.
|
||||||
* If issue creation fails, no task is saved.
|
* If issue creation fails, no task is saved.
|
||||||
*/
|
*/
|
||||||
export async function createIssueAndSaveTask(cwd: string, task: string, piece?: string): Promise<void> {
|
export async function createIssueAndSaveTask(
|
||||||
const issueNumber = createIssueFromTask(task);
|
cwd: string,
|
||||||
|
task: string,
|
||||||
|
piece?: string,
|
||||||
|
options?: { confirmAtEndMessage?: string; labels?: string[] },
|
||||||
|
): Promise<void> {
|
||||||
|
const issueNumber = createIssueFromTask(task, { labels: options?.labels });
|
||||||
if (issueNumber !== undefined) {
|
if (issueNumber !== undefined) {
|
||||||
await saveTaskFromInteractive(cwd, task, piece, { issue: issueNumber });
|
await saveTaskFromInteractive(cwd, task, piece, {
|
||||||
|
issue: issueNumber,
|
||||||
|
confirmAtEndMessage: options?.confirmAtEndMessage,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompt user to select a label for the GitHub Issue.
|
||||||
|
*
|
||||||
|
* Presents 4 fixed options: None, bug, enhancement, custom input.
|
||||||
|
* Returns an array of selected labels (empty if none selected).
|
||||||
|
*/
|
||||||
|
export async function promptLabelSelection(lang: Language): Promise<string[]> {
|
||||||
|
const selected = await selectOption<string>(
|
||||||
|
getLabel('issue.labelSelection.prompt', lang),
|
||||||
|
[
|
||||||
|
{ label: getLabel('issue.labelSelection.none', lang), value: 'none' },
|
||||||
|
{ label: 'bug', value: 'bug' },
|
||||||
|
{ label: 'enhancement', value: 'enhancement' },
|
||||||
|
{ label: getLabel('issue.labelSelection.custom', lang), value: 'custom' },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selected === null || selected === 'none') return [];
|
||||||
|
if (selected === 'custom') {
|
||||||
|
const customLabel = await promptInput(getLabel('issue.labelSelection.customPrompt', lang));
|
||||||
|
return customLabel?.split(',').map((l) => l.trim()).filter((l) => l.length > 0) ?? [];
|
||||||
|
}
|
||||||
|
return [selected];
|
||||||
|
}
|
||||||
|
|
||||||
async function promptWorktreeSettings(): Promise<WorktreeSettings> {
|
async function promptWorktreeSettings(): Promise<WorktreeSettings> {
|
||||||
const customPath = await promptInput('Worktree path (Enter for auto)');
|
const customPath = await promptInput('Worktree path (Enter for auto)');
|
||||||
const worktree: boolean | string = customPath || true;
|
const worktree: boolean | string = customPath || true;
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export {
|
|||||||
type WorktreeConfirmationResult,
|
type WorktreeConfirmationResult,
|
||||||
} from './execute/selectAndExecute.js';
|
} from './execute/selectAndExecute.js';
|
||||||
export { resolveAutoPr, postExecutionFlow, type PostExecutionOptions } from './execute/postExecution.js';
|
export { resolveAutoPr, postExecutionFlow, type PostExecutionOptions } from './execute/postExecution.js';
|
||||||
export { addTask, saveTaskFile, saveTaskFromInteractive, createIssueFromTask, createIssueAndSaveTask } from './add/index.js';
|
export { addTask, saveTaskFile, saveTaskFromInteractive, createIssueFromTask, createIssueAndSaveTask, promptLabelSelection } from './add/index.js';
|
||||||
export { watchTasks } from './watch/index.js';
|
export { watchTasks } from './watch/index.js';
|
||||||
export {
|
export {
|
||||||
listTasks,
|
listTasks,
|
||||||
|
|||||||
@ -173,6 +173,27 @@ export function resolveIssueTask(task: string): string {
|
|||||||
return issues.map(formatIssueAsTask).join('\n\n---\n\n');
|
return issues.map(formatIssueAsTask).join('\n\n---\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter labels to only those that exist on the repository.
|
||||||
|
*/
|
||||||
|
function filterExistingLabels(labels: string[]): string[] {
|
||||||
|
try {
|
||||||
|
const existing = new Set(
|
||||||
|
execFileSync('gh', ['label', 'list', '--json', 'name', '-q', '.[].name'], {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
})
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.filter((l) => l.length > 0),
|
||||||
|
);
|
||||||
|
return labels.filter((l) => existing.has(l));
|
||||||
|
} catch (err) {
|
||||||
|
log.error('Failed to fetch labels', { error: getErrorMessage(err) });
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a GitHub Issue via `gh issue create`.
|
* Create a GitHub Issue via `gh issue create`.
|
||||||
*/
|
*/
|
||||||
@ -184,7 +205,10 @@ export function createIssue(options: CreateIssueOptions): CreateIssueResult {
|
|||||||
|
|
||||||
const args = ['issue', 'create', '--title', options.title, '--body', options.body];
|
const args = ['issue', 'create', '--title', options.title, '--body', options.body];
|
||||||
if (options.labels && options.labels.length > 0) {
|
if (options.labels && options.labels.length > 0) {
|
||||||
args.push('--label', options.labels.join(','));
|
const validLabels = filterExistingLabels(options.labels);
|
||||||
|
if (validLabels.length > 0) {
|
||||||
|
args.push('--label', validLabels.join(','));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('Creating issue', { title: options.title });
|
log.info('Creating issue', { title: options.title });
|
||||||
|
|||||||
@ -103,3 +103,11 @@ retry:
|
|||||||
run:
|
run:
|
||||||
notifyComplete: "Run complete ({total} tasks)"
|
notifyComplete: "Run complete ({total} tasks)"
|
||||||
notifyAbort: "Run finished with errors ({failed})"
|
notifyAbort: "Run finished with errors ({failed})"
|
||||||
|
|
||||||
|
# ===== Issue Creation UI =====
|
||||||
|
issue:
|
||||||
|
labelSelection:
|
||||||
|
prompt: "Label:"
|
||||||
|
none: "None"
|
||||||
|
custom: "Custom input"
|
||||||
|
customPrompt: "Enter label name"
|
||||||
|
|||||||
@ -103,3 +103,11 @@ retry:
|
|||||||
run:
|
run:
|
||||||
notifyComplete: "run完了 ({total} tasks)"
|
notifyComplete: "run完了 ({total} tasks)"
|
||||||
notifyAbort: "runはエラー終了 ({failed})"
|
notifyAbort: "runはエラー終了 ({failed})"
|
||||||
|
|
||||||
|
# ===== Issue Creation UI =====
|
||||||
|
issue:
|
||||||
|
labelSelection:
|
||||||
|
prompt: "ラベル:"
|
||||||
|
none: "なし"
|
||||||
|
custom: "入力(カスタム)"
|
||||||
|
customPrompt: "ラベル名を入力"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user