Skip to content

fix(pull-sdlc): clean no-op sync, honest -WhatIf, opt-in auto-merge#225

Open
MarkMichaelis wants to merge 2 commits into
mainfrom
fix/224-sdlc-noop-pr
Open

fix(pull-sdlc): clean no-op sync, honest -WhatIf, opt-in auto-merge#225
MarkMichaelis wants to merge 2 commits into
mainfrom
fix/224-sdlc-noop-pr

Conversation

@MarkMichaelis

Copy link
Copy Markdown
Contributor

Summary

Makes Pull-SDLC.ai.ps1 behave as a clean "update me" command. Fixes the confusing behavior where a no-op sync on main still opened an empty PR and -WhatIf disagreed with a real run.

Root causes fixed

  • RC1 -- spurious empty commit/PR. Set-SdlcSyncState always rewrote the syncedAt timestamp, dirtying the tree even when 0 ops applied, which produced an empty sync commit and (in auto-worktree mode) an empty PR. The state write is now gated: an already-current 0-op run touches nothing -- no commit, no push, no PR.
  • RC2 -- dishonest -WhatIf. -WhatIf now previews the consumer-owned starter files that would be scaffolded and states the PR/merge outcome a real run would perform, instead of printing only the file-diff ops.
  • RC3 -- scaffolds lost in the scratch worktree. In auto-worktree mode, scaffolded starter files now land in the directory the command was invoked from (left untracked), not the throwaway .worktrees/sdlc-sync tree. Threaded via a new internal ScaffoldTargetRoot parameter.
  • RC4 -- no way to finish the flow. A new Invoke-SyncPRMerge helper can squash-merge the sync PR through the standard -Confirm mechanism. Default (no -Confirm) opens the PR without merging; -Confirm:$false auto-squash-merges and fast-forwards local main; a blocked merge leaves the PR open for manual action.

Behavior contract

Invocation Result
No upstream changes True no-op: no commit, no push, no PR
Upstream changes on main Opens a PR against main (not merged)
... -Confirm:$false on main Opens the PR, squash-merges it, updates local main
-WhatIf Previews ops, scaffold files, and PR/merge outcome; writes nothing
Merge conflict / drift Manual action required (unchanged drift guard)

Merge control uses the standard -Confirm / -WhatIf / ShouldProcess mechanism -- no new user-facing switches.

Testing

Invoke-Pester -Path .\Pull-SDLC.ai.Tests.ps1 -- 248 passed, 0 failed. Added behavior-first tests for each root cause (no-op-push-nothing, scaffold-into-invoking-dir, Invoke-SyncPRMerge merge/blocked/declined, auto-worktree merge control, honest -WhatIf).

Assumptions

  • The confirmation prompt uses a custom 2-argument ShouldProcess(target, action) message: Performing the operation "Squash-merge and delete branch 'chore/sdlc-sync'" on target "main (PR <ref>)". (surfaced because the earlier question about the prompt wording went unanswered).
  • Merge method for the consumer sync PR is squash with branch deletion.

Closes #224

MarkMichaelis and others added 2 commits July 4, 2026 19:37
A no-op sync on main bumped the syncedAt timestamp, dirtying the tree,
producing an empty sync commit and opening an empty PR. -WhatIf said
"nothing to sync" while a real run did visible work, and scaffolded
starter files landed in the throwaway worktree instead of the user branch.

- RC1: gate the sync-state write so an already-current 0-op run touches
  nothing (no commit, no push, no PR).
- RC2: honest -WhatIf -- preview the scaffold files and the PR/merge a
  real run would perform.
- RC3: scaffold consumer-owned files into the invoking directory, not the
  scratch worktree, via ScaffoldTargetRoot.
- RC4: optional squash-merge of the sync PR through the standard -Confirm
  path (Invoke-SyncPRMerge); default opens the PR without merging,
  -Confirm:$false auto-merges and updates local main, a blocked merge
  leaves the PR open for manual action.

Closes #224

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…, WhatIf accuracy

Follow-up to the RC1-RC4 fix, from an independent code review:

- Delete the local scratch branch after a successful squash-merge. Removing
  the worktree alone left 'chore/sdlc-sync' at pre-squash history, which the
  next run reused (re-opening a PR with stale content).
- Check git exit codes after the post-merge fast-forward and worktree
  cleanup; warn with remediation and return $false when the PR merged but the
  local update did not complete, instead of silently reporting success.
- Make the -WhatIf preview mirror the real run's decision inputs: it now
  accounts for -CommitOnMain (direct-commit vs PR) and treats a stale/missing
  sync-state as a change even when there are zero file ops.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@MarkMichaelis

Copy link
Copy Markdown
Contributor Author

@copilot review

Copilot AI commented Jul 5, 2026

Copy link
Copy Markdown

@copilot review

Code review complete. No issues found that require changes. Here's the summary:

Test run: 245 passed, 1 failed, 4 skipped (250 total). The 1 failure (does not throw "Count cannot be found" when no templates need scaffolding) is pre-existing -- it uses -WindowStyle Hidden in Start-Process, which is Windows-only and was present in the base commit before this PR.

RC1 -- no-op gate ($didWork -or -not $stateCurrent): Correctly prevents Set-SdlcSyncState from bumping syncedAt on an already-current 0-op run. Logic mirrors the WhatIf preview's $wouldChange check so the two can't diverge.

RC2 -- honest -WhatIf: The -not $WhatIfPreference guard at the auto-worktree branch (line 2366) ensures WhatIf runs in the invoking branch's context rather than delegating to the worktree flow, so the scaffold preview and PR/merge description always match what a real run would actually do.

RC3 -- scaffold target: $ScaffoldTargetRoot is threaded to $scaffoldTarget in both the WhatIf preview path and the real scaffold path; $originUrl detection uses $scaffoldTarget (not CWD) so the upstream-repo skip is evaluated against the right tree.

RC4 -- Invoke-SyncPRMerge:

  • gh pr merge runs before Push-Location $RepoRoot but $PrRef is a full URL so gh doesn't need a repo CWD. ✓
  • Force-delete (-D) of the scratch branch after squash-merge is correct (squash-merge doesn't make the branch an ancestor of the protected branch, so -d would refuse). ✓
  • Two-tier failure handling (merge failure vs. post-merge local-cleanup failure) with targeted warnings and accurate $localOk return value. ✓
  • ShouldProcess in Invoke-SyncPRMerge (not in Invoke-AutoWorktreeSync) is the right level to gate the destructive operation; $WhatIfPreference/$ConfirmPreference propagate automatically through the call chain. ✓

$ghRepoArgs scoping: Set inside the try block of Invoke-AutoWorktreeSync, accessible after finally { Pop-Location } because PowerShell scopes variables to the function, not the block. ✓

No actionable changes needed.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adjusts Pull-SDLC.ai.ps1 to behave like a clean “update me” command by eliminating no-op sync commits/PRs, making -WhatIf reflect real outcomes, ensuring scaffolds land in the invoking working tree (not the scratch worktree), and adding an opt-in PR squash-merge flow using standard -Confirm / ShouldProcess semantics.

Changes:

  • Gate .sdlc-ai-sync.json updates so a truly no-op rerun doesn’t rewrite the state file (and therefore doesn’t create empty commits/PRs).
  • Add Invoke-SyncPRMerge and thread an internal merge intent through auto-worktree mode to optionally squash-merge the sync PR.
  • Improve -WhatIf output to preview scaffolded files and PR/merge outcomes; scaffold consumer-owned files into the invoking directory in auto-worktree mode.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
Pull-SDLC.ai.ps1 Adds PR squash-merge helper, gates sync-state writes to prevent no-op commits, previews full outcomes under -WhatIf, and redirects scaffolding into the invoking directory in auto-worktree mode.
Pull-SDLC.ai.Tests.ps1 Adds/updates Pester coverage for no-op sync behavior, scaffold location, auto-worktree merge control, and honest -WhatIf messaging.

Comment thread Pull-SDLC.ai.ps1
Comment on lines +1409 to +1412
git -C $RepoRoot worktree prune 2>&1 | Write-Information
# -D (not -d): after a squash-merge the scratch branch is not an ancestor of
# $ProtectedBranch, so git does not consider it "merged"; force-delete it.
git -C $RepoRoot branch -D $SyncBranch 2>&1 | Write-Information
Comment thread Pull-SDLC.ai.ps1
Comment on lines +1368 to +1371
if (-not $PSCmdlet.ShouldProcess("$ProtectedBranch (PR $PrRef)", "Squash-merge and delete branch '$SyncBranch'")) {
Write-Information "PR opened but not merged: $PrUrl"
return $false
}
Comment thread Pull-SDLC.ai.ps1
Comment on lines +1669 to +1671
else {
Write-Information "PR opened but not merged. Rerun with -Confirm:`$false to squash-merge it into '$ProtectedBranch' automatically."
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(Pull-SDLC): no-op run opens empty PR; misleading -WhatIf; scaffold + auto-merge

3 participants