fix(IW-517): the GitLab CI gate reads every pipeline at a commit #559

Merged
mph merged 17 commits from IW-517 into main 2026-09-14 19:42:36 +00:00
Owner

Summary

The GitLab CI gate judged a commit on one pipeline and could report Green
while another pipeline on the same commit was still running. iw phase-pr,
iw phase-merge and iw ci-gate all share that gate, so on GitLab the
"CI is green, safe to merge" contract could be false.

GitLabClient.fetchCiSnapshot built its CiSnapshot from the merge request's
head_pipeline, which names at most one pipeline. GitLab routinely starts
several pipelines for one push (a push pipeline and a merge_request_event
pipeline). Every pipeline but one was invisible to the gate, whatever its state.

GitHub and Forgejo enumerate runs and were never affected.

Defects fixed

Phase 1 — the GitLab snapshot holds at most one pipeline (PR #554, merged)

  • GitLabClient now reads GET projects/:id/merge_requests/:iid/pipelines?per_page=100
    and maps every entry to a CiRun. head_pipeline is no longer read.
  • A parse failure on any entry returns Left rather than defaulting a field.
  • A full 100-entry page sets incomplete, which the model already refuses to judge.
  • A GitLab CiRun carries its pipeline id as workflowLabel, so two pipelines at
    one commit survive dedupe as two separate answers.
  • core/model/CiGate.scala is unchanged by design — the model was already correct;
    it could not judge a pipeline it was never handed.
  • docs/plans/2026-08-11-ci-green-gate.md §G2 records the decision this reverses.

Behaviour changes a GitLab user meets

  • A stale canceled sibling pipeline at the same sha now reads as Red and stops
    the phase. This is the safe direction.
  • A merge request with exactly 100 pipelines is refused rather than paginated.

Testing

  • 5 regression tests added in core/test/GitLabCiTest.scala; the 4 pre-existing
    fetchCiSnapshot tests were repaired for the new endpoint, not weakened. A
    reviewer confirmed this by mutation.
  • ./mill core.test.testOnly 'iw.tests.GitLabCiTest' — exit 0, 187 tests.
  • ./iw ./test unit — exit 0. ./iw ./test lint — exit 0.
  • ./iw ./test — exit 1 on test/start.bats "the tmux session does not carry
    iw's internal plumbing". That failure reproduces at the phase baseline in a
    clean worktree, asserts on tmux pane environment, and has no causal path to
    this change. Filed as #556.

Follow-ups filed

  • #556 — test/start.bats tmux-plumbing test fails at baseline on this machine
  • #557 — GitLabClient.parseMrPipeline has no production caller; removal needs approval
  • #558 — GitLabClient.encodedRepo under-escapes repository slugs

Release notes

project-management/issues/IW-517/release-notes.md (Czech) describes the change
for users of phase-pr, phase-merge and ci-gate.

🤖 Generated with Claude Code

## Summary The GitLab CI gate judged a commit on one pipeline and could report `Green` while another pipeline on the same commit was still running. `iw phase-pr`, `iw phase-merge` and `iw ci-gate` all share that gate, so on GitLab the "CI is green, safe to merge" contract could be false. `GitLabClient.fetchCiSnapshot` built its `CiSnapshot` from the merge request's `head_pipeline`, which names at most one pipeline. GitLab routinely starts several pipelines for one push (a `push` pipeline and a `merge_request_event` pipeline). Every pipeline but one was invisible to the gate, whatever its state. GitHub and Forgejo enumerate runs and were never affected. ## Defects fixed **Phase 1 — the GitLab snapshot holds at most one pipeline** (PR #554, merged) - `GitLabClient` now reads `GET projects/:id/merge_requests/:iid/pipelines?per_page=100` and maps every entry to a `CiRun`. `head_pipeline` is no longer read. - A parse failure on any entry returns `Left` rather than defaulting a field. - A full 100-entry page sets `incomplete`, which the model already refuses to judge. - A GitLab `CiRun` carries its pipeline id as `workflowLabel`, so two pipelines at one commit survive `dedupe` as two separate answers. - `core/model/CiGate.scala` is unchanged by design — the model was already correct; it could not judge a pipeline it was never handed. - `docs/plans/2026-08-11-ci-green-gate.md` §G2 records the decision this reverses. ## Behaviour changes a GitLab user meets - A stale `canceled` sibling pipeline at the same sha now reads as Red and stops the phase. This is the safe direction. - A merge request with exactly 100 pipelines is refused rather than paginated. ## Testing - 5 regression tests added in `core/test/GitLabCiTest.scala`; the 4 pre-existing `fetchCiSnapshot` tests were repaired for the new endpoint, not weakened. A reviewer confirmed this by mutation. - `./mill core.test.testOnly 'iw.tests.GitLabCiTest'` — exit 0, 187 tests. - `./iw ./test unit` — exit 0. `./iw ./test lint` — exit 0. - `./iw ./test` — exit 1 on `test/start.bats` "the tmux session does not carry iw's internal plumbing". That failure reproduces at the phase baseline in a clean worktree, asserts on tmux pane environment, and has no causal path to this change. Filed as #556. ## Follow-ups filed - #556 — `test/start.bats` tmux-plumbing test fails at baseline on this machine - #557 — `GitLabClient.parseMrPipeline` has no production caller; removal needs approval - #558 — `GitLabClient.encodedRepo` under-escapes repository slugs ## Release notes `project-management/issues/IW-517/release-notes.md` (Czech) describes the change for users of `phase-pr`, `phase-merge` and `ci-gate`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
mph added 15 commits 2026-09-14 12:45:41 +00:00
phase-pr reports CI green on GitLab because it queries pipelines by branch ref, never seeing merge_request_event pipelines

Artifacts:
- analysis.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179JhvXg4eopsQNmDLUSuim
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179JhvXg4eopsQNmDLUSuim
phase-pr reports CI green on GitLab because it queries pipelines by branch ref, never seeing merge_request_event pipelines

Artifacts:
- tasks.md
- review-state.json (tasks_ready)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179JhvXg4eopsQNmDLUSuim
The delegated analysis pass returned after the first draft was committed. It
reproduces the defect against the real GitLabClient.fetchCiSnapshot and the
real CiGate.verdict, and it measures a second cause the draft only guessed at:
dedupe groups GitLab runs by (workflowLabel, event), and with workflowLabel a
constant and event the pipeline source, two same-source pipelines at one commit
collapse to the newest. Reading the full pipeline list is therefore necessary
but not sufficient.

Estimate narrows to 4-7h; tasks.md follows.

Artifacts:
- analysis.md
- tasks.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179JhvXg4eopsQNmDLUSuim
Independent review re-ran the pasted evidence and returned sound with no
blocking finding. Seven notes recorded, two marked: the Testing Strategy
understates the work D7 creates, and a stale canceled sibling pipeline
becomes Red for GitLab users.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179JhvXg4eopsQNmDLUSuim
Issue: IW-517
Phase: 1 - The GitLab snapshot holds at most one pipeline, so a second pipeline on the same commit is invisible

This commit marks the checkpoint for phase 1 context generation.
Use dx-fix IW-517 to return to this point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The GitLab snapshot holds at most one pipeline, so a second pipeline on the same commit is invisible

Artifacts:
- phase-01-tasks.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
workflow: Triage lessons for IW-517
Some checks failed
CI / changes (pull_request) Successful in 2s
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Successful in 8s
CI / lint (pull_request) Successful in 8s
CI / test (pull_request) Has been cancelled
CI / dashboard-build (pull_request) Has been cancelled
CI / compile (pull_request) Has been cancelled
5cfd45fed0
Follow-ups filed: 3 (#556, #557, #558), commented: 0, struck: 0
Lessons: 0 approved, 0 rejected, 0 still pending (no LESSONS.md captured)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011EmLSy5eWo2imTeFHvySMc
chore(IW-517): update review-state
All checks were successful
CI / changes (pull_request) Successful in 2s
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Successful in 7s
CI / lint (pull_request) Successful in 8s
CI / compile (pull_request) Successful in 1m17s
CI / dashboard-build (pull_request) Successful in 1m6s
CI / test (pull_request) Successful in 14m43s
72345533ef
mph merged commit 6c2baaadcf into main 2026-09-14 19:42:36 +00:00
Sign in to join this conversation.
No description provided.