GitLab CI/CD Associate study materials
GitLab CI/CD Associate study resources

Two pipelines per push, an invalid config, and a job that ignores its own edit

GitLab CI/CD Associate · Put your knowledge to work in context.

All case studies

Two pipelines per push, an invalid config, and a job that ignores its own edit

Acme's platform repository runs everything from one root .gitlab-ci.yml that pulls in ci/web.yml, then ci/api.yml, then ci/deploy.yml through three include: local entries in that order. Three things are broken at once. (1) Since a workflow template was removed during a cleanup, every push to a branch that has an open merge request creates two pipelines - one with source push, one with source merge_request_event - and MR approvals sometimes point at the older, already-superseded run. (2) The new deploy:staging job in ci/deploy.yml kept only: [merge_requests] from the old template and also gained a rules: entry for the staging environment; now pipeline creation fails for the entire project, unit tests included, and the pipeline list shows a YAML-invalid badge. (3) A developer edited test:unit in ci/web.yml to add coverage flags, but the job that actually runs still uses the plain, un-flagged command from ci/api.yml. Nobody has touched runner configuration.

1. Explain why two pipelines are created for a single push, and give a configuration change that removes the duplicate without losing merge request pipelines.

Read the worked answer

A pipeline is created per context, not per commit. A push on a branch creates a branch pipeline if any job's rules match a branch context, and GitLab additionally creates a merge request pipeline if any job's rules match merge_request_event. Here both sets of jobs match, so both pipelines are created - double runner minutes, and the MR widget can show either one.

The fix belongs at workflow level, because workflow:rules decides whether a pipeline exists at all, before any job is examined:

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never
    - if: $CI_COMMIT_BRANCH

Rules are evaluated top-down, first match wins. The second rule says: on a branch push, if a merge request is already open for that branch, create nothing - the MR pipeline covers that commit. $CI_OPEN_MERGE_REQUESTS is defined in branch pipelines, which is exactly why this idiom works there.

Two caveats worth stating in review: if no workflow rule matches, GitLab creates no pipeline at all, so the fallback branch rule matters; and workflow:rules should not be mixed with only/except in jobs, because mixing those keywords in a single job is a configuration error.

2. The pipeline refuses to be created after `deploy:staging` gained both `only:` and `rules:`. Explain the failure mode, why it affects unrelated jobs, and rewrite the job.

Read the worked answer

only/except and rules are mutually exclusive within one job. GitLab treats a job that uses both as a configuration error: the .gitlab-ci.yml is syntactically valid YAML but semantically invalid, so the whole pipeline fails to be created, not just deploy:staging. That is why unit tests and linters disappear too and the UI shows a YAML-invalid badge rather than a failed job.

The correct move is to delete only: and express the intent in rules, keeping first-match-wins ordering in mind:

deploy:staging:
  stage: deploy
  environment:
    name: staging
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"
      when: manual
      allow_failure: true
    - when: never

The terminal - when: never makes the default explicit; without a matching rule a job is simply not added to the pipeline, which is frequently the real cause of "my job never runs". Note that when: manual inside rules also needs allow_failure decided consciously, since the default for rule-based manual jobs is blocking.

Catch this class of error before merging: run CI Lint (/-/ci/lint in the UI, or POST /projects/:id/ci/lint in the API) against the fully merged configuration, not just the file you edited.

3. Why does the edited `test:unit` from `ci/web.yml` lose to the version in `ci/api.yml`, and what are the safe ways to share job definitions across included files?

Read the worked answer

include does not namespace anything. All included files are merged into one configuration document, and when two files define the same top-level job key, the later definition wins for the keys it sets - here ci/api.yml is included after ci/web.yml, so its script overwrites the coverage-flagged one. Nothing warns you; the pipeline is green, just not doing what the author believes.

The diagnosis is the merged view: the CI Lint page and the job page's "View merged YAML" show only one test:unit, containing the api version.

Safe patterns:

# ci/web.yml
.test-base:
  image: node:20
  script:
    - npm ci
    - npm run test:unit

unit:web:
  extends: .test-base
  script:
    - npm ci
    - npm run test:unit -- --coverage

Hidden keys beginning with . are not jobs, so they can hold shared defaults without colliding. Unique job names per service (unit:web, unit:api) plus extends (or !reference when you need to splice a specific list such as rules or script into a larger block) removes the ambiguity entirely. Also check whether the job should exist in both files at all - include:rules can include a file only when ci/web.yml's paths changed.