resolved #52, resolved #59

This commit is contained in:
nrslib 2026-01-31 19:25:50 +09:00
parent f8fc9a7c83
commit 1e8909d512
5 changed files with 118 additions and 44 deletions

View File

@ -127,6 +127,9 @@ takt --task "fix the auth bug" -w magi -b feat/fix-auth
# Specify repository (for PR creation) # Specify repository (for PR creation)
takt --task "fix the auth bug" --auto-pr --repo owner/repo takt --task "fix the auth bug" --auto-pr --repo owner/repo
# Run workflow only — skip branch creation, commit, and push
takt --task "fix the auth bug" --skip-git
``` ```
In pipeline mode, PRs are **not** created unless `--auto-pr` is explicitly specified. In pipeline mode, PRs are **not** created unless `--auto-pr` is explicitly specified.
@ -154,6 +157,7 @@ In pipeline mode, PRs are **not** created unless `--auto-pr` is explicitly speci
| `-w, --workflow <name>` | Workflow to use | | `-w, --workflow <name>` | Workflow to use |
| `-b, --branch <name>` | Branch name (auto-generated if omitted) | | `-b, --branch <name>` | Branch name (auto-generated if omitted) |
| `--auto-pr` | Create PR after execution (interactive: skip confirmation, pipeline: enable PR) | | `--auto-pr` | Create PR after execution (interactive: skip confirmation, pipeline: enable PR) |
| `--skip-git` | Skip branch creation, commit, and push (pipeline mode, workflow-only) |
| `--repo <owner/repo>` | Repository for PR creation | | `--repo <owner/repo>` | Repository for PR creation |
## Workflows ## Workflows

View File

@ -125,6 +125,9 @@ takt --task "バグを修正" -w magi -b feat/fix-bug
# リポジトリ指定PR作成時 # リポジトリ指定PR作成時
takt --task "バグを修正" --auto-pr --repo owner/repo takt --task "バグを修正" --auto-pr --repo owner/repo
# ワークフロー実行のみブランチ作成・commit・pushをスキップ
takt --task "バグを修正" --skip-git
``` ```
パイプラインモードでは `--auto-pr` を指定しない限りPRは作成されません。 パイプラインモードでは `--auto-pr` を指定しない限りPRは作成されません。
@ -152,6 +155,7 @@ takt --task "バグを修正" --auto-pr --repo owner/repo
| `-w, --workflow <name>` | ワークフロー指定 | | `-w, --workflow <name>` | ワークフロー指定 |
| `-b, --branch <name>` | ブランチ名指定(省略時は自動生成) | | `-b, --branch <name>` | ブランチ名指定(省略時は自動生成) |
| `--auto-pr` | PR作成対話: 確認スキップ、パイプライン: PR有効化 | | `--auto-pr` | PR作成対話: 確認スキップ、パイプライン: PR有効化 |
| `--skip-git` | ブランチ作成・commit・pushをスキップパイプラインモード、ワークフロー実行のみ |
| `--repo <owner/repo>` | リポジトリ指定PR作成時 | | `--repo <owner/repo>` | リポジトリ指定PR作成時 |
## ワークフロー ## ワークフロー

View File

@ -345,4 +345,57 @@ describe('executePipeline', () => {
); );
}); });
}); });
describe('--skip-git', () => {
it('should skip branch creation, commit, push when skipGit is true', async () => {
mockExecuteTask.mockResolvedValueOnce(true);
const exitCode = await executePipeline({
task: 'Fix the bug',
workflow: 'default',
autoPr: false,
skipGit: true,
cwd: '/tmp/test',
});
expect(exitCode).toBe(0);
expect(mockExecuteTask).toHaveBeenCalledWith('Fix the bug', '/tmp/test', 'default');
// No git operations should have been called
const gitCalls = mockExecFileSync.mock.calls.filter(
(call: unknown[]) => call[0] === 'git',
);
expect(gitCalls).toHaveLength(0);
expect(mockPushBranch).not.toHaveBeenCalled();
});
it('should ignore --auto-pr when skipGit is true', async () => {
mockExecuteTask.mockResolvedValueOnce(true);
const exitCode = await executePipeline({
task: 'Fix the bug',
workflow: 'default',
autoPr: true,
skipGit: true,
cwd: '/tmp/test',
});
expect(exitCode).toBe(0);
expect(mockCreatePullRequest).not.toHaveBeenCalled();
});
it('should still return workflow failure exit code when skipGit is true', async () => {
mockExecuteTask.mockResolvedValueOnce(false);
const exitCode = await executePipeline({
task: 'Fix the bug',
workflow: 'default',
autoPr: false,
skipGit: true,
cwd: '/tmp/test',
});
expect(exitCode).toBe(3);
});
});
}); });

View File

@ -198,7 +198,8 @@ program
.option('-b, --branch <name>', 'Branch name (auto-generated if omitted)') .option('-b, --branch <name>', 'Branch name (auto-generated if omitted)')
.option('--auto-pr', 'Create PR after successful execution') .option('--auto-pr', 'Create PR after successful execution')
.option('--repo <owner/repo>', 'Repository (defaults to current)') .option('--repo <owner/repo>', 'Repository (defaults to current)')
.option('-t, --task <string>', 'Task content (triggers pipeline/non-interactive mode)'); .option('-t, --task <string>', 'Task content (triggers pipeline/non-interactive mode)')
.option('--skip-git', 'Skip branch creation, commit, and push (pipeline mode)');
// Common initialization for all commands // Common initialization for all commands
program.hook('preAction', async () => { program.hook('preAction', async () => {
@ -327,6 +328,7 @@ program
branch: opts.branch as string | undefined, branch: opts.branch as string | undefined,
autoPr: opts.autoPr === true, autoPr: opts.autoPr === true,
repo: opts.repo as string | undefined, repo: opts.repo as string | undefined,
skipGit: opts.skipGit === true,
cwd: resolvedCwd, cwd: resolvedCwd,
}); });

View File

@ -39,6 +39,8 @@ export interface PipelineExecutionOptions {
autoPr: boolean; autoPr: boolean;
/** Repository in owner/repo format */ /** Repository in owner/repo format */
repo?: string; repo?: string;
/** Skip branch creation, commit, and push (workflow-only execution) */
skipGit?: boolean;
/** Working directory */ /** Working directory */
cwd: string; cwd: string;
} }
@ -135,7 +137,7 @@ function buildPipelinePrBody(
* Returns a process exit code (0 on success, 2-5 on specific failures). * Returns a process exit code (0 on success, 2-5 on specific failures).
*/ */
export async function executePipeline(options: PipelineExecutionOptions): Promise<number> { export async function executePipeline(options: PipelineExecutionOptions): Promise<number> {
const { cwd, workflow, autoPr } = options; const { cwd, workflow, autoPr, skipGit } = options;
const globalConfig = loadGlobalConfig(); const globalConfig = loadGlobalConfig();
const pipelineConfig = globalConfig.pipeline; const pipelineConfig = globalConfig.pipeline;
let issue: GitHubIssue | undefined; let issue: GitHubIssue | undefined;
@ -164,20 +166,23 @@ export async function executePipeline(options: PipelineExecutionOptions): Promis
return EXIT_ISSUE_FETCH_FAILED; return EXIT_ISSUE_FETCH_FAILED;
} }
// --- Step 2: Create branch --- // --- Step 2: Create branch (skip if --skip-git) ---
const branch = options.branch ?? generatePipelineBranchName(pipelineConfig, options.issueNumber); let branch: string | undefined;
info(`Creating branch: ${branch}`); if (!skipGit) {
try { branch = options.branch ?? generatePipelineBranchName(pipelineConfig, options.issueNumber);
createBranch(cwd, branch); info(`Creating branch: ${branch}`);
success(`Branch created: ${branch}`); try {
} catch (err) { createBranch(cwd, branch);
error(`Failed to create branch: ${err instanceof Error ? err.message : String(err)}`); success(`Branch created: ${branch}`);
return EXIT_GIT_OPERATION_FAILED; } catch (err) {
error(`Failed to create branch: ${err instanceof Error ? err.message : String(err)}`);
return EXIT_GIT_OPERATION_FAILED;
}
} }
// --- Step 3: Run workflow --- // --- Step 3: Run workflow ---
info(`Running workflow: ${workflow}`); info(`Running workflow: ${workflow}`);
log.info('Pipeline workflow execution starting', { workflow, branch, issueNumber: options.issueNumber }); log.info('Pipeline workflow execution starting', { workflow, branch, skipGit, issueNumber: options.issueNumber });
const taskSuccess = await executeTask(task, cwd, workflow); const taskSuccess = await executeTask(task, cwd, workflow);
@ -187,52 +192,58 @@ export async function executePipeline(options: PipelineExecutionOptions): Promis
} }
success(`Workflow '${workflow}' completed`); success(`Workflow '${workflow}' completed`);
// --- Step 4: Commit & push --- // --- Step 4: Commit & push (skip if --skip-git) ---
const commitMessage = buildCommitMessage(pipelineConfig, issue, options.task); if (!skipGit && branch) {
const commitMessage = buildCommitMessage(pipelineConfig, issue, options.task);
info('Committing changes...'); info('Committing changes...');
try { try {
const commitHash = commitChanges(cwd, commitMessage); const commitHash = commitChanges(cwd, commitMessage);
if (commitHash) { if (commitHash) {
success(`Changes committed: ${commitHash}`); success(`Changes committed: ${commitHash}`);
} else { } else {
info('No changes to commit'); info('No changes to commit');
}
info(`Pushing to origin/${branch}...`);
pushBranch(cwd, branch);
success(`Pushed to origin/${branch}`);
} catch (err) {
error(`Git operation failed: ${err instanceof Error ? err.message : String(err)}`);
return EXIT_GIT_OPERATION_FAILED;
} }
info(`Pushing to origin/${branch}...`);
pushBranch(cwd, branch);
success(`Pushed to origin/${branch}`);
} catch (err) {
error(`Git operation failed: ${err instanceof Error ? err.message : String(err)}`);
return EXIT_GIT_OPERATION_FAILED;
} }
// --- Step 5: Create PR (if --auto-pr) --- // --- Step 5: Create PR (if --auto-pr) ---
if (autoPr) { if (autoPr) {
info('Creating pull request...'); if (skipGit) {
const prTitle = issue ? issue.title : (options.task ?? 'Pipeline task'); info('--auto-pr is ignored when --skip-git is specified (no push was performed)');
const report = `Workflow \`${workflow}\` completed successfully.`; } else if (branch) {
const prBody = buildPipelinePrBody(pipelineConfig, issue, report); info('Creating pull request...');
const prTitle = issue ? issue.title : (options.task ?? 'Pipeline task');
const report = `Workflow \`${workflow}\` completed successfully.`;
const prBody = buildPipelinePrBody(pipelineConfig, issue, report);
const prResult = createPullRequest(cwd, { const prResult = createPullRequest(cwd, {
branch, branch,
title: prTitle, title: prTitle,
body: prBody, body: prBody,
repo: options.repo, repo: options.repo,
}); });
if (prResult.success) { if (prResult.success) {
success(`PR created: ${prResult.url}`); success(`PR created: ${prResult.url}`);
} else { } else {
error(`PR creation failed: ${prResult.error}`); error(`PR creation failed: ${prResult.error}`);
return EXIT_PR_CREATION_FAILED; return EXIT_PR_CREATION_FAILED;
}
} }
} }
// --- Summary --- // --- Summary ---
console.log(); console.log();
status('Issue', issue ? `#${issue.number} "${issue.title}"` : 'N/A'); status('Issue', issue ? `#${issue.number} "${issue.title}"` : 'N/A');
status('Branch', branch); status('Branch', branch ?? '(current)');
status('Workflow', workflow); status('Workflow', workflow);
status('Result', 'Success', 'green'); status('Result', 'Success', 'green');