Pipeline Model and Rules
1. What happens between a git push and the first job starting?
GitLab builds the pipeline configuration first, then creates jobs, then waits for a runner to pick them up. A strong candidate describes the order, because most "why did my job not run" bugs live in one of these steps:
- Config fetch: GitLab reads
.gitlab-ci.yml at the pushed commit and resolves every include, including components. Inputs are interpolated at this point, so their values are fixed for the whole run.
- Workflow rules:
workflow:rules decide whether a pipeline is created at all. If no rule matches, no pipeline runs.
- Job rules: each job's
rules are evaluated in order, and the first match decides whether the job is added and with which when.
- Scheduling: jobs become pending when their stage or their
needs allow it.
- Pickup: a runner whose tags match polls GitLab, takes the job, and gets a short-lived
CI_JOB_TOKEN for the duration of the job.
A weak answer jumps straight to "the runner runs the script" and cannot explain why a job that exists in the YAML never appears in the pipeline graph.
2. How do stages and needs differ, and what is the DAG?
Stages run in order, and a job in stage N waits for every job in earlier stages to finish. needs replaces that wait with explicit edges, so a job starts as soon as the jobs it lists succeed.
The result is a directed acyclic graph that can run faster than the stage view suggests.
stages: [build, test, deploy]
build_api:
stage: build
script: make api
build_web:
stage: build
script: make web
test_api:
stage: test
needs: [build_api] # starts when build_api finishes, ignores build_web
script: make test-api
lint:
stage: test
needs: [] # starts immediately at pipeline creation
script: make lint
Facts from the CI/CD YAML reference a candidate should know: a job can need up to 50 jobs on GitLab.com (self-managed admins can change the limit), it can need jobs in the same stage, and it downloads artifacts only from the jobs in its needs list.
A job that needs a job missing from the pipeline makes the pipeline fail to start, unless the entry is marked optional: true. A senior candidate adds that needs and dependencies should not be combined in one job.
3. Why use rules instead of only and except?
The deprecated keywords page states that only and except are deprecated, and it maps each form to a rules replacement: only:refs and only:variables become rules:if, and only:changes becomes rules:changes.
GitLab rejects a job that mixes rules with only or except, so migration is per job, not per line.
rules are more expressive because each rule can also set when, allow_failure, variables and needs for the job:
deploy_prod:
script: ./deploy.sh
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: never
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
changes:
paths: [services/api/**/*]
when: manual
variables:
DEPLOY_TARGET: production
A strong candidate mentions the hidden default: a job with none of only, except or rules behaves as only: [branches, tags], which is why an old job without conditions can still run in a merge request pipeline setup it was never meant for.
4. Find the bug: every push to a merge request branch runs two pipelines.
build:
script: make build
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH
Bug: a push to a branch with an open merge request creates a branch pipeline (the second rule matches) and a merge request pipeline (the first rule matches).
Both run the same job, so runner minutes double and the merge request shows two pipelines with possibly different results.
Fix: decide at the workflow level which pipeline type wins. The common pattern is to run merge request pipelines when an MR exists, and branch pipelines otherwise:
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH
A senior candidate knows the edge case: the first push that creates a branch runs a branch pipeline, because no MR exists yet, and opening the MR afterwards starts a merge request pipeline. That is expected and only happens once.
5. What does this produce on main and on a feature branch?
deploy:
script: ./deploy.sh
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
- if: $CI_COMMIT_BRANCH == "main"
when: never
- when: on_success
On main (the default branch), the first rule matches and the job is manual. The second rule is never reached, because evaluation stops at the first match.
On a feature branch, neither if matches, the last rule has no condition, and the job runs automatically with on_success. The author meant "manual on main, nothing elsewhere" and wrote "deploy every feature branch automatically".
Two follow-ups separate levels. First, a when: manual job inside rules defaults to allow_failure: false, so the pipeline waits on it and shows as blocked until someone clicks it.
Second, a catch-all rule at the end is the usual source of jobs running where nobody expected them; a reviewer should ask what the fallback rule is for before approving it.
6. How does rules:changes behave on a new branch, and when do you need compare_to?
For a branch pipeline, rules:changes compares against the previous push to that branch.
On the first push of a new branch, or in a pipeline with no push such as a schedule, there is nothing to compare, so changes evaluates to true and the job runs. The YAML reference says this holds unless compare_to is set.
api_tests:
script: make test-api
rules:
- changes:
paths: [services/api/**/*]
compare_to: refs/heads/main
compare_to accepts a branch, tag or commit SHA and requires paths. In merge request pipelines, changes already compares with the target branch.
The limits matter in large monorepos: 50 patterns per section and 50,000 file checks, after which a patterned rule always matches. rules:exists uses the same limits and, since GitLab 18.2, accepts directories as well as files.
7. Explain merge request pipelines, merged results pipelines and merge trains.
- Merge request pipeline: runs on the source branch commit when the MR is created or updated. It proves the branch works on its own.
- Merged results pipeline: runs on a temporary commit that merges the source into the current target. It catches breakage that only appears after the merge.
- Merge train (Premium and Ultimate): queues MRs and runs each one's merged result on top of the MRs ahead of it, so main never receives a combination that was not tested.
The trade-off is cost against safety. A merged results pipeline goes stale as soon as someone else merges, and a merge train fixes that by testing in order, at the price of reruns when an MR ahead of you fails.
GitLab 19.0 made the merge train's parallel pipeline limit configurable on self-managed and Dedicated; it was fixed at 20 before, and a limit of 1 runs each MR alone against a clean target.
A strong candidate asks how long the test suite takes before recommending a train, because a 40-minute suite with a busy repo makes trains slow.
8. Parent-child or multi-project pipeline, and what does trigger:strategy do?
A parent-child pipeline runs a child pipeline in the same project from a generated or included config, which suits monorepos and dynamic pipelines.
A multi-project pipeline triggers a pipeline in another project, which suits a library that must test its consumers or an app repo that triggers a deploy repo.
generate:
stage: build
script: ./gen-pipeline.sh > child.yml
artifacts:
paths: [child.yml]
run_child:
stage: test
trigger:
include:
- artifact: child.yml
job: generate
strategy: mirror
Without a strategy, the trigger job succeeds as soon as the downstream pipeline is created, which hides failures. strategy: mirror, added in GitLab 18.2, makes the trigger job mirror the downstream status and wait for it, and the YAML reference now recommends it over depend.
With depend, a failed downstream job that has allow_failure: true still reports success.
On variables, forward:yaml_variables defaults to true and forward:pipeline_variables defaults to false, so manual or scheduled pipeline variables do not reach the child unless you ask for them.
9. How do interruptible and auto-cancel interact?
interruptible defaults to false and does nothing unless auto-cancel is on. workflow:auto_cancel:on_new_commit controls what happens to an older pipeline when a new commit arrives on the same ref:
conservative (default): jobs that have not started count as interruptible, and a running pipeline is canceled only if every started job is interruptible.
interruptible: running or pending jobs marked interruptible: true are canceled.
none: nothing is canceled.
on_job_failure: all cancels the rest of the pipeline when one job fails, which saves minutes on long suites.
The trap the docs call out: do not mark deployment jobs as interruptible, because canceling a deploy halfway leaves the environment partly updated. A good answer marks build and test jobs interruptible and leaves deploys alone.
10. Where can CI/CD variables come from, and which one wins?
Variables come from the YAML (default and job level), from project, group and instance settings, from the pipeline itself (manual runs, schedules, API and trigger calls), from dotenv report artifacts, and from GitLab's predefined set.
Pipeline variables override project, group and instance settings, and those override variables written in the YAML. Predefined variables sit at the bottom.
Settings-level variables carry the security flags:
- Protected: only exposed to pipelines on protected branches and tags. This is how a production key stays out of a feature branch.
- Masked: redacted in job logs. Masking is a log filter, not access control: a script can still send the value anywhere.
- Environment scope: a variable can be limited to jobs that deploy to
production or a pattern such as review/*.
A senior candidate adds that the ability to set pipeline variables is itself a privilege.
Anyone who can run a pipeline with variables can override a YAML value such as a deploy target, which is one reason GitLab recommends typed inputs and suggests disabling pipeline variables once inputs are in place (question 15).