A dangerous GitLab CI change rarely looks dangerous in a merge request. The YAML diff is small: one rules: block becomes if: $CI_COMMIT_BRANCH, a job gains needs: [], somebody edits a cache key because a blog post said it would be faster. Reviewers read it like prose, approve it, and then discover the real effect later: a job silently stopped running on tags, artifacts now expire before downstream jobs can fetch them, or the pipeline became fully parallel in a way that makes main nondeterministic.

The problem is that .gitlab-ci.yml is not documentation. It is a little compiler input. The meaningful unit is not the edited line; it is the effective job graph after include, extends, rules, needs, dependencies, cache, and artifacts have interacted. I stopped reviewing CI YAML as text and started forcing every proposal through a tiny “effective graph” preview. The preview does not need to perfectly reimplement GitLab. It needs to make semantic movement visible before merge.

Below is a reproducible local version. It is intentionally conservative: where GitLab semantics are complicated, it emits unknown instead of pretending to be right.

The artifact: compile CI YAML into an effective graph

This Python example expands local include entries, merges extends, then emits one JSON record per job with the fields reviewers actually argue about: when it may run, what it needs, what it consumes, what it publishes, and which cache namespace it touches.