feat(gsd): create draft PR on milestone completion when git.auto_pr enabled (#1627)

Adds createDraftPR() to git-service.ts and hooks it into the milestone
transition block in auto-loop.ts. Best-effort, non-fatal on failure.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
TÂCHES 2026-03-20 11:48:13 -06:00 committed by GitHub
parent 70cf14f72d
commit 1d3e3ee46b
2 changed files with 79 additions and 0 deletions

View file

@ -840,6 +840,25 @@ export async function autoLoop(
// Worktree lifecycle on milestone transition — merge current, enter next
deps.resolver.mergeAndExit(s.currentMilestoneId!, ctx.ui);
// Opt-in: create draft PR on milestone completion
if (prefs?.git?.auto_pr) {
try {
const { createDraftPR } = await import("./git-service.js");
const prUrl = createDraftPR(
s.basePath,
s.currentMilestoneId!,
`[GSD] ${s.currentMilestoneId} complete`,
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
);
if (prUrl) {
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
}
} catch {
// Non-fatal — PR creation is best-effort
}
}
deps.invalidateAllCaches();
state = await deps.deriveState(s.basePath);
@ -893,6 +912,24 @@ export async function autoLoop(
// All milestones complete — merge milestone branch before stopping
if (s.currentMilestoneId) {
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
// Opt-in: create draft PR on milestone completion
if (prefs?.git?.auto_pr) {
try {
const { createDraftPR } = await import("./git-service.js");
const prUrl = createDraftPR(
s.basePath,
s.currentMilestoneId,
`[GSD] ${s.currentMilestoneId} complete`,
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
);
if (prUrl) {
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
}
} catch {
// Non-fatal — PR creation is best-effort
}
}
}
deps.sendDesktopNotification(
"GSD",
@ -974,6 +1011,24 @@ export async function autoLoop(
// Milestone merge on complete (before closeout so branch state is clean)
if (s.currentMilestoneId) {
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
// Opt-in: create draft PR on milestone completion
if (prefs?.git?.auto_pr) {
try {
const { createDraftPR } = await import("./git-service.js");
const prUrl = createDraftPR(
s.basePath,
s.currentMilestoneId,
`[GSD] ${s.currentMilestoneId} complete`,
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
);
if (prUrl) {
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
}
} catch {
// Non-fatal — PR creation is best-effort
}
}
}
deps.sendDesktopNotification(
"GSD",

View file

@ -584,6 +584,30 @@ export class GitServiceImpl {
}
// ─── Draft PR Creation ─────────────────────────────────────────────────────
/**
* Create a draft pull request for a completed milestone using `gh pr create`.
* Returns the PR URL on success, or null on failure.
* Non-fatal: callers should treat failure as best-effort.
*/
export function createDraftPR(
basePath: string,
milestoneId: string,
title: string,
body: string,
): string | null {
try {
const result = execSync(
`gh pr create --draft --title ${JSON.stringify(title)} --body ${JSON.stringify(body)}`,
{ cwd: basePath, encoding: "utf8", timeout: 30000, env: GIT_NO_PROMPT_ENV },
);
return result.trim();
} catch {
return null;
}
}
// ─── Factory ───────────────────────────────────────────────────────────────
/** Create a GitServiceImpl with the current effective git preferences. */