Commit 7380350
fix(github): don't add commits to an already-open PR (#95)
* fix(github): short-circuit CreatePullRequest when an open PR covers the head branch
The GitHub engine's CreatePullRequest had a silent-commit footgun that
produced the "311 commits on one PR" incident observed on
zelyo-operator-fix/demo-app-payment-service PR #12.
Flow before the fix:
1. getRef → base SHA
2. createRef returns 422 "branch exists" — engine swallows and continues
3. createOrUpdateFile for each file — COMMITS LAND ON THE EXISTING PR's
BRANCH (this is the regression)
4. openPR returns 422 "a pull request already exists" — error bubbles up
5. Controller catches the error but the file commit already shipped
6. Next reconcile: same branch, another commit pile
This was only partially mitigated by the controller-layer existingBranches
dedup shipped in #91 and #94. That mitigation misses when ListOpenPRs
fails, when pagination hits the safety cap, or when any other caller of
CreatePullRequest skips the upstream check. The engine itself needed to
be the last line of defense.
Fix: after createRef returns 422, query GitHub for any open PR whose head
branch is exactly pr.HeadBranch. If one exists, log and return it — no
commit, no duplicate openPR. If no open PR exists (legitimate case when a
user manually closed a PR but left the branch), proceed with commit +
openPR as before. If the dedup lookup ITSELF fails, fail closed —
remediation defers to the next cycle rather than risk polluting an open
PR with duplicate commits.
New tests:
- SkipsWhenOpenPRExists: createRef=422 + open PR exists → asserts PUT
/contents and POST /pulls are never called, existing PR is returned
- BranchExistsWithoutOpenPR: createRef=422 + empty PR list → asserts
commit + openPR both run (the legitimate orphan-branch case)
- BranchExistsLookupFails: createRef=422 + PR list returns 500 → asserts
error return, no commit, no openPR (fail-closed)
Uses GitHub's head=owner:ref query param to scope the lookup server-side
(only one open PR per head branch is possible, so no pagination needed),
with in-memory ref equality as a defense-in-depth filter against mocks
or proxies that ignore the query.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* address PR review: typed APIError, owner:branch dedup match, shared PR struct
Three reviewer asks on #95:
1. Codex P2 — findOpenPRByHeadBranch was filtering on head.ref alone,
so a fork PR using the same branch name could false-match the dedup
check when a proxy/mock ignores the server-side head= query. Switch
to matching on head.label ("owner:ref"), which GitHub always sets
to the canonical identifier.
2. Gemini medium — string-matching "422" in the error message was
brittle. doRequest now returns a typed *APIError with StatusCode
and Body; the createRef 422 check uses errors.As for a direct
integer comparison against http.StatusUnprocessableEntity.
3. Gemini medium — the PR-response anonymous struct was duplicated
between findOpenPRByHeadBranch and ListOpenPRs. Extract
githubPullResponse at package scope and reuse.
Tests:
- Existing SkipsWhenOpenPRExists mock now sets head.label so the
engine's new equality check matches.
- New DoesNotMatchForkPRWithSameRef: server (simulating a proxy that
ignored head= query) returns a fork PR with head.label =
"forkuser:<branch>" distinct from our owner. Asserts the engine
proceeds with its own commit + openPR instead of false-matching.
- New APIErrorTypedUnwrap: pins the typed-error contract. Covers
both the direct-%w-wrap path callers use and the live doRequest
path, so a future refactor that swallows the type breaks here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 21e3eac commit 7380350
2 files changed
Lines changed: 526 additions & 15 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
23 | 24 | | |
24 | 25 | | |
25 | 26 | | |
| |||
32 | 33 | | |
33 | 34 | | |
34 | 35 | | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
35 | 71 | | |
36 | 72 | | |
37 | 73 | | |
| |||
69 | 105 | | |
70 | 106 | | |
71 | 107 | | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
72 | 127 | | |
73 | | - | |
74 | | - | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
75 | 134 | | |
76 | 135 | | |
77 | | - | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
78 | 148 | | |
79 | 149 | | |
80 | 150 | | |
| |||
147 | 217 | | |
148 | 218 | | |
149 | 219 | | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
150 | 266 | | |
151 | 267 | | |
152 | 268 | | |
| |||
165 | 281 | | |
166 | 282 | | |
167 | 283 | | |
168 | | - | |
169 | | - | |
170 | | - | |
171 | | - | |
172 | | - | |
173 | | - | |
174 | | - | |
175 | | - | |
176 | | - | |
177 | | - | |
178 | | - | |
| 284 | + | |
179 | 285 | | |
180 | 286 | | |
181 | 287 | | |
| |||
382 | 488 | | |
383 | 489 | | |
384 | 490 | | |
385 | | - | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
386 | 495 | | |
387 | 496 | | |
388 | 497 | | |
| |||
0 commit comments