1. Foundations of CI/CD and GitLab
This chapter covers the basics that the rest of the guide builds on. It explains what CI/CD is and why teams adopt it. It also introduces how GitLab organizes code, people and permissions, and walks through writing, checking and reading your first pipeline. Every later topic depends on these ideas, from runners to deployment strategies.
<a id="11-cicd-core-concepts"></a>
1.1 CI/CD Core Concepts
This subchapter covers the engineering practices that GitLab CI/CD automates. These ideas apply to any CI/CD tool, not only GitLab.
Continuous Integration
Why it exists. Before CI was common, developers often worked on separate branches for weeks and only merged their work near a release date. Each branch drifted further from the others, so the final merge brought conflicts, broken assumptions and bugs nobody could trace. This was known as "integration hell". Think of several authors who each edit a private copy of a book for a month and then try to combine the copies in one afternoon. Compare that with authors who save small edits into a shared copy many times a day.
What it is. Continuous Integration (CI) is a practice in which every developer merges small changes into a shared mainline branch often, at least daily. An automated build and test suite checks each of those merges. The goal is to find integration problems minutes after they are introduced, while the change is still small and fresh in the author's mind.
How it works. A CI system watches the repository. On every push, it checks out the exact commit, builds it, and runs automated checks such as compilation, unit tests and linters. It then reports the result back to the developer. Successful CI depends on a few core practices:
- A single shared mainline. Everyone integrates into one branch (usually
main) rather than long-lived personal branches. - A self-testing build. The build fails if any automated test fails. A green build means something.
- Frequent commits. Small changes are easier to review, test and revert.
- Fix broken builds immediately. A red mainline blocks everyone, so fixing it is the team's top priority.
- Keep the build fast. Feedback should arrive within minutes. A common target is about 10 minutes for the main feedback loop.
Developer GitLab Runner (CI executor)
| | |
|--- git push ------>| |
| |--- create pipeline, queue jobs ->|
| | |-- check out commit
| | |-- install dependencies
| | |-- build
| | |-- run unit tests and linters
| |<-- job status: passed / failed --|
|<-- feedback -------| |
| (MR widget, email, commit status icon) |
Here is a minimal CI-only configuration for a Node.js project. There is no deployment yet; it only checks that each commit builds and passes its checks:
# .gitlab-ci.yml
stages:
- build
- test
build:
stage: build
image: node:22 # every job runs inside this container image
script:
- npm ci # clean, reproducible install from package-lock.json
- npm run build
artifacts:
paths:
- dist/ # keep the build output for later jobs or download
unit-tests:
stage: test
image: node:22
script:
- npm ci
- npm test # a non-zero exit code fails the job, and so the pipeline
lint:
stage: test # same stage as unit-tests, so the two run in parallel
image: node:22
script:
- npm ci
- npm run lint
Key takeaways
- CI is a practice (integrating often and checking automatically), not just a tool. A CI server attached to month-old branches is not CI.
- Every push triggers an automated build and test run on the exact commit.
- Small, frequent changes keep integration problems small and easy to diagnose.
- A broken mainline is the team's highest priority.
- Fast feedback is essential. A slow pipeline quietly pushes people back toward large, infrequent merges.
Practice
- (Beginner) List three symptoms you would expect to see in a team that runs a CI server but does not actually practice continuous integration.
- (Beginner) Extend the example above with a
type-checkjob (npm run typecheck) that runs in parallel withunit-testsandlint.- (Intermediate) Your team's CI feedback loop takes 45 minutes. Explain how this affects developer behavior, and propose two structural changes to the pipeline that would shorten it.
- Interview question: "Your team uses feature branches that live for three weeks, but every push runs a pipeline. Is the team doing continuous integration?" Hint: separate "running automated builds" from "integrating into a shared mainline", and think about what goes untested until the merge.
Continuous Delivery vs Continuous Deployment
Why they exist. CI proves that code integrates and passes tests. It says nothing about getting that code to users. Historically, releases were rare, manual and stressful events: long checklists, weekend deployment windows and frequent rollbacks. The longer the gap between releases, the more changes each release contained, and the riskier it became.
What they are. Both practices extend CI toward production, but they differ in one decision:
- Continuous Delivery keeps every change that passes the pipeline in a releasable state. The change is automatically built, tested and deployed to pre-production environments. Releasing to production is a business decision that a human triggers, often with one click.
- Continuous Deployment removes that human gate. Every change that passes all automated checks goes to production automatically.
A restaurant analogy: in continuous delivery, every dish is cooked, plated and waiting at the pass, and a waiter decides when to carry it out. In continuous deployment, dishes go straight onto a conveyor belt to the customer as soon as they pass the chef's quality check.
| Aspect | Continuous Delivery | Continuous Deployment |
|---|---|---|
| Production release trigger | Manual approval or button | Automatic, on pipeline success |
| Always releasable? | Yes | Yes |
| Human gate before production | Yes | No |
| Required test confidence | High | Very high (the tests are the gate) |
| Typical fit | Regulated industries, scheduled launches, B2B software | High-traffic SaaS, mature teams with strong observability |
| GitLab building block | when: manual, protected environments, deployment approvals | Automatic deploy job on the default branch |
Continuous Integration
commit --> build --> unit tests --> integration tests
|
Continuous Delivery v
... --> deploy to staging --> acceptance tests --> [ human approves ] --> production
Continuous Deployment
... --> deploy to staging --> acceptance tests --> (automatic) ---------> production
In GitLab, the difference between the two can be a single line:
stages:
- build
- test
- staging
- production
# ... build and test jobs omitted ...
deploy-staging:
stage: staging
script:
- ./deploy.sh staging
environment:
name: staging # tracked under Operate > Environments
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # only deploy from the default branch
deploy-production:
stage: production
script:
- ./deploy.sh production
environment:
name: production
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual # continuous delivery: a person clicks "Run". Delete this line for continuous deployment.
A manual job defined inside rules defaults to allow_failure: false. The pipeline therefore shows the status blocked until someone runs the job, which makes the waiting release clearly visible.
Teams often pair continuous deployment with feature flags, which separate deploying code from releasing a feature. Unfinished features can ship to production switched off.
Key takeaways
- Continuous delivery means every passing change could go to production. Continuous deployment means it does.
- The only structural difference is whether a human approval sits before production.
- Continuous deployment needs a lot of trust in automated tests, monitoring and fast rollback.
- In GitLab,
when: manual(plus protected environments and approvals in higher tiers) implements the delivery gate. - Feature flags let you deploy continuously while still controlling when users see a feature.
Practice
- (Beginner) Classify each scenario as continuous delivery or continuous deployment: (a) a mobile app whose builds are uploaded to the store when the product owner approves; (b) a web API where every merge to
mainreaches production within 20 minutes; (c) a bank's internal system released after a change advisory board meeting, using a pipeline-produced artifact.- (Intermediate) Modify the example so that production deploys happen automatically, but only for pipelines started from a Git tag (hint:
$CI_COMMIT_TAG), while staging still deploys from the default branch.- Interview question: "What must be true about a team and its systems before it moves from continuous delivery to continuous deployment?" Hint: consider test coverage and reliability, production monitoring and alerting, rollback speed, feature flags, and regulatory constraints.
Pipeline-Driven Development
Why it exists. Without automation, quality checks live in people's heads, in wiki pages, or in "remember to run this before merging" messages. Someone forgets a step, a laptop has a different tool version, and "it works on my machine" becomes a recurring argument. If the build process is configured by clicking through a CI server's UI, nobody can review, version or reproduce it.
What it is. Pipeline-driven development is a workflow in which the automated pipeline is the single authoritative gate for every change. Nothing merges or ships unless the pipeline passes. It rests on four ideas:
- Pipeline as code. The pipeline definition (
.gitlab-ci.yml) lives in the repository next to the application. It is versioned, branched and reviewed in merge requests like any other code. - The pipeline as a contract. If the pipeline is green, the change meets the team's agreed quality bar. If the bar changes, the pipeline file changes, and that change is reviewed too.
- Shift left. Move checks such as security scans, linting and contract tests as early as possible, where problems are cheapest to fix.
- Fail fast. Order jobs so that cheap, quick checks run first and expensive ones run only when the cheap ones pass.
Think of an airport security checkpoint. Every passenger goes through the same steps in the same order, and there is no side door. The rules are written down and applied the same way to everyone.
feature branch merge request main
-------------- ------------- ----
commit --> push --> MR pipeline runs --> review + green --> merge --> main pipeline
^ | |
+---- fix and push <----+ failed v
deploy / release
The configuration below follows the fail-fast principle. Checks that take seconds run first, so a formatting mistake never uses up time on a 15-minute integration suite:
stages:
- validate # seconds: formatting, linting, static checks
- test # minutes: unit tests
- integration # longer: tests against real services
- deploy
format-check:
stage: validate
image: golang:1.23
script:
- test -z "$(gofmt -l .)" # gofmt -l prints unformatted files; any output fails the job
vet:
stage: validate
image: golang:1.23
script:
- go vet ./...
unit-tests:
stage: test
image: golang:1.23
script:
- go test ./... -short
integration-tests:
stage: integration
image: golang:1.23
services:
- postgres:16 # a throwaway database container for this job only
variables:
POSTGRES_PASSWORD: "test"
script:
- go test ./... -run Integration
To make the pipeline a real gate, enable Settings > Merge requests > Merge checks > Pipelines must succeed. GitLab then refuses to merge an MR whose latest pipeline did not pass.
Key takeaways
- The pipeline definition belongs in the repository, so it is reviewed, versioned and reproducible for any past commit.
- The pipeline is the single quality gate. "Pipelines must succeed" turns it from advice into enforcement.
- Shift checks left and order them cheapest-first, so failures surface as quickly as possible.
- Containerized jobs give everyone the same environment and remove "works on my machine" differences.
Practice
- (Beginner) List every manual check your team performs before merging (for example "run the linter", "check the migration"). Mark which ones could move into the pipeline.
- (Intermediate) A pipeline has these stages in order:
integration-tests(12 min),build(3 min),lint(20 s),unit-tests(2 min),deploy. Reorder them to fail fast, and justify any dependencies that limit the reordering.- Interview question: "Why should the pipeline configuration live in the same repository as the application, rather than be configured in a CI server's web UI?" Hint: think about code review, branching, auditing, and rebuilding a commit from six months ago.
The DevOps Lifecycle
Why it exists. In many organizations, developers wrote code and then handed it to a separate operations team to deploy and run. The groups had different goals (change versus stability), different tools and little shared feedback. Problems in production reached developers slowly, if at all, and handoffs bred blame. DevOps is the cultural and technical response: one team, or closely collaborating teams, owns software from idea to production and learns continuously from how it behaves there.
What it is. The DevOps lifecycle is usually drawn as an infinity loop. The development half feeds the operations half, and what operations learns flows back into planning:
+-------> Plan -------> Code -------> Build -------+
| |
| DEV v
Monitor ------- Test
^ OPS |
| v
+------ Operate <------ Deploy <------ Release <----+
CI/CD automates the middle of the loop (build, test, release, deploy). The loop only works if planning and monitoring are connected to it. GitLab's pitch is that a single application covers the whole loop. Its product areas map roughly as follows (GitLab occasionally renames or regroups these stages):
| Lifecycle area | What happens | Example GitLab features |
|---|---|---|
| Plan | Define and prioritize work | Issues, epics, boards, milestones |
| Create | Write and review code | Repositories, merge requests, code review, Web IDE |
| Verify | Build and test automatically | CI/CD pipelines, test reports, code quality |
| Package | Store build outputs | Container registry, package registry |
| Secure | Find vulnerabilities | SAST, secret detection, dependency and container scanning |
| Deploy | Ship to environments | Environments, releases, feature flags, Kubernetes agent |
| Monitor | Observe production | Error tracking, incident management |
| Govern | Enforce policy and compliance | Security policies, compliance frameworks, audit events |
The DORA metrics (from the DevOps Research and Assessment program) are the standard way to measure how well this lifecycle performs:
| Metric | Measures | Good CI/CD improves it by |
|---|---|---|
| Deployment frequency | How often you deploy to production | Making deployments cheap and routine |
| Lead time for changes | Time from commit to running in production | Automating every step between merge and deploy |
| Change failure rate | Share of deployments that cause a failure | Catching defects before production |
| Time to restore service | How long recovery takes after a failure | Fast, automated rollbacks and redeploys |
A single change can travel the whole loop inside GitLab:
# Plan: issue #42 "Rate-limit the login endpoint" exists on the board.
# Create: branch named after the issue
git switch -c 42-rate-limit-login
# "Closes #42" is a closing pattern: merging into the default branch closes the issue
git commit -am "Add rate limiting to login endpoint" -m "Closes #42"
# Push options open a merge request straight from the command line
git push -u origin 42-rate-limit-login -o merge_request.create -o merge_request.target=main
# Verify/Secure: the MR pipeline runs tests and scanners.
# Deploy: after merge, the main pipeline deploys and the environment records the deployment.
# Monitor: errors or incidents link back to new issues, which feed the next Plan.
Key takeaways
- DevOps is a culture of shared ownership supported by automation. CI/CD is its delivery engine, not the whole of it.
- The lifecycle is a loop. Feedback from production must flow back into planning.
- GitLab covers the whole loop in one application, so issues, MRs, pipelines and deployments link to one another.
- DORA metrics measure delivery speed (frequency, lead time) and stability (failure rate, restore time) together.
Practice
- (Beginner) Map the tools your team currently uses (or a team you know) onto the lifecycle areas in the table. Which areas have no tool, or rely on manual work?
- (Intermediate) Choose one DORA metric and describe two concrete pipeline changes that would improve it, along with one way those changes could accidentally hurt a different DORA metric.
- Interview question: "People say DevOps is a culture, not a toolchain. What do they mean?" Hint: consider what happens if a company buys every DevOps tool but keeps separate dev and ops teams with opposing incentives.
Benefits and Trade-offs of CI/CD
Why it matters. CI/CD is not free. It costs compute, maintenance effort and discipline. To argue for adopting it, or to fix a struggling setup, you need to know what you gain and what you pay.
Benefits.
| Benefit | Why it happens |
|---|---|
| Faster feedback | Defects surface minutes after the commit that caused them |
| Lower release risk | Small, frequent releases are easier to test, understand and roll back |
| Consistency | Every change goes through the same automated steps in the same environment |
| Higher productivity | Machines do repetitive build, test and deploy work |
| Auditability | Every pipeline, job log and deployment is recorded and linked to a commit |
| Faster time to market | Features reach users as soon as they are ready, not at the next release window |
Costs and trade-offs.
| Trade-off | Description | Mitigation |
|---|---|---|
| Compute cost | Every push uses runner time | Caching, rules to skip irrelevant jobs, auto-canceling obsolete pipelines |
| Maintenance burden | Pipelines are code and need upkeep | Shared templates and CI/CD components, an owner for the CI config |
| Flaky tests | Non-deterministic tests erode trust in the pipeline | Quarantine flaky tests, retry only infrastructure failures, track flakiness |
| Slow pipelines | Long feedback loops push teams back to big merges | Parallelism, needs, fail-fast ordering, smaller test scopes |
| Up-front investment | Test suites and automation take time to build | Start with CI only, add delivery stages gradually |
| Security exposure | Pipelines hold credentials and can reach production | Protected variables and branches, least-privilege runners, OIDC |
Several of these trade-offs can be reduced with a few lines of configuration:
default:
interruptible: true # a newer push to the same branch can cancel this pipeline
retry:
max: 2
when:
- runner_system_failure # retry infrastructure problems...
- stuck_or_timeout_failure # ...but not genuine test failures
unit-tests:
image: python:3.12
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" # pip's cache must be inside the project dir to be cached
cache:
key:
files:
- requirements.txt # reuse the cache until dependencies change
paths:
- .cache/pip
script:
- pip install -r requirements.txt
- pytest
rules:
- changes: # skip this job when only documentation changed
- "**/*.py"
- requirements.txt
Key takeaways
- CI/CD trades compute and maintenance cost for faster feedback, lower risk and consistent delivery.
- Flaky and slow pipelines are the most common reasons teams lose trust in CI/CD.
- Retry infrastructure failures, never real test failures, or you will hide genuine bugs.
- Most costs can be reduced with caching, conditional jobs and canceling obsolete pipelines.
Practice
- (Beginner) Name two benefits of CI/CD that matter most to a developer and two that matter most to a manager or auditor.
- (Intermediate) A pipeline has 4 jobs of 5 minutes each. The team pushes 30 times per working day, with 22 working days per month. Calculate the monthly compute minutes (assume a cost factor of 1). Then propose two changes that would cut the total by at least 30%.
- Interview question: "A manager says that since adopting CI/CD, the team ships more slowly. How would you investigate?" Hint: collect data first: pipeline duration, queue time, failure and flakiness rates, and how often developers wait on the pipeline. Then decide whether the problem is the practice or its implementation.
<a id="12-gitlab-platform-overview"></a>
1.2 GitLab Platform Overview
This subchapter covers how GitLab is delivered, how it organizes work, and who may do what. Each of these affects how your pipelines behave.
GitLab.com vs Self-Managed vs Dedicated
Why it matters. GitLab is the same software in every deployment option, but who operates it changes a lot: who provides runners, when upgrades happen, which networks pipelines can reach, and where data lives. A pipeline that works in one place can fail in another for purely operational reasons.
What the options are.
- GitLab.com is the multi-tenant SaaS run by GitLab Inc. You sign up and start working. GitLab-hosted runners are available out of the box, metered in compute minutes.
- GitLab Self-Managed is GitLab installed on your own infrastructure (Linux package, Helm chart, GitLab Operator or Docker). You control everything, including network isolation, and you are responsible for everything, including upgrades, backups and runners.
- GitLab Dedicated is a single-tenant SaaS instance that GitLab operates for one customer, in a cloud region of the customer's choice. It suits organizations that need isolation and data residency but do not want to run GitLab themselves.
An analogy: GitLab.com is renting an apartment in a managed building. Self-managed is owning a house, so you fix the roof yourself. Dedicated is a private house that the building company maintains for you.
| Aspect | GitLab.com | Self-Managed | Dedicated |
|---|---|---|---|
| Who operates it | GitLab | You | GitLab |
| Tenancy | Multi-tenant | Single-tenant (your own) | Single-tenant (managed) |
| Available tiers | Free, Premium, Ultimate | Free, Premium, Ultimate | Ultimate |
| Upgrades | Continuous, always latest | You choose when (monthly releases) | GitLab, in scheduled maintenance windows |
| Runners | GitLab-hosted runners plus your own | Only your own | Hosted runners for Dedicated plus your own |
| Network isolation / air-gap | No | Yes, fully possible | Private connectivity options, no air-gap |
| Typical user | Individuals, startups, most companies | Regulated or air-gapped environments, full control needs | Large enterprises with compliance and residency needs |
Self-managed installations can also choose between Community Edition (CE, open source) and Enterprise Edition (EE). EE without a license behaves like the Free tier and can be upgraded to Premium or Ultimate by adding a license, so EE is usually the recommended choice.
Most .gitlab-ci.yml content is portable across all three. The most visible difference is how jobs choose a runner:
# GitLab.com: select a GitLab-hosted runner size with a predefined tag
build-on-gitlab-com:
tags:
- saas-linux-medium-amd64
script:
- make build
# Self-managed: target your own runner through the tags it was registered with
build-on-self-managed:
tags:
- docker
- linux
script:
- make build
Key takeaways
- All three offerings run the same GitLab product. They differ in who operates it, tenancy, upgrade timing and runner provisioning.
- GitLab.com is the fastest way to start and includes hosted runners metered by compute minutes.
- Self-managed gives full control, including air-gapped networks, at the cost of operating everything yourself.
- Dedicated provides single-tenant isolation and data residency without the operational burden. It is Ultimate-only.
- Runner tags and network reachability are the most common sources of non-portable pipelines.
Practice
- (Beginner) Recommend an offering for each case and justify it: (a) a five-person startup; (b) a defense contractor whose network has no internet access; (c) a large bank that needs data stored in a specific region but has no team to operate GitLab.
- (Intermediate) List at least five CI/CD-related responsibilities a self-managed administrator owns that GitLab.com handles for you (for example runner fleets or object storage for artifacts).
- Interview question: "A pipeline that worked on GitLab.com fails after migrating to a self-managed instance. What are the likely causes?" Hint: think about runner tags, missing hosted runners, outbound network access to registries and package mirrors, instance version and tier, and instance-level limits.
Groups, Subgroups and Projects
Why it matters. A real organization has many repositories, teams and shared resources. Configuring permissions, runners and variables one repository at a time does not scale. GitLab's hierarchy lets you configure something once and have everything beneath it inherit it.
What they are.
- A project is the basic unit of work. It holds a Git repository plus its issues, merge requests, CI/CD pipelines, registries, environments and settings.
- A group is a container for projects and other groups. It has members, settings, CI/CD variables and runners that cascade down to everything inside it.
- A subgroup is a group nested inside another group. Nesting can go up to 20 levels deep.
- A namespace is the path prefix that identifies where a project lives: a personal namespace (
alice/) or a group path (acme-corp/products/).
On GitLab.com, the top-level group is where the subscription (tier), compute minutes, SSO and many policies attach. That makes it the most important design decision in the hierarchy.
acme-corp/ top-level group: subscription, SSO, shared runners and variables
├── platform/ subgroup
│ ├── ci-templates project: shared CI/CD components
│ └── infrastructure project: Terraform code
└── products/ subgroup
├── web-shop/ subgroup
│ ├── frontend project
│ └── backend project
└── mobile-app project
Things defined higher in the tree flow downward:
| Defined at a group | Effect on descendants |
|---|---|
| Membership and roles | Inherited. A subgroup can grant a higher role, never a lower one |
| CI/CD variables | Available to jobs in every descendant project |
| Group runners | Can pick up jobs from every descendant project |
| Many settings (for example MR and protection defaults) | Applied as defaults or enforced, depending on the setting |
Pipelines can read the hierarchy through predefined variables. For the backend project above:
show-context:
script:
- echo "Project name = $CI_PROJECT_NAME" # backend
- echo "Full path = $CI_PROJECT_PATH" # acme-corp/products/web-shop/backend
- echo "Namespace = $CI_PROJECT_NAMESPACE" # acme-corp/products/web-shop
- echo "Root namespace = $CI_PROJECT_ROOT_NAMESPACE" # acme-corp
The = signs are deliberate. A : (colon followed by a space) inside an unquoted YAML script line would be parsed as a mapping and break the configuration. Chapter 2 covers this pitfall.
Key takeaways
- Projects hold code and pipelines. Groups and subgroups organize projects and share configuration.
- Members, variables and runners defined on a group are inherited by everything beneath it.
- On GitLab.com, the top-level group carries the subscription and quotas, so design it carefully.
- Inherited roles can be raised in a subgroup or project but not lowered. Grant broad access high in the tree with care.
- Predefined variables such as
CI_PROJECT_PATHandCI_PROJECT_NAMESPACEexpose a project's position to its jobs.
Practice
- (Beginner) Design a group hierarchy for a company with three product teams, one platform team and a shared CI templates repository. Mark where you would register shared runners.
- (Beginner) For the project
globex/research/ml/model-training, write the expected values ofCI_PROJECT_NAME,CI_PROJECT_NAMESPACEandCI_PROJECT_ROOT_NAMESPACE.- Interview question: "Where would you define a deployment credential needed by 40 projects, and what are the risks of that choice?" Hint: compare group-level and project-level variables in terms of exposure. Think about who can run pipelines in those 40 projects and how protected variables limit access.
Repositories, Branches and Merge Requests
Why it matters. Pipelines don't run on their own. Repository events trigger them: pushing a branch, creating a tag, opening or updating a merge request. Which event happened decides which pipeline runs, which variables exist and which secrets the jobs can access.
What they are.
- The repository is the project's Git repository. The default branch (usually
main) is the branch that MRs target by default and that is often deployed. - A branch is a movable pointer to a line of commits. Feature branches isolate work in progress.
- A protected branch restricts who can push to it and merge into it, and blocks force pushes and deletion. Protected branches (and protected tags) are also the only refs that can read protected CI/CD variables, which is how secrets stay out of untrusted branches.
- A tag marks a specific commit, typically a release (
v1.4.0). - A merge request (MR) asks to merge a source branch into a target branch. It brings together the diff, discussion, approvals and pipeline results in one place.
| Event | Pipeline type | Useful variables |
|---|---|---|
| Push to a branch | Branch pipeline | CI_COMMIT_BRANCH, CI_PIPELINE_SOURCE == "push" |
| Push a tag | Tag pipeline | CI_COMMIT_TAG |
| MR opened or new commits pushed to it | Merge request pipeline (when configured) | CI_PIPELINE_SOURCE == "merge_request_event", CI_MERGE_REQUEST_IID |
MR merged into main | Branch pipeline on main | CI_COMMIT_BRANCH == CI_DEFAULT_BRANCH |
main o-----------o-----------------------o (merge commit) --> main pipeline: deploy
\ /
feature o-----o-----o-------------------o
| |
| +-- push --> MR pipeline (tests, scans)
+-------- push --> MR pipeline (tests, scans)
MR !17 "Add search": diff + review comments + approvals + latest pipeline status
A common starting configuration runs pipelines for merge requests, branches and tags, and avoids running two pipelines for one push when a branch already has an open MR. Section 4.4 explains it in depth:
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event" # run MR pipelines
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS # branch push that has an open MR...
when: never # ...skip it: the MR pipeline covers it
- if: $CI_COMMIT_BRANCH # other branch pushes
- if: $CI_COMMIT_TAG # tag pushes (releases)
Key takeaways
- Repository events (push, tag, MR) trigger pipelines, and
CI_PIPELINE_SOURCEand related variables tell jobs which event occurred. - Merge requests bring code review and pipeline results together, and are the main place where quality is enforced.
- Protected branches and tags control who can change important refs and are the only refs that receive protected variables.
- Without
workflow:rules, a push to a branch with an open MR can create duplicate pipelines.
Practice
- (Beginner) Create a branch, push a commit, open an MR and identify which pipeline(s) ran and what
CI_PIPELINE_SOURCEeach one had (print it in a job).- (Intermediate) Configure
mainas protected with "Allowed to push and merge: No one" and "Allowed to merge: Maintainers". Describe the resulting workflow for a Developer who wants to ship a fix.- Interview question: "Why can a deployment job read a secret on
mainbut not on a feature branch?" Hint: look into protected variables and protected branches, and consider what an attacker could do if any branch could read production credentials.
GitLab Tiers and Feature Availability
Why it matters. Every GitLab documentation page states which tiers and offerings a feature is available in. Building a pipeline around a feature your subscription lacks wastes time, and knowing the tier boundaries helps you make the case for an upgrade or plan around it.
What the tiers are. GitLab has three tiers: Free, Premium and Ultimate. Each one includes everything in the tiers below it. As a broad rule, Free covers individual developers and core CI/CD, Premium adds team productivity and delivery controls, and Ultimate adds security, compliance and portfolio-level governance.
The table below shows typical CI/CD-related availability at the time of writing. Tier boundaries shift between releases, so always check the "Tier" label at the top of the relevant documentation page.
| Capability | Free | Premium | Ultimate |
|---|---|---|---|
Pipelines, stages, needs, rules, includes | Yes | Yes | Yes |
| Parent-child pipelines, CI/CD components and catalog | Yes | Yes | Yes |
| Artifacts, caching, container and package registries | Yes | Yes | Yes |
| Environments, review apps, releases, GitLab Pages | Yes | Yes | Yes |
| SAST and secret detection (basic results) | Yes | Yes | Yes |
| Merged results pipelines, merge trains | No | Yes | Yes |
| Protected environments, deployment approvals | No | Yes | Yes |
| Required MR approval rules, code owners | No | Yes | Yes |
| Security dashboards, vulnerability management | No | No | Yes |
| DAST, dependency scanning, API security testing, fuzzing | No | No | Yes |
| Security and pipeline execution policies | No | No | Yes |
| DORA metrics dashboards | No | No | Yes |
| Monthly compute minutes on GitLab.com hosted runners | 400 | 10,000 | 50,000 |
A documentation page shows availability in a header like this:
Tier: Premium, Ultimate
Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated
To check which version and edition an instance runs (useful on self-managed, where an older version may lack recent features):
# Version, revision and whether this is Enterprise Edition ("enterprise": true)
curl --silent --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"https://gitlab.example.com/api/v4/metadata"
When you use a feature your tier lacks, the outcome varies. Some keywords are rejected during validation. In other cases the job runs and produces its report, but the screen that displays the results (for example the security dashboard) is unavailable. Do not assume a passing job means the feature is fully working.
Key takeaways
- There are three tiers, Free, Premium and Ultimate, and each includes everything in the tiers below it.
- Core CI/CD (pipelines, rules, artifacts, registries, environments, components) is available in Free.
- Premium adds delivery controls such as merge trains, protected environments and approval rules. Ultimate adds security and compliance.
- Feature availability also depends on the offering (GitLab.com, Self-Managed, Dedicated) and, for self-managed, the installed version.
- Treat any tier table, including this one, as a snapshot and confirm it against the current documentation.
Practice
- (Beginner) Pick five features you plan to use from later chapters of this guide and look up their tier and offering labels in the GitLab documentation.
- (Intermediate) A team on GitLab.com Premium has 20 developers, each triggering about 15 pipelines per working day (22 days per month), and each pipeline uses 8 compute minutes. Does the included quota cover this? If not, propose a mix of changes (self-hosted runners, pipeline optimization, quota purchase) and discuss the trade-offs.
- Interview question: "Your company is on Premium and the security team wants vulnerability dashboards. What are your options?" Hint: compare upgrading the tier with running open-source scanners and processing their report artifacts yourself, and weigh licensing cost against engineering and maintenance cost.
GitLab Roles and Permissions
Why it matters. Pipelines can deploy to production, read secrets and publish packages, so who may start, change or approve them is a security question. GitLab's role system decides who can run pipelines, edit CI/CD settings and deploy. Jobs also act with the identity of the user who triggered them.
What the roles are. A member of a group or project has one of these roles, from least to most privileged:
| Role | Typical holder | CI/CD summary |
|---|---|---|
| Guest | Stakeholders, external viewers | Very limited pipeline visibility |
| Planner | Product and project managers | Planning features. No meaningful extra CI/CD rights |
| Reporter | QA, support, read-only engineers | View pipelines and job logs, download artifacts |
| Developer | Engineers | Run pipelines, retry and cancel jobs, run manual jobs on unprotected refs |
| Maintainer | Tech leads, DevOps engineers | Manage CI/CD settings, variables, runners, protected branches and environments |
| Owner | Group or project administrators | Everything, including deleting pipelines and changing visibility |
Premium and Ultimate add Minimal Access (group membership without project access). Ultimate also supports custom roles, which build on a base role and grant specific extra permissions.
A simplified view of key CI/CD permissions (see the permissions reference in the documentation for the full matrix):
| Action | Guest | Reporter | Developer | Maintainer | Owner |
|---|---|---|---|---|---|
| View pipelines and job logs | Limited* | Yes | Yes | Yes | Yes |
| Download job artifacts | Limited* | Yes | Yes | Yes | Yes |
| Run a pipeline, retry or cancel jobs (unprotected refs) | No | No | Yes | Yes | Yes |
| Run jobs on protected branches | No | No | If branch protection allows | Yes (by default) | Yes |
| Create pipeline schedules | No | No | Yes | Yes | Yes |
| Manage CI/CD variables and settings | No | No | No | Yes | Yes |
| Register and manage project runners | No | No | No | Yes | Yes |
| Configure protected branches and environments | No | No | No | Yes | Yes |
| Delete a pipeline | No | No | No | No | Yes |
* Depends on project visibility and the project's pipeline visibility setting.
Pipelines run as the user. Every job runs with the permissions of the user who triggered the pipeline. The job's CI_JOB_TOKEN can reach other projects (clone repos, pull packages) only to the extent that user can, and only if the target project's job token allowlist permits it. Two consequences follow:
- Removing someone from a project immediately stops their pipelines from reaching resources they no longer have access to.
- Anyone who can push a change to
.gitlab-ci.ymlon a branch can make jobs on that branch do anything that user's permissions and that branch's variables allow. This is why secrets live in protected variables and deployments use protected environments.
deploy-production:
stage: deploy
script:
- echo "Deployment started by $GITLAB_USER_LOGIN" # the user who triggered the pipeline or job
- ./deploy.sh production
environment:
name: production # if this environment is protected (Premium+), only allowed users or roles can run this job
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
Key takeaways
- From least to most privileged, the roles are Guest, Planner, Reporter, Developer, Maintainer and Owner. Roles are inherited down the group hierarchy.
- Developers run pipelines. Maintainers configure CI/CD (variables, runners, protections). Owners can delete pipelines.
- Jobs act with the triggering user's identity, including what
CI_JOB_TOKENcan access. - Protected branches, protected variables and protected environments together keep production credentials and deployments away from untrusted changes.
- Grant the lowest role that lets someone do their job, and use custom roles (Ultimate) when the built-in roles do not fit.
Practice
- (Beginner) Choose the minimal role for: (a) a QA engineer who only downloads test report artifacts; (b) a contractor who opens MRs and runs pipelines on feature branches; (c) a team lead who manages CI/CD variables and project runners.
- (Intermediate) A Developer clicks "Run" on a manual job that deploys to a
productionenvironment that only Maintainers may deploy to. Describe what happens, and how you would let one specific Developer deploy without promoting them to Maintainer.- Interview question: "Why does GitLab run jobs with the triggering user's permissions, and what security risks does that create?" Hint: consider
CI_JOB_TOKENscope, a malicious edit to.gitlab-ci.ymlin a merge request, and how protected refs limit the damage.
<a id="13-getting-started-with-gitlab-cicd"></a>
1.3 Getting Started with GitLab CI/CD
This subchapter is hands-on. It covers the configuration file, a first working pipeline, the tools for editing and checking configuration, and how to read the results.
The .gitlab-ci.yml File
Why it exists. GitLab needs to know what to run, in what order and under what conditions. Putting that definition in a file inside the repository makes the pipeline part of the code. It changes with the code, is reviewed with the code, and stays reproducible for any commit in history.
What it is. .gitlab-ci.yml is a YAML file at the root of the repository. When a pipeline is created, GitLab reads the file from the commit being built, so every branch and every historical commit carries its own pipeline definition. A project can point to a different path, or to a file in another project, under Settings > CI/CD > General pipelines > CI/CD configuration file.
The file contains two kinds of top-level entries:
| Entry type | Purpose | Examples |
|---|---|---|
| Global keywords | Configure the pipeline as a whole | stages, variables, default, workflow, include |
| Jobs | Units of work run by a runner | Any other top-level key, such as build, unit-tests, deploy-prod |
Each job needs at least a script, or a trigger for jobs that start other pipelines. Jobs whose names start with a dot (.template) are hidden. They never run on their own but can be reused as templates.
An annotated anatomy of the file:
# ---------- Global keywords: affect the whole pipeline ----------
stages: # ordered stages; jobs in the same stage run in parallel
- build
- test
- deploy
variables: # available to every job
APP_NAME: "inventory-service"
default: # values every job inherits unless it overrides them
image: alpine:3.20
before_script:
- echo "Running $CI_JOB_NAME for $APP_NAME"
# ---------- Jobs: any non-reserved top-level key ----------
compile:
stage: build
script:
- ./build.sh
unit-test:
stage: test
script:
- ./run-tests.sh
.deploy-template: # hidden job (leading dot): never runs by itself
script:
- ./deploy.sh "$DEPLOY_TARGET"
deploy:
stage: deploy
extends: .deploy-template # reuse the template's keywords
variables:
DEPLOY_TARGET: "production"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH # only on the default branch
If you don't declare stages, GitLab uses the defaults .pre, build, test, deploy, .post. A job with no stage goes into test.
Key takeaways
.gitlab-ci.ymlsits at the repository root by default and is read from the commit being built.- Top-level keys are either global keywords (
stages,variables,default,workflow,include) or job names. - Every runnable job needs a
script(ortrigger). Hidden jobs starting with.are templates only. - Jobs without a
stagebelong totest. Jobs in the same stage run in parallel.
Practice
- (Beginner) In the anatomy example, list which top-level keys are global keywords, which are runnable jobs and which are hidden jobs.
- (Beginner) Remove the
stagesblock from the example. Which stage does each job end up in, and does the pipeline still work?- Interview question: "Because GitLab reads
.gitlab-ci.ymlfrom the commit being built, what does that imply for rebuilding old commits and for merge requests from forks?" Hint: think about reproducibility on one side and untrusted contributors changing the pipeline on the other.
Creating a First Pipeline
Why it matters. Building one small, working pipeline end to end shows every moving part: the configuration file, the runner, jobs, stages, logs and artifacts. Everything later in this guide refines one of these pieces.
What you need.
- A GitLab project.
- An available runner. Check Settings > CI/CD > Runners. On GitLab.com, GitLab-hosted runners are enabled by default. New Free-tier accounts may need identity verification before they can use them. On self-managed, an administrator or maintainer must install and register a runner (chapter 6).
- A
.gitlab-ci.ymlfile committed to the repository.
Example project. A tiny Python application with one test:
hello-ci/
├── .gitlab-ci.yml
├── app.py
├── requirements.txt # contains: pytest
└── test_app.py
# app.py
def greet(name: str) -> str:
return f"Hello, {name}!"
# test_app.py
from app import greet
def test_greet():
assert greet("GitLab") == "Hello, GitLab!"
# .gitlab-ci.yml
stages:
- test
- package
default:
image: python:3.12-slim # every job runs in this container
unit-tests:
stage: test
script:
- pip install -r requirements.txt
- pytest --junitxml=report.xml # JUnit XML lets GitLab show results in the Tests tab
artifacts:
when: always # upload the report even when tests fail
reports:
junit: report.xml
package:
stage: package # starts only if every job in "test" succeeded
script:
- mkdir -p dist
- tar -czf "dist/hello-ci-$CI_COMMIT_SHORT_SHA.tar.gz" app.py
artifacts:
paths:
- dist/ # downloadable from the job page
expire_in: 1 week # delete automatically to save storage
Push it to GitLab:
git init --initial-branch=main
git remote add origin git@gitlab.com:<your-namespace>/hello-ci.git
git add .
git commit -m "Add first GitLab CI/CD pipeline"
git push -u origin main
What happens after the push:
git push
-> GitLab reads .gitlab-ci.yml at the pushed commit
-> validates it (if invalid: the pipeline fails immediately and is labeled "yaml invalid")
-> creates the pipeline and its jobs (status: created -> pending)
-> a runner whose tags match picks up a pending job
-> the runner pulls the image, fetches the repository, runs the script and streams the log
-> the job finishes with success or failed; artifacts are uploaded
-> when every job in stage "test" succeeds, stage "package" starts
-> the pipeline status appears on the commit, branch and any merge request
Open Build > Pipelines to watch it run. To push without creating a pipeline (for example, a typo fix in a README), add [skip ci] to the commit message or use git push -o ci.skip.
Key takeaways
- A first pipeline needs three things: a project, an available runner and a
.gitlab-ci.ymlfile. - Pushing a commit is enough to trigger the pipeline. No extra setup is required.
- Stages run in order. A later stage starts only after every job in the previous stage succeeds.
- Artifacts keep job outputs, and
reports: junitturns test results into a readable Tests tab. - A job that stays
pendingusually means no runner is available or none matches the job's tags.
Practice
- (Beginner) Recreate
hello-ci, push it and confirm the pipeline passes. Then change the expected string in the test, push again and find the failing assertion in both the job log and the Tests tab.- (Intermediate) Add a
lintjob that runsruff check .in theteststage, in parallel withunit-tests. Makepackagerun only on the default branch.- Interview question: "A job has been in the
pendingstate for 30 minutes. What do you check?" Hint: consider whether any runner is online for the project, whether its tags match the job'stags, protected runners combined with unprotected branches, and compute quota or identity verification on GitLab.com.
Pipeline Editor
Why it exists. The usual edit, commit, push, wait and read-the-error loop is slow when the error is a misspelled keyword. The pipeline editor gives feedback before you commit, much as an IDE flags compile errors while you type instead of at build time.
What it is. The pipeline editor (Build > Pipeline editor) is a browser-based editor built specifically for .gitlab-ci.yml. If the project has no configuration yet, it offers to create one from a starter template. Its main parts are:
| Feature | What it does |
|---|---|
| Branch selector | Edits the configuration on a specific branch |
| Edit tab | Syntax highlighting, keyword autocompletion and live syntax validation |
| Visualize tab | Draws the stages and jobs the configuration defines |
| Validate tab | Simulates pipeline creation to catch errors that only appear at runtime |
| Full configuration tab | Shows the merged result after include, extends and anchors are resolved |
| CI/CD Catalog button | Opens the catalog to find reusable components |
| Commit section | Commits to the current branch, or to a new branch with an optional MR |
+---------------------------------------------------------------------------+
| Branch: [ feature/ci-update v ] [ Browse CI/CD Catalog ] |
+----------+------------+------------+-------------------------------------+
| Edit | Visualize | Validate | Full configuration |
+----------+------------+------------+-------------------------------------+
| stages: |
| - build |
| - test |
| ... |
+---------------------------------------------------------------------------+
| Pipeline syntax is correct. |
+---------------------------------------------------------------------------+
| Commit message: [ Update CI configuration ] |
| Branch: [ feature/ci-update ] [x] Start a new merge request |
| [ Commit changes ] |
+---------------------------------------------------------------------------+
(simplified layout)
The Full configuration tab is the quickest way to understand configuration split across files. Given these two files:
# .gitlab-ci.yml
include:
- local: ci/templates.yml
test:
extends: .python-job
script:
- pytest
# ci/templates.yml
.python-job:
image: python:3.12
before_script:
- pip install -r requirements.txt
the Full configuration tab shows what GitLab actually runs:
# Merged result (simplified)
".python-job":
image: python:3.12
before_script:
- pip install -r requirements.txt
test:
image: python:3.12 # inherited through extends
before_script:
- pip install -r requirements.txt # inherited through extends
script:
- pytest # defined in the job itself
Key takeaways
- The pipeline editor validates syntax as you type, which shortens the feedback loop for configuration changes.
- Visualize shows pipeline structure, Validate simulates pipeline creation, and Full configuration shows the merged result of includes and templates.
- It always works against a specific branch. Committing from the editor triggers a pipeline like any other push.
- Valid syntax does not guarantee the pipeline behaves as intended.
rulesand scripts are evaluated only at run time.
Practice
- (Beginner) Open the pipeline editor, misspell
scriptassciptin one job and read the error. Then reference a stage that is not listed instagesand compare the messages.- (Intermediate) Move the shared
imageandbefore_scriptof two jobs into a hidden template in an included local file. Use the Full configuration tab to confirm that the merged result is the same as before.- Interview question: "Why can a configuration that the pipeline editor reports as valid still create no jobs when you push?" Hint: consider
rulesandworkflow:rulesthat depend on run-time values such as the branch name orCI_PIPELINE_SOURCE.
CI Lint and Configuration Validation
Why it exists. A configuration mistake found after pushing wastes a pipeline run and a few minutes of attention. In shared template repositories it can break dozens of downstream projects at once. Validating configuration before it is used, interactively or in automation, catches these errors early.
What it is. Validation happens at several levels, and each tool covers a different set of them:
| Level | What is checked | Where |
|---|---|---|
| YAML syntax | Well-formed YAML (indentation, quoting) | Any YAML-aware editor, pipeline editor |
| Schema | Known keywords and correct value types | Pipeline editor, IDE plugins using GitLab's CI JSON schema |
| Semantics | Stages exist, needs targets exist, includes resolve | CI Lint, pipeline editor, Lint API |
| Simulation | Which jobs a ref would actually create | CI Lint dry run, pipeline editor Validate tab |
| Runtime | Scripts actually succeed | Only a real pipeline run |
How to use it.
- CI Lint in the UI. It lives in the pipeline editor, and also at
https://<gitlab-host>/<project-path>/-/ci/lint. Paste a configuration, optionally enable "Simulate pipeline creation for the default branch", and GitLab reports errors, warnings and the resulting jobs. - The Lint API.
POST /projects/:id/ci/lintvalidates arbitrary content in the project's context (soinclude:localand project includes resolve).GET /projects/:id/ci/lintvalidates the configuration already stored in the project. - The
glabCLI.glab ci lintsends a local file to the Lint API of the current project.
# Lint .gitlab-ci.yml in the current directory against the current project
glab ci lint
# Lint a different file, such as a child pipeline definition
glab ci lint ci/child-pipeline.yml
The Lint API in automation, for example inside a shared templates repository:
# Build the JSON body from the local file; dry_run simulates pipeline creation
jq --null-input --arg yaml "$(cat .gitlab-ci.yml)" \
'{content: $yaml, dry_run: true, include_jobs: true}' > lint-request.json
# Send it to the Lint API of project 12345
curl --silent --request POST \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
--header "Content-Type: application/json" \
--data @lint-request.json \
"https://gitlab.com/api/v4/projects/12345/ci/lint" > lint-response.json
# Print only the parts we care about: validity, errors, warnings and job names
jq '{valid, errors, warnings, jobs: [.jobs[]?.name]}' lint-response.json
Typical error messages look like this:
jobs:unit-tests config contains unknown keys: scipt
unit-tests job: chosen stage tests does not exist; available stages are .pre, build, test, deploy, .post
jobs:deploy:rules:rule if invalid expression syntax
Key takeaways
- Validation is layered: YAML syntax, schema, semantics, simulation and runtime. No single tool covers every layer.
- CI Lint resolves includes in the project's context, and its dry run shows which jobs a ref would actually create.
- Use
glab ci lintor the Lint API to validate configuration in scripts and in CI for template repositories. - Only running the pipeline proves that the scripts themselves work.
Practice
- (Beginner) Run
glab ci linton one of your projects. Then add a job thatneedsa non-existent job and record the error message.- (Intermediate) In a repository of shared CI templates, write a job that lints every
templates/*.ymlfile through the Lint API, using a project access token stored in a masked CI/CD variable, and fails if any file is invalid.- Interview question: "What is the difference between static validation of a CI configuration and a dry-run simulation, and when does the difference matter?" Hint: think about
rules,workflow:rulesand includes that depend on the ref and its variables.
Reading Pipeline and Job Views
Why it matters. Every CI/CD debugging session starts with reading what GitLab shows: which pipeline ran, which job failed, why, on which runner and with what output. Reading these views quickly is the most-used CI/CD skill there is.
The pipelines list (Build > Pipelines). Each row shows a pipeline's status, its ID, the source ref and commit, who or what triggered it, its duration and a mini-graph of stages. Labels explain why the pipeline exists or what is special about it:
| Label | Meaning |
|---|---|
latest | Most recent pipeline for this ref |
merge request | Merge request pipeline |
merged results | Pipeline runs on the result of merging source into target |
merge train | Pipeline belongs to a merge train |
scheduled | Started by a pipeline schedule |
child | Downstream child pipeline |
yaml invalid | Configuration failed validation, no jobs were created |
stuck | Jobs are pending with no runner able to pick them up |
There are two kinds of pipeline number. CI_PIPELINE_ID is unique across the whole GitLab instance. CI_PIPELINE_IID counts up within the project, and is the number shown with # in most project views.
The pipeline detail page. Its tabs show the Pipeline graph (jobs grouped by stage, or by needs dependencies), a flat Jobs list, Failed jobs with log excerpts, and Tests (from JUnit reports). Some tiers add more tabs, such as Security and Code Quality.
Pipeline #1287 passed with warnings main 1a2b3c4d triggered by @alice
build test deploy
+-------------+ +------------------+ +---------------------+
| (v) compile | --> | (v) unit-tests | --> | (>) deploy-prod | <- manual: click Run
+-------------+ | (!) lint | +---------------------+
+------------------+
Legend: (v) passed (x) failed (!) failed but allowed to fail (>) manual (-) skipped
"Passed with warnings" means every required job succeeded, but at least one job marked allow_failure: true failed.
Job statuses.
| Status | Meaning |
|---|---|
created | Job exists but its stage has not started yet |
waiting_for_resource | Waiting for a resource_group lock |
pending | Queued and waiting for a matching runner |
preparing | A runner accepted it and is preparing the environment |
running | Script is executing |
success / failed | Finished with exit code 0 / non-zero (or a system error) |
canceling / canceled | Cancellation in progress / completed |
skipped | Will not run (for example a previous stage failed) |
manual | Waiting for someone to run it |
scheduled | Delayed job waiting for its start time |
The job log page. The header shows the status, duration, queue time and the runner that ran the job. The sidebar shows artifacts (keep, download, browse), the commit, the pipeline, tags, the timeout and a Retry button. The log itself is divided into collapsible sections that follow the runner's lifecycle:
Running with gitlab-runner 19.4.0 (abcdef12) <- runner version
on docker-runner-01 xYz12AbC, system ID: s_0123456789ab <- which runner took the job
Preparing the "docker" executor <- executor setup
Using Docker executor with image python:3.12-slim ...
Pulling docker image python:3.12-slim ...
Preparing environment
Getting source from Git repository <- fetch the exact commit
Fetching changes with git depth set to 20...
Checking out 1a2b3c4d as detached HEAD (ref is main)...
Executing "step_script" stage of the job script <- before_script + script
$ pip install -r requirements.txt
...
$ pytest --junitxml=report.xml
FAILED test_app.py::test_greet - AssertionError <- the real failure
Uploading artifacts for failed job <- because artifacts:when: always
report.xml: found 1 matching artifact files and directories
Cleaning up project directory and file based variables
ERROR: Job failed: exit code 1 <- non-zero exit code = failed job
Read a failed log from the bottom up. Find the last command ($ ...) before the error, then look just above it for the first real error message. Errors in the "Preparing" or "Getting source" sections point to infrastructure (runner, image, network). Errors in "step_script" point to your code or scripts.
Key takeaways
- Pipeline labels tell you why a pipeline exists (MR, schedule, child) or what is wrong with it (
yaml invalid,stuck). CI_PIPELINE_IIDis the per-project number you usually see.CI_PIPELINE_IDis unique across the instance.- Job statuses separate waiting for a runner (
pending), waiting for a person (manual) and waiting for a lock (waiting_for_resource). - Job logs follow the runner lifecycle. The section where the error appears tells you whether to look at infrastructure or at your code.
- "Passed with warnings" means an allowed-to-fail job failed. "Blocked" means a required manual job is waiting.
Practice
- (Beginner) Open any job log and find: the runner that ran the job, the image used, the commit SHA checked out and the job's total duration.
- (Intermediate) Create a pipeline whose final status is "passed with warnings", and a second pipeline whose status is "blocked". Explain which configuration produced each.
- Interview question: "A job fails with
ERROR: Job failed: exit code 1, but the visible output shows no error. How do you find the cause?" Hint: look at the last command that ran, at commands that fail silently, at scripts swallowing errors, and at debug tracing (CI_DEBUG_TRACE, covered in chapter 14).
<a id="2-pipeline-configuration-fundamentals"></a>