Phase CI gate: phase-pr restarts CI on every resume, cannot resume on Forgejo, and expires against a green pipeline #437
Labels
No labels
agent-ready
bug
contract
documentation
duplicate
enhancement
good first issue
help wanted
invalid
needs-human
needs-input
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
iterative-works/iw-cli#437
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Three defects in the phase CI gate. Defects 1 and 2 were hit while shipping Phase 1 of IW-423. Defect 3 was found by reading the code afterwards and is confirmed by a harness probe. Together they make the gate unable to finish a phase without manual intervention.
1. A 409 is not recognised as "already exists"
./iw phase-prcreated PR 434, waited 481s, and exited 3 asking to be run again. The second run failed:PhasePr.resolvePullRequest(core/commands/PhasePr.scala:139-154) is written for exactly this case — on a duplicate it falls back to the PR url on the card. It gates that fallback ondescribesExisting(:157-158), which matcheserr.toLowerCase.contains("already exists"). ButForgejoClient.createPullRequest(core/adapters/ForgejoClient.scala:391-399) collapses every unhandled status intos"Forgejo API error: ${response.code}", so a 409 Conflict arrives as the stringForgejo API error: 409.The comment at
PhasePr.scala:131-137("A gate that ends unfinished asks to be called again... The card holds the url the first invocation recorded") describes behaviour the code does not deliver on this forge.Fix: handle
StatusCode.Conflictexplicitly inForgejoClient.createPullRequest, and match on the status inPhasePrrather than on prose. A retry path keyed on the text of an error message is only as good as the adapter that formats it. Wants a test: a 409 must reachdescribesExisting, whatever wording the forge uses.2. The CI wait budget is wall-clock since the gate first ran, not time spent waiting
CI on PR 434 was green — combined state
successon the branch tip, verified through/api/v1/repos/.../commits/<sha>/status../iw phase-merge --max-retries 0nonetheless refused with:The 30m
--timeouthad been consumed by the earlierphase-prgate plus the manual waiting that defect 1 forced — none of which was time this invocation spent waiting.Fix: decide deliberately whether the budget measures elapsed wall-clock or accumulated polling, and whether a gate should refuse at all once the forge reports a terminal green. A budget measured from the first gate run expires against a green pipeline whenever anything delays the caller between the gate and the merge.
3.
phase-prrestarts CI every time it hands the wait back, so the wait never convergesThis is the defect that makes the exit-3 loop non-terminating. It is forge-independent.
phase-prcommits and pushesreview-state.jsonwhen its poll window ends. The push moves the head of the pull request branch, so the forge starts CI again on a new commit. The path:CiGate.markPending(core/commands/CiGate.scala:240) writes the card, andheartbeat(:265) rewrites it every poll interval.ReviewStateUpdateralways refresheslast_updated(core/model/ReviewStateUpdater.scala:85), so the file is always dirty.CiVerdict.Running.PhasePr.finishcallspersistfor that verdict (core/commands/PhasePr.scala:234).persist(:336) runscommitFileWithRetryand thengit pushon the phase branch.concurrency: cancel-in-progress,.github/workflows/ci.yml:19) and starts a fresh one.Any pipeline longer than the poll window can therefore never be proved green. The 30m budget survives across invocations, so the phase ends as
ci_failedalthough nothing failed.--batchhas the same defect through a different door: the gate policy isCommitPolicy.CommitBeforeGate(:190), and the entrygit pushof the next invocation (:115) pushes that commit.Raising
--poll-windowis not a way out. A foreground tool call is capped at 600000 ms, so the window can never exceed about 9 minutes for an agent.Confirmed. A probe assertion added to the existing exit-3 harness test fails:
The push buys nothing. The dashboard reads
review-state.jsonfrom the worktree path (dashboard/jvm/src/ReviewStateService.scala:152), not from the forge. A wait whose card stays local is still visible.iw ci-gateis already correct and is the model to copy: policyDeferToCaller, and theRunningcase only prints the resume report (core/commands/CiGateCommand.scala:135,:147).Fix. Hold this invariant: while a CI wait is unfinished,
phase-prmust not move the head of the branch.PhasePr.finish, removepersist(r, env)from theCiVerdict.Runningcase. Keep the card write. The file stays uncommitted until the verdict is terminal.PhasePr.gate, useCommitPolicy.DeferToCallerfor--batchtoo. Move the card commit intodoBatchMerge, immediately beforemergeSquashAndDelete— the only step that needs a clean tree, becausegh pr merge --delete-branchdoes a checkout.The terminal paths (green, red, no-CI) keep their commit and push.
Two alternatives were considered and rejected:
paths-ignoreon the workflow. Per project, and it leaves the new head with no run at all, so the gate then reportsRunsForOtherShaOnly.phase-mergeis safe today only because it has no entry push.Tests to add:
PhasePrHarnessTest: the exit-3 path makes no commit (the probe above).PhasePrHarnessTest: the exit-3 path under--batchmakes no commit either.PhasePrHarnessTest: a green--batchrun still commits the card before the merge.Defects 1 and 2 recorded during IW-423; see
project-management/issues/IW-423/LESSONS.mdentries 10 and 12.Phase CI gate: phase-pr cannot resume on Forgejo, phase-merge expires against a green pipelineto Phase CI gate: phase-pr restarts CI on every resume, cannot resume on Forgejo, and expires against a green pipeline