phase-pr treats 'no pipeline yet' as 'no CI' and passes the gate before the pipeline is created #485
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
iterative-works/iw-cli#485
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?
This issue was refiled with corrected evidence on 2026-09-01. The original text claimed a
pipeline that never started; that was wrong — see the correction note at the bottom.
iw phase-prexits 0 and writes review-stateawaiting_review— documented as "CI is green, orthere is no CI" — in the window between pushing the branch and the forge creating the pipeline for
that push. Finding no pipeline, the gate reads "no pipeline" as "no CI configured" and reports the
phase verified.
Observed
MR !426 on
CMI/mdr/medeca-modul-poptavky(GitLab, gitlab.e-bs.cz), issue MEDECA-447 phase 01.phase-prwrites review-stateawaiting_review2026-08-28T23:58:22.678Z2026-08-28T23:58:29.779Z2026-08-28T23:58:58.279Zsuccess,duration: 12532026-08-29T00:19:51.510ZThe gate declared the phase CI-verified 7 seconds before the pipeline existed and ~21 minutes
before it finished. The phase was merged on that basis. (The build did in fact pass, so nothing
broken shipped — but the gate's success was independent of that outcome and would have read the
same had it failed.)
Expected
After pushing,
phase-prmust not conclude "there is no CI" until it has given the forge time tocreate the pipeline for the pushed SHA. "No pipeline object yet" and "this project runs no CI" are
different states, and only the second one satisfies the gate.
A workable discriminator: resolve the pipeline for the pushed SHA and require either a pipeline
to appear within a grace window, or positive evidence that the project has no CI configuration at
all — rather than inferring absence from one immediate query.
A trap for whoever implements this
GET /projects/:id/merge_requests/:iid/pipelinesreturnsstarted_at: nullfor every pipelineit lists, including ones that ran and succeeded. Only
GET /projects/:id/pipelines/:pipeline_idcarries a real
started_at. Verified on the same instance: pipelines 5280 and 5281 aresuccesswith a real
started_atanddurationon the single-pipeline endpoint, andstarted_at: nullonthe list endpoint. Any check reading
started_atfrom the list will treat every pipeline asunstarted.
Correction to the original filing
This issue first claimed pipeline 5258 was "created and never picked up", citing
status: pendingwith
started_at: nullobserved roughly two hours later. The pipeline's own record contradictsthat: it started 29 seconds after creation and finished
success. The original observation almostcertainly read
started_atfrom the MR-pipelines list endpoint (always null, see above) and pairedit with a
pendingstatus read during the first seconds. The defect above — the gate concludingbefore the pipeline exists — is what the timestamps actually support.
Distinct from #445 (CI re-triggered on every invocation), #472 (whole status history on a re-run)
and #473 (corrupt
ci_wait_started).Found during MEDECA-447 (medeca-modul-poptavky); corrected while merging that issue's final MR.
phase-pr treats a pipeline that never started as satisfiedto phase-pr treats 'no pipeline yet' as 'no CI' and passes the gate before the pipeline is createdSame window, opposite verdict, on Forgejo (found during IW-583):
iw phase-pr, resumed after a killed run, pushed a card commit and at once reported "CI did not run" (cardci_failed) while Forgejo run 890 for that commit had started. A second call waited and passed. The gate reads the checks before the forge registers the run for the pushed SHA — here it fails closed instead of passing open, but the missing grace window is the same defect.The code on
main(1ab91787) no longer has this path.phase-prno longer infers "no CI" from an empty pipeline list. It asksCiPresence.isCiExpected(core/model/CiPresence.scala), which reads the workflow files out of the commit under test —.gitlab-ci.yml,.forgejo/workflows,.gitea/workflows,.github/workflows— and an unreadable tree is aLeft, never afalse.CiVerdict.NoCihas exactly two producers,core/commands/PhasePr.scala:420andcore/model/CiGate.scala:499, and both are guarded by!ciExpected. No pipeline query can reach that verdict.The grace window this issue asked for also exists:
CiGateConfig.runAppearanceBudgetMs(5 minutes) holds an empty or sha-mismatched run list atRunningbeforeemptyVerdictmay call itNoRunForEvent(core/model/CiGate.scala:492-508).CiGateTestcovers both sides of that boundary.The fix landed in
0881a351(2026-08-11, "verdict model that never reads absence of evidence as green") and reachedphase-prin10fc4b09(2026-08-12). MR !426 was observed on 2026-08-28, so the project was almost certainly running an older installediw; the refile on 2026-09-01 reused that evidence without re-reading the code.The list-endpoint trap this issue documented —
started_at: nullonGET /projects/:id/merge_requests/:iid/pipelines— stays true and stays worth knowing.The IW-583 comment above is a different defect: there the gate failed early rather than passing early, because a stale
ci_wait_startedhad already spent the appearance budget for a head that was seconds old. It is recorded on #491, which is where the fix belongs.Closing as implemented.