From c5739f12829015cac4c00f5de46bbe3fb761ef89 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 17 Mar 2026 23:45:29 -0400 Subject: [PATCH] feat: auto-create PR on milestone completion (#687) (#1084) * docs: add Node LTS pinning guide for macOS Homebrew users New doc (docs/node-lts-macos.md) explains how to pin Node 24 LTS via Homebrew to avoid running on odd-numbered development releases. Covers brew install/link/pin, version managers as alternatives, and verification steps. Added notice banner in README linking to the guide. * feat: auto-create PR on milestone completion (#687) New git preferences: - git.auto_pr (boolean, default false): create a PR when a milestone completes via gh CLI - git.pr_target_branch (string, default main branch): target branch for auto-created PRs (e.g. develop, qa, staging) Implementation: - GitPreferences: added auto_pr and pr_target_branch fields - preferences.ts: added validation for both fields - auto-worktree.ts: after push, pushes milestone branch and creates PR via 'gh pr create' (non-fatal on failure) Documentation: - configuration.md: added fields to git config block, table, and new git.auto_pr section with requirements and flow - git-strategy.md: added Automatic Pull Requests section with Gitflow example config --- docs/configuration.md | 27 ++++++++++++++++ docs/git-strategy.md | 16 ++++++++++ src/resources/extensions/gsd/auto-worktree.ts | 31 +++++++++++++++++-- src/resources/extensions/gsd/git-service.ts | 10 ++++++ src/resources/extensions/gsd/preferences.ts | 11 +++++++ 5 files changed, 93 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index f3379841e..c71bd56a6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -238,6 +238,8 @@ git: commit_docs: true # commit .gsd/ artifacts to git (set false to keep local) manage_gitignore: true # set false to prevent GSD from modifying .gitignore worktree_post_create: .gsd/hooks/post-worktree-create # script to run after worktree creation + auto_pr: false # create a PR on milestone completion (requires push_branches) + pr_target_branch: develop # target branch for auto-created PRs (default: main branch) ``` | Field | Type | Default | Description | @@ -254,6 +256,8 @@ git: | `commit_docs` | boolean | `true` | Commit `.gsd/` planning artifacts to git. Set `false` to keep local-only | | `manage_gitignore` | boolean | `true` | When `false`, GSD will not modify `.gitignore` at all — no baseline patterns, no self-healing. Use if you manage your own `.gitignore` | | `worktree_post_create` | string | (none) | Script to run after worktree creation. Receives `SOURCE_DIR` and `WORKTREE_DIR` env vars | +| `auto_pr` | boolean | `false` | Automatically create a pull request when a milestone completes. Requires `auto_push: true` and `gh` CLI installed and authenticated | +| `pr_target_branch` | string | (main branch) | Target branch for auto-created PRs (e.g. `develop`, `qa`). Defaults to `main_branch` if not set | #### `git.worktree_post_create` @@ -280,6 +284,29 @@ ln -sf "$SOURCE_DIR/assets" "$WORKTREE_DIR/assets" The path can be absolute or relative to the project root. The script runs with a 30-second timeout. Failure is non-fatal — GSD logs a warning and continues. +#### `git.auto_pr` + +Automatically create a pull request when a milestone completes. Designed for teams using Gitflow or branch-based workflows where work should go through PR review before merging to a target branch. + +```yaml +git: + auto_push: true + auto_pr: true + pr_target_branch: develop # or qa, staging, etc. +``` + +**Requirements:** +- `auto_push: true` — the milestone branch must be pushed before a PR can be created +- [`gh` CLI](https://cli.github.com/) installed and authenticated (`gh auth login`) + +**How it works:** +1. Milestone completes → GSD squash-merges the worktree to the main branch +2. Pushes the main branch to remote (if `auto_push: true`) +3. Pushes the milestone branch to remote +4. Creates a PR from the milestone branch to `pr_target_branch` via `gh pr create` + +If `pr_target_branch` is not set, the PR targets the `main_branch` (or auto-detected main branch). PR creation failure is non-fatal — GSD logs and continues. + ### `notifications` Control what notifications GSD sends during auto mode: diff --git a/docs/git-strategy.md b/docs/git-strategy.md index 75e32e6b5..40576256f 100644 --- a/docs/git-strategy.md +++ b/docs/git-strategy.md @@ -147,6 +147,22 @@ git: main_branch: main # primary branch name commit_docs: true # commit .gsd/ to git isolation: worktree # "worktree", "branch", or "none" + auto_pr: false # create PR on milestone completion + pr_target_branch: develop # PR target branch (default: main) +``` + +### Automatic Pull Requests + +For teams using Gitflow or branch-based workflows, GSD can automatically create a pull request when a milestone completes: + +```yaml +git: + auto_push: true + auto_pr: true + pr_target_branch: develop +``` + +This pushes the milestone branch and creates a PR targeting `develop` (or whichever branch you specify). Requires `gh` CLI installed and authenticated. See [git.auto_pr](./configuration.md#gitauto_pr) for details. ``` ### `commit_docs: false` diff --git a/src/resources/extensions/gsd/auto-worktree.ts b/src/resources/extensions/gsd/auto-worktree.ts index c91c7de5e..60d90f3ac 100644 --- a/src/resources/extensions/gsd/auto-worktree.ts +++ b/src/resources/extensions/gsd/auto-worktree.ts @@ -487,7 +487,7 @@ export function mergeMilestoneToMain( originalBasePath_: string, milestoneId: string, roadmapContent: string, -): { commitMessage: string; pushed: boolean } { +): { commitMessage: string; pushed: boolean; prCreated: boolean } { const worktreeCwd = process.cwd(); const milestoneBranch = autoWorktreeBranch(milestoneId); @@ -602,6 +602,33 @@ export function mergeMilestoneToMain( } } + // 9b. Auto-create PR if enabled (requires push_branches + push succeeded) + let prCreated = false; + if (prefs.auto_pr === true && pushed) { + const remote = prefs.remote ?? "origin"; + const prTarget = prefs.pr_target_branch ?? mainBranch; + try { + // Push the milestone branch to remote first + execSync(`git push ${remote} ${milestoneBranch}`, { + cwd: originalBasePath_, + stdio: ["ignore", "pipe", "pipe"], + encoding: "utf-8", + }); + // Create PR via gh CLI + execSync( + `gh pr create --base "${prTarget}" --head "${milestoneBranch}" --title "Milestone ${milestoneId} complete" --body "Auto-created by GSD on milestone completion."`, + { + cwd: originalBasePath_, + stdio: ["ignore", "pipe", "pipe"], + encoding: "utf-8", + }, + ); + prCreated = true; + } catch { + // PR creation failure is non-fatal — gh may not be installed or authenticated + } + } + // 10. Remove worktree directory first (must happen before branch deletion) try { removeWorktree(originalBasePath_, milestoneId, { branch: null as unknown as string, deleteBranch: false }); @@ -620,5 +647,5 @@ export function mergeMilestoneToMain( originalBase = null; nudgeGitBranchCache(previousCwd); - return { commitMessage, pushed }; + return { commitMessage, pushed, prCreated }; } diff --git a/src/resources/extensions/gsd/git-service.ts b/src/resources/extensions/gsd/git-service.ts index 97e39ef34..d3b3f0a09 100644 --- a/src/resources/extensions/gsd/git-service.ts +++ b/src/resources/extensions/gsd/git-service.ts @@ -66,6 +66,16 @@ export interface GitPreferences { * Failure is non-fatal — logged as a warning. */ worktree_post_create?: string; + /** When true, automatically create a pull request after milestone completion. + * The PR targets `pr_target_branch` (default: the main branch). + * Requires `push_branches: true` and a configured remote. + * Default: false. + */ + auto_pr?: boolean; + /** Target branch for auto-created PRs (e.g. "develop", "qa"). + * Default: the main branch (from `main_branch` or auto-detected). + */ + pr_target_branch?: string; } export const VALID_BRANCH_NAME = /^[a-zA-Z0-9_\-\/.]+$/; diff --git a/src/resources/extensions/gsd/preferences.ts b/src/resources/extensions/gsd/preferences.ts index d2bf0a72f..270559717 100644 --- a/src/resources/extensions/gsd/preferences.ts +++ b/src/resources/extensions/gsd/preferences.ts @@ -1368,6 +1368,17 @@ export function validatePreferences(preferences: GSDPreferences): { errors.push("git.worktree_post_create must be a non-empty string (path to script)"); } } + if (g.auto_pr !== undefined) { + if (typeof g.auto_pr === "boolean") git.auto_pr = g.auto_pr; + else errors.push("git.auto_pr must be a boolean"); + } + if (g.pr_target_branch !== undefined) { + if (typeof g.pr_target_branch === "string" && g.pr_target_branch.trim()) { + git.pr_target_branch = g.pr_target_branch.trim(); + } else { + errors.push("git.pr_target_branch must be a non-empty string (branch name)"); + } + } // Deprecated: merge_to_main is ignored (branchless architecture). if (g.merge_to_main !== undefined) { warnings.push("git.merge_to_main is deprecated — milestone-level merge is now always used. Remove this setting.");