* fix(cli): make GitHub Actions setup flow idempotent and stop bundling local changes (#904)
The setup flow opened duplicate PRs and swept unrelated local changes into
the generated branch. Two root causes in `openWorkflowPullRequest`:
1. Non-idempotent: `findUniqueBranchName` only avoided a *local* branch
collision by appending a timestamp, and nothing checked for an
already-open setup PR. A re-run (the workflow file lives only on the prior
PR's branch, so it's re-written each run) minted `…-<timestamp>` branches
and opened a second PR. Now we `gh pr list` for an open PR whose head is
under the setup prefix and surface it (`pr-exists`) instead of opening a
duplicate.
2. Dirty-tree bundling: `git checkout -b <new> origin/<default>` carries the
index across and the whole-index `git commit` then committed whatever the
user had staged (modified app files, a deleted/rewritten lockfile). Now we
bail (`working-tree-dirty`) when the tree has tracked changes other than
the workflow file itself, falling back to staging the workflow file. The
workflow path is excluded from the check so the v1→v2 upgrade flow (which
modifies the tracked workflow in place) still works.
Both callers handle the new `pr-exists` status; the runner / gh-availability
probe are now injectable for hermetic tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(cli): harden setup-flow fix from adversarial review
Findings from an adversarial review+deslop pass over the #904 fix:
- fix(correctness): normalize `workflowRelative` with `toForwardSlashes` so the
`:!` exclude pathspec matches git's forward-slash repo paths on Windows.
Without it, `path.relative` yields backslashes there, git's magic pathspec
doesn't treat them as separators, and the upgrade flow (tracked workflow
modified in place) would always report `working-tree-dirty` and never open
the PR. Applied in `openWorkflowPullRequest` and `stageWorkflowFile`.
- fix(correctness): on the upgrade flow's `pr-exists` path, restore the
original @v1 workflow content. `pr-exists` returns without touching git
state, so the @v2 edit written before the call would otherwise linger
uncommitted — matching the pr-opened path's "bump lives only on the PR
branch" contract.
- chore: extract the `gh pr list` cap to `GH_PR_LIST_MAX` in constants.ts
(AGENTS.md: no inline magic numbers).
- test: assert the dirty-tree guard skips add/commit/push (not just checkout),
the happy path stages ONLY the workflow file (no `git add -A`/`.`), and
branch-pushed keeps the branch (no `git branch -D`) and restores HEAD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): trigger duplicate-PR guard on match presence, not URL truthiness
Bot review (Cursor) flagged that the idempotency short-circuit keyed on a
truthy URL: if `gh pr list` returned a matching setup head branch with a
missing/empty `url`, `findExistingSetupPullRequest` returned null and the
flow could still mint a branch and open a duplicate PR — the exact failure
the guard exists to prevent. `gh` always populates `url` for `--json url`, so
this is defensive, but the headline #904 guarantee shouldn't hinge on it.
Return the matched PR object and short-circuit on its existence; use its url
only for the `pr-exists` message (empty when absent). Adds a regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>