GitLab CI/CD DevSecOps
DevSecOps Learning Series by Georges MOMO

From git push to a signed, verified deployment on GitLab

A free, hands-on course that teaches GitLab and GitLab CI/CD by building a real DevSecOps pipeline: tests, code and secret scanning, image scanning with a blocking gate, an SBOM, keyless signatures, and a deployment that refuses any image it cannot verify.

  • 18 stations
  • ~8 h at your own pace
  • 75 GitLab features explained
  • Free, MIT licensed
Written by Georges MOMO, DevSecOps engineer. I built this lab to learn in public and to give others a real project to practise on, not just slides.

Welcome aboard

15 min, nothing to install
By the end of this station you can
  • say what this course builds and why each step exists;
  • choose how you will follow it: reading, practising, or preparing a certification;
  • read the tier labels (Free, Premium, Ultimate) used everywhere below.

Most CI/CD tutorials stop when the tests turn green. Real companies need more: they need to know what they ship, that nobody slipped a secret or a vulnerable library into it, and that the thing running in production is exactly the thing the pipeline built. That is what DevSecOps means in practice: security checks woven into the delivery pipeline instead of a review at the end.

This course follows a small but complete project, devsecops-gitlab-lab. It contains a tiny Python web API (Flask), a Dockerfile, and a .gitlab-ci.yml file that turns every push into the journey drawn above. Every screenshot comes from that project on GitLab.com, and every file you read here is the real file in the repository.

Think of it like this

The pipeline is a train line. Your commit is the train. Each station checks something; some stations are security gates where the train stops if a check fails. The course itself uses the same picture: the line on the left fills up as you complete stations.

Three ways to use this course

Read the stations in order. Click the numbered dots on screenshots, open the feature sheets that interest you, answer the quizzes. You need nothing but a browser. Expect about eight hours in total, in sessions of 20 to 60 minutes.

Create a free GitLab.com account, fork the lab project, and run the pipeline yourself. Station 7 walks through every file; each station has a hands-on block. On GitLab.com the Free tier includes a monthly quota of compute minutes (400 at the time of writing), which is far more than this lab needs: one full run used about 4.5 minutes.

Station 16 maps the course to the GitLab Certified CI/CD Associate exam. Use the quizzes, the feature sheets' exam tips and the final exam (station 17) as revision material.

How to read the tier labels

GitLab comes in three tiers. Everything this lab needs works in Free. Some features need Premium (team governance: mandatory approvals, code owners, protected environments, merge trains) or Ultimate (consolidated security: vulnerability report, security widget in merge requests, dependency scanning, security policies). Every feature sheet tells you which tier it needs, because in real companies that decides what you can actually use.

Feature sheets

Grey cards with a dot are feature sheets. The first line says what the feature is; open the card to see why it exists, how companies use it, how to set it up, its limits and an exam tip. There are 75 of them, all listed in the feature index.

Hands-onGet your own copy of the lab

  1. Create an account on GitLab.com (free) and turn on two-factor authentication.
  2. Fork the project: open the lab project and click Fork, then pick your personal namespace.
  3. Run the pipeline: in your fork, go to Build, Pipelines, New pipeline, branch main, Run pipeline.
Show a hint

New GitLab.com accounts may have to validate their identity (for example with a phone number or a card) before they can use the shared runners. That is an anti-abuse measure, not a charge.

GitLab in five minutes

25 min, concepts
By the end of this station you can
  • name the building blocks of GitLab: group, project, merge request, work item, pipeline, runner, registry;
  • explain how they fit together;
  • explain what makes GitLab different from GitHub.

GitLab describes itself as one application for the whole DevSecOps lifecycle: code, tickets, CI/CD, registries, security, deployment and monitoring live in the same product. That is its biggest difference with GitHub, which relies more on apps and third-party actions. For a learner it is good news: everything you need is in one place, behind one left sidebar.

The building blocks

BlockWhat it isIn this lab
Group and subgroupA team container (like a GitHub organization), nestable without limit: acme/platform/payments. Permissions, CI variables and runners defined on a group flow down.The personal namespace georges.momo.n8n.
ProjectA git repository plus everything GitLab adds around it: work items, merge requests, pipelines, registry, environments, wiki.devsecops-gitlab-lab
Merge requestGitLab's pull request: propose merging a branch, get it reviewed, see the pipeline and security reports, then merge.Merge request !1
Work itemThe generic name for tickets: issue, task, epic, incident. They live under Plan, on boards and milestones.Bug template
Pipeline, stage, jobA pipeline is one run of .gitlab-ci.yml. It is made of stages (columns) that contain jobs (shell scripts running in a container).6 stages, 11 jobs
RunnerThe program that executes jobs. GitLab.com provides instance runners; you can also install your own.GitLab.com Linux runners
Container registryDocker images (and, next door, a package registry for npm, Maven, PyPI…) built into each project, authenticated by the job's token.One image per commit, plus its signature
GitLab DuoThe built-in AI: chat, code suggestions, agents and flows.Station 14
Think of it like this

A group is a building, a project is an apartment, the repository is the furniture. Merge requests are the renovation requests that the landlord reviews, and the pipeline is the inspector who checks every renovation before anyone can move in.

How the pieces talk to each other

  1. You push a branch and open a merge request.
  2. GitLab reads .gitlab-ci.yml and creates a pipeline for that event.
  3. Runners pick up the jobs, run them in containers and send back logs, reports and artifacts.
  4. The merge request shows the result: tests, scans, approvals. A reviewer merges.
  5. The merge to main starts another pipeline, which builds, signs and deploys.
In real companies

Groups mirror the organisation chart: one top-level group per company (that is where the subscription is bought), subgroups per department and team. Platform teams put shared runners, CI templates and security variables at the top so hundreds of projects inherit them. Compute minutes on GitLab.com are counted per top-level group.

Hands-onDraw your own organisation

Sketch the groups and subgroups you would create for a company with a web team, a mobile team and a platform team. Where would you define a variable that every project needs, such as the address of an internal registry mirror?

Show the solution

One top-level group acme, with subgroups acme/web, acme/mobile and acme/platform. A variable needed by everyone goes in the top-level group's CI/CD settings, so it is inherited by every project. Team-specific values go on the team subgroup.

Finding your way around

25 min, interface
By the end of this station you can
  • use the top bar, search and To-Do List;
  • create projects and groups and invite members with the right role;
  • find your account security settings.

GitLab moved almost all navigation into the left sidebar. The top bar keeps search, quick creation, your counters and your avatar; a column on the far right opens GitLab Duo. Click the numbered dots on the screenshot to see what each element does.

GitLab project page with the top bar and the GitLab Duo column on the right
The top bar and the Duo column are the same on every page.
To-Do List and countersFreeThe three counters at the top right: issues assigned to you, merge requests (assigned or waiting for your review) and your To-Do List.
Why it exists
You see at a glance what is waiting for you: a review request, a mention, a broken pipeline on your merge request.
In real companies
Many developers start the day by emptying their To-Do List like an inbox.
How to use it
A to-do is created automatically when someone mentions you, assigns you or asks for your review. Click Mark as done when handled. Shortcut Shift+t.
Limits and pitfalls
Email notifications are configured separately (avatar, Preferences, Notifications, or the bell on a project).

The + button

The + menu open, listing creation options for the project and globally
The + menu: first what you can create in this project, then globally.

The menu is contextual: the first block creates things inside the project you are looking at, the second block creates things globally.

Groups and subgroupsFreeA space that contains projects and other groups, with its own members, CI/CD variables, runners and settings.
Why it exists
Companies organise work by teams and products, and want to define settings once so every project inherits them.
In real companies
acme/platform for the infrastructure team, with a group runner and a shared REGISTRY_MIRROR variable. Rights given on the group apply to every project inside.
How to use it
+, New group. Group settings, CI/CD (variables, runners); Manage, Members. A paid subscription is bought on the top-level group.
Limits and pitfalls
Moving a project to another group changes its URLs and its registry path. On GitLab.com, compute minutes are counted per top-level group.
Members and rolesFree (custom roles: Ultimate)The people who can access a project or group, each with a role: Guest, Planner, Reporter, Developer, Maintainer, Owner.
Why it exists
Least privilege: decide who reads, who plans, who pushes, who merges into main, who administers.
In real companies
Developers get Developer (push branches, open merge requests); tech leads get Maintainer (merge into protected branches, settings); Owner is kept for two people.
How to use it
Manage, Members, Invite members: pick a role and an expiry date. Access given on a group flows down to its projects.
Limits and pitfalls
The role also decides what the CI_JOB_TOKEN can do when that person runs a pipeline. Custom roles need Ultimate.
Exam tip
Know the role ladder, and that Maintainer is the default minimum to push or merge into a protected branch.
Common pitfall

Giving everyone Maintainer "to make things easier". Maintainers can change settings, merge into protected branches and, through the CI_JOB_TOKEN of pipelines they start, reach more than a Developer can. Start with Developer and raise only when needed.

The avatar menu

The avatar menu open
The avatar menu.
Profile and preferencesFreeThe avatar menu: status, profile, preferences (theme, language, editor), subscription, what's new, sign out.
Why it exists
This is where your account security lives: SSH keys, personal tokens, two-factor authentication, authorised applications.
In real companies
On day one in a company: turn on two-factor authentication, add an SSH key, set your time zone so log timestamps make sense.
How to use it
Avatar, Edit profile, then the left menu: Access tokens, SSH keys, Applications, Account (2FA), Notifications.
Limits and pitfalls
"GitLab Next" switches to the preview version of GitLab.com: useful to try new things, not for stable daily work.
Keyboard shortcuts

/ search, t find a file, . open the Web IDE, Shift+t To-Do List, ? the full list.

Hands-onSecure your account in five minutes

  • Turn on two-factor authentication (avatar, Edit profile, Account).
  • Add an SSH key (Edit profile, SSH keys) and clone your fork with git clone git@gitlab.com:….
  • Set your time zone in Preferences so log timestamps match your clock.

A project and its sidebar

45 min, interface tour
By the end of this station you can
  • find any feature from the left sidebar, whose sections follow the DevSecOps lifecycle;
  • explain what Plan, Code, Build, Secure, Deploy, Operate, Monitor and Analyze contain;
  • clone a project and edit it in the browser.

The left sidebar is the map of GitLab. Its sections follow the lifecycle: plan, code, build, secure, deploy, operate, monitor, analyze. Here is each one, opened on the lab project.

Home page of the devsecops-gitlab-lab project with the left sidebar, file tree and project information
The home page of the lab project.
Branches, commits and tagsFreeCode, Branches / Commits / Tags: the web view of the project's git history.
Why it exists
Compare, create and delete branches, see the pipeline status of every commit, create version tags.
In real companies
Spot stale branches, find the commit that broke production thanks to the pipeline dots, tag v1.2.0 to trigger a release.
How to use it
Branch selector above the file tree. Code, Compare revisions for a diff between two refs. A tag starts a pipeline where CI_COMMIT_TAG is set.
Limits and pitfalls
Deleting a branch does not delete its pipelines. Protected tags stop anyone from creating v* tags.
Project information shortcutsFreeThe right-hand column of a project page: description, statistics and links such as Add LICENSE, Add CHANGELOG, CI/CD configuration, Environments.
Why it exists
One click to the key files and to the first setup steps of a project.
In real companies
When you land on an unknown project, this column tells you whether it has a pipeline, environments, how many branches and tags.
How to use it
"CI/CD configuration" opens .gitlab-ci.yml; "2 Environments" opens Operate, Environments.
Limits and pitfalls
The "Add …" shortcuts disappear once the file exists. That is expected.

Plan and Code

The left sidebar with Plan and Code expanded
The Plan and Code sections, expanded.
Work items (issues, tasks, epics)Free (epics: Premium)Plan, Work items: the project's tickets. A work item has a type (issue, task, epic, incident…), a status, assignees, labels, a milestone, a weight.
Why it exists
Track bugs, requests and tasks, and link them to the merge requests that resolve them.
In real companies
A bug reported with the Bug.md template, split into tasks, linked to a merge request whose description says Closes #12 so it closes on merge.
How to use it
+, New work item, or from a merge request. Templates come from .gitlab/issue_templates/.
Limits and pitfalls
Epics and multi-level hierarchies are paid features. A confidential work item is only visible to members with Planner role and above.
Issue boards and milestonesFree (scoped labels: Premium)A kanban board whose columns are labels, assignees or milestones. Milestones group tickets by due date.
Why it exists
Visualise the flow of work (to do, doing, review, done) and the content of a sprint or a release.
In real companies
A team board used in the daily stand-up; a v1.3 milestone with its burndown chart.
How to use it
Plan, Issue boards, columns per label (for example workflow::doing). Plan, Milestones, New milestone with dates.
Limits and pitfalls
Scoped labels (exclusive key::value) and advanced burndown charts are paid.
LabelsFree (scoped: Premium)Coloured tags on work items and merge requests, defined per project or per group.
Why it exists
Classify (bug, security), drive boards, filter, trigger triage rules.
In real companies
Group labels shared by every project: type::bug, priority::1, workflow::review.
How to use it
Manage, Labels. Scoped labels use :: and are mutually exclusive (paid tiers).
Limits and pitfalls
Too many labels kill labels: define them at group level.
WikiFreeMarkdown documentation stored in a separate git repository attached to the project.
Why it exists
Keep runbooks, architecture decisions and onboarding guides next to the code.
In real companies
An on-call runbook linked from incidents; the secret rotation procedure.
How to use it
Plan, Wiki, Create your first page. Clone it with <project>.wiki.git.
Limits and pitfalls
No merge request review by default. For controlled documentation, prefer a docs/ folder in the repository, like this course.
SnippetsFreeVersioned fragments of code or configuration, shared at project or personal level.
Why it exists
Share a script, a query or a config excerpt without creating a repository.
In real companies
A runner diagnostic script shared in the platform project.
How to use it
Code, Snippets, New snippet; visibility private, internal or public.
Limits and pitfalls
Never put a secret in a snippet: public snippets are indexed by search engines.

Build, Secure and Deploy

The left sidebar with Build, Secure and Deploy expanded
The Build, Secure and Deploy sections, expanded.

Build holds the CI features you will meet in stations 5 to 9. Secure holds the scanners (station 10). Deploy holds what the pipeline publishes:

ReleasesFreeA published version tied to a git tag, with release notes, links to binaries or images, and evidence.
Why it exists
Tell people what was delivered and give stable download links.
In real companies
A pipeline on tag v1.2.0 builds and signs, then a release job creates the release with a link to the signed image.
How to use it
Deploy, Releases, New release, or in YAML: release: tag_name, description (using release-cli or glab).
Limits and pitfalls
A release does not build anything: it points to what the pipeline produced.
Feature flagsFreeSwitches for features, managed in GitLab and compatible with Unleash SDKs.
Why it exists
Ship code switched off, turn it on for 10% of users or one environment, switch it off without redeploying.
In real companies
A new checkout page enabled first in staging, then for a percentage of production users.
How to use it
Deploy, Feature flags, New flag, strategies per environment. The application queries the API with the URL and instance ID GitLab gives you.
Limits and pitfalls
Every flag is debt: plan its removal.
Package registryFreeA built-in registry for npm, Maven, PyPI, NuGet, Composer, Conan, Helm, Go and generic packages.
Why it exists
Publish and consume internal libraries with the project's own permissions.
In real companies
An internal Python library published by the pipeline on every tag, installed by other teams with pip and a token.
How to use it
Deploy, Package registry. Publish from CI with the CI_JOB_TOKEN, for example to $CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/pypi.
Limits and pitfalls
The dependency proxy (a cache for Docker Hub images) is a separate, group-level feature.

Manage and AI

The left sidebar with Manage and AI expanded
The Manage and AI sections, expanded.

Manage is where members and labels live (seen above). AI lists the agents enabled in the project; station 14 covers them.

Operate, Monitor, Analyze and Settings

The left sidebar with Operate, Monitor, Analyze and Settings expanded
The Operate, Monitor, Analyze and Settings sections, expanded.
Terraform / OpenTofu states and modulesFreeAn HTTP backend for Terraform state managed by GitLab, plus a module registry.
Why it exists
Infrastructure as code without managing a state bucket and its locks yourself.
In real companies
The OpenTofu CI component runs plan in the merge request and apply on merge.
How to use it
Operate, Terraform states. HTTP backend at $CI_API_V4_URL/projects/$CI_PROJECT_ID/terraform/state/<name>.
Limits and pitfalls
State files can contain secrets: restrict who can read them (Maintainer by default).
Monitor: error tracking, alerts, incidents, Service DeskFree (advanced on-call: paid)Operations tools: application errors (Sentry-compatible), incoming alerts, incidents with a timeline, Service Desk (tickets by email).
Why it exists
Keep operations in the same tool as the code and the delivery.
In real companies
An alert opens an incident, the on-call engineer follows the wiki runbook, the fix merge request is linked to the incident.
How to use it
Monitor, then the relevant page; alerts arrive through an HTTP endpoint.
Limits and pitfalls
Advanced on-call (rotations, escalations) is paid. Many companies keep a dedicated tool.
Analyze: value stream, CI/CD analytics, DORAFree (full DORA: Ultimate)Dashboards about the delivery flow: time per stage, pipeline success rate and duration, deployment frequency.
Why it exists
Measure to improve: where work waits, which pipelines are slow or flaky.
In real companies
Show that adding needs and a cache cut the median pipeline time in half.
How to use it
Analyze, CI/CD analytics; Value stream analytics; Repository analytics.
Limits and pitfalls
The four DORA metrics in full and multi-project dashboards need Ultimate.

The Code button

The Code dropdown with clone URLs, Web IDE, IDE and download options
The Code button.
Clone over SSH or HTTPSFreeThe Code menu gives clone URLs, IDE shortcuts and source downloads.
Why it exists
Get the project on your machine to work with git.
In real companies
SSH with one key per laptop; HTTPS with a token when a corporate proxy blocks SSH.
How to use it
Avatar, Edit profile, SSH keys, Add new key, then git clone git@gitlab.com:…. Over HTTPS the password is a token (scopes read_repository or write_repository).
Limits and pitfalls
With two-factor authentication on, passwords do not work over HTTPS: use a token.
Web IDEFreeA VS Code editor in the browser, opened on the repository (press . or Code, Web IDE).
Why it exists
Edit several files, review diffs and commit without installing anything.
In real companies
Fix a pipeline YAML and a Dockerfile from a locked-down laptop.
How to use it
Code, Web IDE, edit, Source Control panel, Commit and push (to a new branch plus a merge request).
Limits and pitfalls
No terminal and no execution: for that you need a Workspace.
WorkspacesPremiumRemote development environments (a container plus an IDE) started from GitLab in your own Kubernetes cluster.
Why it exists
A reproducible development machine in one click, described by a devfile in the repository.
In real companies
Onboarding: a newcomer opens a workspace that already has the right Python and Docker versions.
How to use it
Needs a Kubernetes agent configured for workspaces and a .devfile.yaml.
Limits and pitfalls
Unlike GitHub Codespaces, the infrastructure is yours to run.
Think of it like this

The Web IDE is a notebook you can write in anywhere but cannot run anything in. A Workspace is a full workshop with tools plugged in, but someone has to rent the building (your Kubernetes cluster).

Hands-onChange the API message from the browser

In your fork, press . to open the Web IDE, open app/app.py, change the MESSAGE string, and commit to a new branch with "Start a new merge request" ticked.

Show a hint

In the Source Control panel, the commit button offers "Commit to a new branch". GitLab then proposes to open a merge request. Keep it open: station 4 uses it.

Anatomy of a merge request

35 min, collaboration
By the end of this station you can
  • read every part of a merge request;
  • explain approvals, code owners and templates, and which tier enforces them;
  • explain why a push can create two pipelines and how the lab avoids it.

The merge request is where code review, automated checks and decisions meet. In the lab, merge request !1 comes from the branch feature/nouveau-message ("new message") and changes the API message. Its description was filled in automatically by a template.

Merge request !1 from feature/nouveau-message to main, with description, pipeline, approve and merge options
Merge request !1 in the lab: branch feature/nouveau-message into main.
Merge requestFree (security widget: Ultimate)A request to merge a source branch into a target branch, with discussion, diff, pipeline, approvals and reports.
Why it exists
The single checkpoint before code enters main: human review, tests, scans.
In real companies
Every change to main goes through a merge request: protected branch without direct push, pipeline required, review by a peer.
How to use it
Code, Merge requests, New, or the link printed after a git push. Tabs: Overview, Commits, Pipelines, Changes. Set to auto-merge merges as soon as checks pass. Draft: in the title marks a draft.
Limits and pitfalls
"Delete source branch" is ticked by default. The detailed security widget is Ultimate.
Exam tip
Branch pipeline versus merge request pipeline versus merged results pipeline (the merge request virtually merged with its target, a paid feature).
Merge request approvalsFree (required rules: Premium)The Approve button and, in paid tiers, mandatory approval rules (how many, who, which groups).
Why it exists
Guarantee that at least N qualified people reviewed the change before it is merged.
In real companies
Two approvals including someone from the security team for any change under .gitlab/; authors cannot approve their own work.
How to use it
Free: approving is possible but optional ("Approval is optional"). Premium: Settings, Merge requests, Approval rules.
Limits and pitfalls
In Free an approval does not block the merge: rely on merge checks and branch protection.
CODEOWNERSPremiumA file that maps paths to owners (people or groups), optionally in sections.
Why it exists
Pick the right reviewers automatically and make their approval mandatory on protected branches.
In real companies
[CI] section: .gitlab-ci.yml owned by the platform team; [Application]: app/ owned by the product team. The lab has one.
How to use it
File at the root, in docs/ or in .gitlab/. Turn on "Require approval from code owners" on the protected branch.
Limits and pitfalls
In Free the file is present but not enforced.
Issue and merge request templatesFreeMarkdown files offered in the Description list when you create an issue or a merge request.
Why it exists
Get complete bug reports and merge requests that follow a checklist (tests, secrets, pipeline).
In real companies
The DevSecOps checklist of the lab's merge requests comes from .gitlab/merge_request_templates/Default.md.
How to use it
.gitlab/issue_templates/*.md and .gitlab/merge_request_templates/*.md. A template called Default is applied automatically.
Limits and pitfalls
Templates are suggestions: anyone can clear them. To enforce, combine with merge checks.

The lab's templates and owners, as they are in the repository

## What / why

Closes #

## DevSecOps checklist
- [ ] Tests added or updated
- [ ] No secret in the code
- [ ] Images and includes pinned
- [ ] Pipeline green

/assign me

Closes # links the merge request to an issue that closes on merge. /assign me is a quick action: a slash command GitLab executes when the description is saved.

## What is happening?

## Steps to reproduce

## Pipeline link (if relevant)

/label ~bug

The quick action /label ~bug adds the label automatically.

# CODEOWNERS sections: enforced with "Require approval from code owners" on a protected branch (Premium and Ultimate)
[CI]
.gitlab-ci.yml @georges.momo.n8n
.gitlab/ @georges.momo.n8n

[Application]
app/ @georges.momo.n8n
Dockerfile @georges.momo.n8n

Two sections, so the pipeline files and the application code can have different owners. In Free the file is ignored; in Premium it adds required approvers.

Why one push can create two pipelines

When a branch has an open merge request, a push is two events at once: a push to a branch, and an update to a merge request. Without care, GitLab creates a branch pipeline and a merge request pipeline for the same commit, which doubles your compute minutes. The lab's workflow:rules (station 6) keeps only the merge request pipeline:

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"   # merge request pipeline
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS    # avoids the duplicate branch + MR pipeline
      when: never
In real companies

main is protected so nobody can push to it directly; every change goes through a merge request with a green pipeline, at least one approval, and code owners for sensitive paths such as CI files, infrastructure code and authentication modules. Merge checks (station 11) turn these habits into rules.

Hands-onReview your own merge request

Open the merge request you created in station 3. Find: the pipeline it started (which source does it show?), the Changes tab, the Approve button, and the merge options. Then merge it and watch the pipeline on main.

Show the solution

The merge request pipeline carries the merge request badge: its source is merge_request_event. After you merge, a new pipeline starts on main with source push; that one also signs and deploys, because the lab's sign and deploy jobs only run on the default branch.

How GitLab CI works

50 min, the core model
By the end of this station you can
  • describe how an event becomes a pipeline, stages, jobs and runner work;
  • tell a cache from an artifact without hesitating;
  • use predefined variables and the pipeline editor.

One file at the root of the repository, .gitlab-ci.yml, describes everything. On every event (push, merge request, tag, schedule, API call, button click) GitLab reads that file, builds a pipeline, and runners come and fetch the jobs one by one.

  1. An event happens. A push, a merge request update, a tag, a schedule, an API call. GitLab records it as CI_PIPELINE_SOURCE.
  2. workflow:rules decides whether a pipeline is created at all for that event.
  3. Each job's rules decide whether that job is added to the pipeline.
  4. Stages run in order. By default a stage waits until every job of the previous stage is finished; jobs inside a stage run in parallel. needs can break that barrier (station 6).
  5. A runner polls GitLab ("any job for me?"), starts the job's container from its image, plus side containers called services, runs the script, and sends back the log and the artifacts.
Think of it like this

GitLab is a restaurant kitchen's order screen. .gitlab-ci.yml is the recipe book, the pipeline is one table's order, stages are courses (starters before mains), jobs are dishes, and runners are cooks who pick up the next ticket when their hands are free. A cache is the pantry: handy, but sometimes empty. An artifact is the plate passed to the next cook: it must arrive.

The keywords that matter

KeywordWhat it doesIn the lab
workflow:rulesDecides whether a pipeline is created, for which event.Merge request, branch (unless a merge request is open), tag.
rulesDecides whether a job is added (if, changes, exists, when). Replaces the old only/except.sign, the deployments and pages: only on main (sign also on tags).
stages, stageOrder of the columns; every job belongs to one stage (default: test).lint, test, build, scan, sign, deploy.
needsExplicit dependencies: start without waiting for the whole stage.The whole path from build to deploy.
image, servicesThe job's container, and side containers (database, Docker-in-Docker).docker:28 with docker:28-dind.
extends, hidden jobs .nameConfiguration inheritance. A job whose name starts with a dot never runs on its own..python, .docker, .deploy.
includeAssemble several files: local, project, remote, template, component.templates.yml plus three security templates.
parallel:matrixGenerate N jobs from a list of variables.Tests on Python 3.12 and 3.13.
cache, artifactsCache: go faster (may be missing). Artifact: pass a result (guaranteed, downloadable, expires).pip cache; JUnit, dotenv and SBOM artifacts.
environment, resource_groupTrack a deployment in Operate, Environments; run only one job at a time for a resource.staging, production.
when: manualThe job waits for a click. At job level it implies allow_failure: true, so the pipeline can still pass.deploy_production.
id_tokensAsk GitLab for a signed OIDC token with a given audience.SIGSTORE_ID_TOKEN for cosign.
interruptibleThe job may be cancelled when a newer commit arrives (with workflow:auto_cancel).The Python jobs.
PipelinesFreeBuild, Pipelines: every run, with status, source (branch, merge request, tag), author and stages.
Why it exists
See what is running, what failed, retry, start a run by hand.
In real companies
Check that a merge to main really deployed; rerun a nightly pipeline that hit an unavailable runner.
How to use it
New pipeline: pick a branch and, if allowed, variables or inputs. Badges: latest, merge request, branch, tag.
Limits and pitfalls
On new projects nobody may pass manual pipeline variables by default ("Minimum role to use pipeline variables: No one allowed"). That is a safety measure: prefer spec:inputs.
Exam tip
Know the pipeline sources, and that merge request pipelines only see protected variables if both branches are protected (configurable).
Pipeline editorFreeAn editor dedicated to .gitlab-ci.yml with live validation, visualisation, simulation and merged configuration.
Why it exists
Write and check a pipeline without costly trial and error in compute minutes.
In real companies
Before changing a shared include, check the merged result in Full configuration and the created jobs in Validate.
How to use it
Build, Pipeline editor. Tabs: Edit, Visualize, Validate (simulation on a branch), Full configuration (includes and extends resolved). Buttons for the CI/CD Catalog and help.
Limits and pitfalls
Validate simulates: rules that depend on runtime variables can behave differently. The /ci/lint API does the same from a script.
Predefined variablesFreeVariables GitLab gives every job: CI_COMMIT_SHA, CI_COMMIT_SHORT_SHA, CI_DEFAULT_BRANCH, CI_PIPELINE_SOURCE, CI_REGISTRY_IMAGE, CI_JOB_TOKEN, CI_PROJECT_URL…
Why it exists
Write a generic pipeline that works in any project.
In real companies
IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA works unchanged if you copy the lab into another project.
How to use it
Full list in the "Predefined CI/CD variables" documentation. To inspect them, print only the ones you need: dumping every variable can expose sensitive values.
Limits and pitfalls
Some only exist in a context: CI_MERGE_REQUEST_IID in merge request pipelines, CI_COMMIT_TAG on tags, CI_COMMIT_BRANCH not in merge request pipelines.
Exam tip
Precedence, highest first: pipeline variables (manual, trigger, schedule), project, group, instance, dotenv report variables, job-level YAML variables, global YAML variables, predefined variables.

Cache or artifact? Sort them

The cache-or-artifact sorter

For each file, pick where it belongs. You get the reasoning right away.

    CacheFreeFiles kept between pipelines to go faster (pip, npm, Maven downloads).
    Why it exists
    Do not download the same dependencies in every job.
    In real companies
    A cache key based on requirements-dev.txt: the cache only changes when the dependencies change.
    How to use it
    cache: key: files: [app/requirements-dev.txt], paths: [.cache/pip], policy: pull-push (or pull for jobs that only read).
    Limits and pitfalls
    Best effort: a cache can be missing. Never rely on it to pass a result. Never put secrets in it.
    Exam tip
    Cache = optimisation between pipelines; artifact = result passed between jobs.
    ArtifactsFreeFiles a job keeps after it ends: binaries, reports, SBOMs. Build, Artifacts lists them for the whole project.
    Why it exists
    Pass a result to later jobs, make it downloadable, and feed the interface (tests, security, coverage).
    In real companies
    A JUnit report in the Tests tab, a CycloneDX SBOM kept 30 days for audit, an image digest passed on with a dotenv report.
    How to use it
    artifacts: paths, expire_in, when (on_success, on_failure, always), reports: junit, dotenv, cyclonedx, sast, coverage_report… Later jobs receive them according to needs or dependencies.
    Limits and pitfalls
    Artifacts use storage: always set expire_in. Artifacts of the latest successful pipeline are kept by default (Settings, CI/CD, Artifacts).
    Exam tip
    Cache versus artifacts, and the fact that a dotenv report creates variables for downstream jobs.
    Pipeline schedulesFreePlanned runs of the pipeline (cron) on a branch, with their own variables.
    Why it exists
    Some work must happen even when nobody commits: a nightly scan, a weekly rebuild to pick up OS fixes, a cleanup.
    In real companies
    Every Monday, rebuild and rescan the image on main: new CVEs are published every day, even when your code does not change.
    How to use it
    Build, Pipeline schedules, New schedule (cron, time zone, branch, variables). In YAML: rules: - if: $CI_PIPELINE_SOURCE == "schedule".
    Limits and pitfalls
    The pipeline runs with the schedule owner's permissions. If that person leaves, the schedule fails: use Take ownership.
    Exam tip
    CI_PIPELINE_SOURCE is schedule. Know the other values: push, merge_request_event, web, api, trigger, pipeline, parent_pipeline.

    Hands-onFind where a value comes from

    Open any job log of your fork's pipeline. Without printing secrets, which predefined variable gives the registry address of your project, and which one gives the short commit SHA used as the image tag?

    Show the solution

    CI_REGISTRY_IMAGE (for example registry.gitlab.com/<namespace>/devsecops-gitlab-lab) and CI_COMMIT_SHORT_SHA. The lab combines them in the global variable IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA, which is why the pipeline works unchanged in your fork.

    Controlling the flow: rules, needs and reuse

    60 min, with a simulator
    By the end of this station you can
    • predict which pipeline and which jobs an event creates, using the lab's real rules;
    • turn a staged pipeline into a faster graph with needs;
    • reuse configuration with include, extends, components and child pipelines.

    Rules: the first match wins

    rules are read from top to bottom, and the first rule whose condition is true decides. If it says when: never, the job (or with workflow:rules, the whole pipeline) is dropped. If no rule matches at all, the job is not added. That single sentence explains most "why didn't my job run?" questions.

    The pipeline rules simulator

    Pick an event. The simulator evaluates the lab's real workflow:rules and job rules and tells you what GitLab would create, and why.

    used for the tag event

    workflow:rules

      Try this

      Pick "Scheduled pipeline" on main: sign and both deployments are added, because their rules only look at the branch. If a weekly rebuild should scan but not deploy, add a rule - if: $CI_PIPELINE_SOURCE == "schedule" with when: never before the others in .deploy.

      Common pitfall

      CI_COMMIT_BRANCH is not set in merge request pipelines (use CI_MERGE_REQUEST_SOURCE_BRANCH_NAME) and not set in tag pipelines (use CI_COMMIT_TAG). A rule like if: $CI_COMMIT_BRANCH == "main" is therefore false in every merge request pipeline, even for a merge request targeting main.

      rules and workflow:rulesFreeConditions checked in order; the first one that matches decides (when, allow_failure, variables).
      Why it exists
      Run a job, or create a pipeline at all, only in the right context: merge request, main, tag, changed files.
      In real companies
      Deploy only from main; build the docs only when docs/** changed; no pipeline for commits that say [skip ci].
      How to use it
      rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH, - changes: [app/**/*], - exists: [Dockerfile], when: never | manual | on_success | delayed.
      Limits and pitfalls
      rules and only/except cannot be mixed in a job. If no rule matches, the job is not added. changes compares with the previous commit or with the merge request target.
      Exam tip
      Classic question: when: manual inside rules (allow_failure false by default) versus at job level (true).

      Stages versus a graph with needs

      Without needs, a stage is a barrier: the lab's build would wait for the slow SAST job in the test stage, although the image does not depend on it. With needs: [lint, test], build starts the moment lint and both test jobs are done. GitLab calls the result a DAG (directed acyclic graph).

      stage lint    lint
      stage test    test [3.12]   test [3.13]   semgrep-sast (slow)   secret_detection
                    ---- barrier: everything above must finish ----
      stage build   build
      lint ──┐
      test ──┴──> build ──┬──> trivy_gate ──┬──> sign ──> deploy_staging ──> deploy_production (manual)
                          └──> container_scanning  │
      semgrep-sast, secret_detection run alongside and do not block build
      pages needs: [] and starts immediately
      The same pipeline shown as a dependency graph
      The same pipeline viewed by dependencies (needs).
      needs (DAG)FreeThe list of jobs a job really depends on. It starts as soon as they finish, without waiting for its whole stage.
      Why it exists
      Shorter pipelines, and control over which artifacts a job downloads.
      In real companies
      The build starts while the slow SAST job is still running; the deployment only downloads the build's dotenv.
      How to use it
      needs: [build], needs: [{job: build, artifacts: true}], needs: [] to start immediately. See it with "Job dependencies" in the pipeline graph.
      Limits and pitfalls
      A job cannot need a job that does not exist in this pipeline (because of rules, for example): use needs:optional: true.
      Exam tip
      needs ignores stage order. Without needs, a job receives the artifacts of every job in earlier stages.

      Reusing configuration

      At company scale, nobody writes a pipeline from scratch. Four mechanisms help, from the most local to the most shared:

      MechanismReusesTypical use
      extends + hidden jobsA block inside the same configurationThe lab's .python, .docker, .deploy
      includeWhole files: local, other project, URL, GitLab templateThe lab's templates.yml and security templates
      CI/CD componentsVersioned, parameterised blocks from the CI/CD Catalogcomponents/sast@3.5.0
      Child pipelinesA whole separate pipelineOne pipeline per service in a monorepo
      include, extends and hidden jobsFreeWays to reuse configuration: assemble files (include) and inherit blocks (extends).
      Why it exists
      Nobody wants to copy the same YAML into 200 projects. Centralising good practice is how platform teams scale.
      In real companies
      A versioned acme/ci-templates project included everywhere: include: project: acme/ci-templates, ref: v3, file: python.yml.
      How to use it
      include: local, project (+ref, file), remote, template, component. extends: .python deep-merges keys. YAML anchors and !reference [.job, script] reuse parts.
      Limits and pitfalls
      An unpinned remote include can change under your feet: prefer project with ref on a tag, or a versioned component.
      Exam tip
      Merge order: the job's own keys win over what it extends or includes.
      CI/CD components and the CI/CD CatalogFreeVersioned pipeline building blocks with typed parameters (spec:inputs), published in a searchable catalog.
      Why it exists
      The GitLab equivalent of GitHub actions: reuse a proven step (SAST, OpenTofu, deployment) in one line.
      In real companies
      The platform team publishes acme/components/docker-build@2.1.0; product teams include it instead of maintaining their own build.
      How to use it
      include: - component: $CI_SERVER_FQDN/components/sast/sast@3.5.0 with inputs:. Explore, CI/CD Catalog to search. The lab's Jobs/*.gitlab-ci.yml templates have component equivalents.
      Limits and pitfalls
      Pin an exact version (@3.5.0). @~latest follows the newest release.
      Exam tip
      Template (a file included as is) versus component (versioned, parameterised with inputs, published in the catalog).
      Parent-child and multi-project pipelinesFreeA job with trigger: that starts another pipeline: a child pipeline (same project, other YAML) or another project's pipeline.
      Why it exists
      Split a monorepo, generate a pipeline dynamically, chain the build of one project with the deployment of another.
      In real companies
      Monorepo: one child pipeline per service, started only if its folder changed; the application project triggers the infrastructure project.
      How to use it
      trigger: include: services/api/.gitlab-ci.yml, or trigger: project: acme/infra, branch: main, strategy: depend (wait for the result).
      Limits and pitfalls
      Not every variable is passed down automatically: use variables: or inputs on the trigger job.
      Exam tip
      strategy: depend mirrors the child's status on the trigger job.

      ChallengeRun the docs job only when docs change

      Write a job docs_check that runs a link checker, but only in merge request pipelines and only when files under docs/ changed.

      Show a hint

      Combine if and changes in the same rule item: both must be true for the rule to match.

      Show the solution
      docs_check:
        stage: lint
        image: alpine:3.22
        rules:
          - if: $CI_PIPELINE_SOURCE == "merge_request_event"
            changes: [docs/**/*]
        script:
          - echo "run your link checker here"

      No other rule matches, so in every other case the job is simply not added.

      The lab, file by file

      70 min, hands-on
      By the end of this station you can
      • explain every file of the project and every line of its pipeline;
      • write and validate a pipeline in the pipeline editor;
      • build a small, non-root container image with a multi-stage Dockerfile.

      The goal of the lab: every push builds an image, scans it, signs it without managing any key, deploys it to a short-lived environment and checks that it answers. Here is what is in the project.

      FileRole
      app/app.py, app/test_app.pyThe Flask API (/ and /healthz) and its pytest tests.
      app/requirements*.txtDependencies. gunicorn==22.0.0 is deliberately old so the scanners have something to report.
      DockerfileMulti-stage build, non-root user (uid 10001), OCI label pointing to the project.
      .gitlab-ci.ymlThe pipeline.
      .gitlab/ci/templates.ymlReusable hidden jobs, included with include: local.
      .gitlab/CODEOWNERSOwners per path, in [CI] and [Application] sections (enforced in Premium).
      .gitlab/issue_templates/, .gitlab/merge_request_templates/Templates offered when creating an issue or a merge request.
      SECURITY.md, AGENTS.mdHow to report a vulnerability; instructions for AI agents working in the repository.
      docs/This course, published by the pages job (station 13).

      Step 1: the application and its tests

      """Mini API du labo DevSecOps.
      
      Modifie MESSAGE ou VERSION, pousse sur main : le pipeline
      build -> scan -> signe -> deploie automatiquement.
      """
      
      import os
      
      from flask import Flask, jsonify
      
      VERSION = os.environ.get("APP_VERSION", "1.0.0")
      MESSAGE = "Hello from GitLab CI DevSecOps lab!"
      
      app = Flask(__name__)
      
      
      @app.get("/")
      def index():
          return jsonify(message=MESSAGE, version=VERSION)
      
      
      @app.get("/healthz")
      def healthz():
          return jsonify(status="ok")
      
      
      if __name__ == "__main__":
          app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "8080")))
      from app import app
      
      
      def test_index():
          client = app.test_client()
          res = client.get("/")
          assert res.status_code == 200
          assert "message" in res.get_json()
      
      
      def test_healthz():
          client = app.test_client()
          res = client.get("/healthz")
          assert res.status_code == 200
          assert res.get_json() == {"status": "ok"}
      flask==3.1.3
      # Deliberately old version (known CVE): the scanners report it -> exercise in the course
      gunicorn==22.0.0

      /healthz is a health check endpoint: the deployment calls it to decide whether the new version is alive. The version comes from the APP_VERSION environment variable, which the pipeline sets to the commit SHA, so you can always tell which build is answering.

      Step 2: the Dockerfile

      # syntax=docker/dockerfile:1
      # ---------- Stage 1: build the dependencies ----------
      FROM python:3.13-slim AS build1
      WORKDIR /src
      COPY app/requirements.txt .
      RUN pip install --no-cache-dir --prefix=/install -r requirements.txt2
      
      # ---------- Stage 2: minimal final image ----------
      FROM python:3.13-slim
      LABEL org.opencontainers.image.source="https://gitlab.com/georges.momo.n8n/devsecops-gitlab-lab" \
            org.opencontainers.image.description="GitLab CI DevSecOps lab" \
            org.opencontainers.image.licenses="MIT"
      
      # Non-root user (security best practice, checked by the scanners)
      RUN useradd --create-home --uid 10001 appuser3
      WORKDIR /app
      COPY --from=build /install /usr/local4
      COPY app/app.py .
      
      ARG APP_VERSION=dev
      ENV APP_VERSION=${APP_VERSION} PORT=8080 PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
      USER 100015
      EXPOSE 8080
      HEALTHCHECK --interval=30s --timeout=3s CMD python -c "import urllib.request,sys; urllib.request.urlopen('http://127.0.0.1:8080/healthz'); sys.exit(0)"6
      CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "2", "app:app"]7
      1. A multi-stage build: the first stage installs dependencies, the second copies only the result. Build tools never reach the final image.
      2. Packages are installed into /install so they can be copied in one line.
      3. A dedicated user with a fixed uid. Scanners and Kubernetes policies flag images that run as root.
      4. Only the installed packages and app.py reach the final image; tests and dev tools are excluded (see also .dockerignore).
      5. From here on, the container runs as uid 10001: a compromised app cannot modify system files.
      6. Docker itself can check the container's health with the same endpoint the pipeline uses.
      7. A production web server (gunicorn) instead of Flask's development server.

      Step 3: write and check the pipeline in the editor

      1. Create the project: +, New project/repository, Create blank project. Untick "Initialize repository with a README" if you are pushing existing code.
      2. Open Build, Pipeline editor. It validates the syntax as you type and offers Visualize, Validate and Full configuration.
      3. Commit from the field at the bottom, either straight to main or to a new branch with a merge request. The pipeline starts immediately.
      Pipeline editor showing .gitlab-ci.yml with validation status and tabs
      Build, Pipeline editor, open on .gitlab-ci.yml.
      Full configuration tab showing the merged YAML
      Full configuration: what GitLab really runs.

      Step 4: read .gitlab-ci.yml block by block

      # GitLab DevSecOps pipeline: lint -> tests -> SAST/secrets -> build -> scan -> SBOM -> signature -> deployment -> verification
      # GitLab twin of the GitHub Actions lab (georgesmomo/devsecops-actions-lab).
      # Interactive course: see docs/index.html (published by the "pages" job at the end of this file).
      
      workflow:1
        rules:
          - if: $CI_PIPELINE_SOURCE == "merge_request_event"   # merge request pipeline
          - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS    # avoids the duplicate branch + MR pipeline
            when: never
          - if: $CI_COMMIT_BRANCH                                # push to a branch
          - if: $CI_COMMIT_TAG                                   # tag v1.2.3
        auto_cancel:
          on_new_commit: interruptible                           # cancels interruptible jobs of an older commit
      
      stages: [lint, test, build, scan, sign, deploy]2
      
      variables:
        IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA3
        PIP_CACHE_DIR: $CI_PROJECT_DIR/.cache/pip
      
      include:4
        - local: .gitlab/ci/templates.yml                    # hidden jobs (.python, .docker, .deploy) reused with extends
        - template: Jobs/SAST.gitlab-ci.yml                  # SAST (Semgrep) - Free: JSON report
        - template: Jobs/Secret-Detection.gitlab-ci.yml      # finds secrets committed to the history
        - template: Jobs/Container-Scanning.gitlab-ci.yml    # scans the image (Trivy)
      
      # ---------- Quality ----------
      lint:
        extends: .python
        stage: lint
        script:
          - ruff check app
          - ruff format --check app
      
      test:
        extends: .python
        stage: test
        image: python:${PY}-slim
        parallel:5
          matrix:
            - PY: ["3.12", "3.13"]                           # 2 generated jobs
        script:
          - cd app && pytest --junitxml=report.xml
        artifacts:
          when: always
          expire_in: 1 week
          reports:
            junit: app/report.xml                            # pipeline Tests tab6
      
      # ---------- Image ----------
      build:
        extends: .docker
        stage: build
        needs: [lint, test]
        script:
          - docker build --build-arg APP_VERSION="$CI_COMMIT_SHORT_SHA" --label org.opencontainers.image.revision="$CI_COMMIT_SHA" -t "$IMAGE" .
          - docker push "$IMAGE"
          - DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE" | cut -d@ -f2)
          - echo "IMAGE_DIGEST=$DIGEST" >> build.env
          - echo "Image published $CI_REGISTRY_IMAGE@$DIGEST"
        artifacts:
          reports:
            dotenv: build.env                                # IMAGE_DIGEST becomes a variable in later jobs7
      
      container_scanning:                                    # overrides the job provided by the template8
        stage: scan
        needs: [build]
        variables:
          CS_IMAGE: $IMAGE
      
      trivy_gate:
        stage: scan
        needs: [build]
        image:
          name: aquasec/trivy:0.76.0
          entrypoint: [""]
        variables:
          TRIVY_USERNAME: $CI_REGISTRY_USER
          TRIVY_PASSWORD: $CI_REGISTRY_PASSWORD
        script:
          - trivy image --exit-code 1 --severity CRITICAL --ignore-unfixed "$IMAGE"          # security gate9
          - trivy image --format cyclonedx --output sbom.cdx.json "$IMAGE"                   # SBOM
        artifacts:
          paths: [sbom.cdx.json]
          reports:
            cyclonedx: sbom.cdx.json10
          expire_in: 30 days
      
      # ---------- Supply chain ----------
      sign:
        stage: sign
        image: alpine:3.22
        needs: [build, trivy_gate]
        rules:
          - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
          - if: $CI_COMMIT_TAG
        id_tokens:11
          SIGSTORE_ID_TOKEN:                                 # GitLab OIDC token, read automatically by cosign
            aud: sigstore
        before_script:
          - apk add --no-cache cosign
          - cosign login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
        script:
          - cosign sign --yes "$CI_REGISTRY_IMAGE@$IMAGE_DIGEST"
      
      # ---------- Deployment ----------
      deploy_staging:
        extends: .deploy
        needs: [build, sign]
        environment:
          name: staging
          deployment_tier: staging
          url: $CI_PROJECT_URL/container_registry
        resource_group: staging                              # only one staging deployment at a time12
      
      deploy_production:
        extends: .deploy
        needs: [build, sign, deploy_staging]
        when: manual                                         # Play button: human approval13
        environment:
          name: production
          deployment_tier: production
          url: $CI_PROJECT_URL/container_registry
        resource_group: production
      
      # ---------- Course website ----------
      # GitLab Pages publishes whatever a job named "pages" leaves in public/ as an artifact.
      # needs: [] starts it at once, without waiting for the other stages: the course does not depend on the image.
      pages:14
        stage: deploy
        image: busybox:1.37
        needs: []
        script:
          - rm -rf public && cp -r docs public               # the course lives in docs/, Pages serves public/
        artifacts:
          paths: [public]
        rules:
          - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH      # publish only from the default branch
      1. workflow: one pipeline per event, never a duplicate branch plus merge request pipeline; jobs marked interruptible of an older commit are cancelled when a new commit arrives.
      2. stages: the six columns, in order.
      3. IMAGE: project registry plus short SHA, so every commit gets its own tag. $CI_REGISTRY_IMAGE is predefined.
      4. include: one local file and three official templates, which add the jobs semgrep-sast, secret_detection and container_scanning.
      5. parallel:matrix creates test: [3.12] and test: [3.13] from one definition.
      6. The JUnit report feeds the pipeline's Tests tab. when: always keeps it even when tests fail, which is exactly when you need it.
      7. build pushes the image, then reads its digest (the immutable fingerprint) into build.env. The dotenv report turns IMAGE_DIGEST into a variable for every job that needs: [build].
      8. Overriding a template job: same name as the template's job, so these keys are merged into it (new stage, needs and image to scan).
      9. The security gate: exit code 1 if a CRITICAL vulnerability that has a fix exists. This job really fails the pipeline.
      10. The SBOM in CycloneDX format, published as a report and kept 30 days.
      11. id_tokens gives the job an OIDC token with audience sigstore. cosign reads it from SIGSTORE_ID_TOKEN and signs without a stored key (station 8).
      12. resource_group: two pipelines can never deploy staging at the same time.
      13. when: manual: production waits for a human to press Play. At job level this also means allow_failure: true, so the pipeline shows Passed while production waits.
      14. pages publishes docs/ as this course. needs: [] lets it start at once (station 13).

      Step 5: the shared hidden jobs

      # Hidden jobs (name starting with a dot): never run on their own, reused with extends.
      
      .python:
        image: python:3.13-slim
        interruptible: true1
        cache:
          key:
            files: [app/requirements-dev.txt]               # cache key = hash of this file2
          paths: [.cache/pip]
        before_script:
          - pip install -r app/requirements-dev.txt
      
      .docker:
        image: docker:28
        services:
          - docker:28-dind                                    # Docker-in-Docker3
        variables:
          DOCKER_TLS_CERTDIR: "/certs"
        before_script:
          - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"4
      
      .deploy:
        extends: .docker
        stage: deploy
        rules:
          - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
        script:
          - apk add --no-cache cosign curl
          # Zero trust: only run the image signed by THIS pipeline of THIS project
          - cosign verify "$CI_REGISTRY_IMAGE@$IMAGE_DIGEST" --certificate-identity "${CI_PROJECT_URL}//.gitlab-ci.yml@refs/heads/${CI_COMMIT_REF_NAME}" --certificate-oidc-issuer "${CI_SERVER_URL}" > /dev/null5
          - echo "Signature verified for $CI_REGISTRY_IMAGE@$IMAGE_DIGEST"
          # Ephemeral environment: the hardened container runs inside the Docker service
          - docker run -d --name app -p 8080:8080 --read-only --tmpfs /tmp --cap-drop ALL "$CI_REGISTRY_IMAGE@$IMAGE_DIGEST"6
          - for i in $(seq 1 20); do curl -fsS http://docker:8080/healthz && break; sleep 2; done
          - curl -fsS http://docker:8080/healthz | grep -q ok7
          - curl -fsS http://docker:8080/
          - echo "Smoke test OK on $CI_ENVIRONMENT_NAME"
      1. Python jobs can be cancelled safely when a newer commit arrives.
      2. The cache key is a hash of the dependency file: the cache is reused until the dependencies change.
      3. Docker-in-Docker: a side container that provides a Docker daemon at the hostname docker.
      4. Login with the job's own registry credentials, passed on stdin so the password never appears in the process list.
      5. The DevSecOps heart. Before running anything, cosign verify demands that the signature was produced by this .gitlab-ci.yml, of this project, on this branch, with GitLab as the issuer. An image pushed by hand, or signed by another project, is refused.
      6. An ephemeral environment: the image runs inside the Docker service, read-only, with every Linux capability dropped.
      7. The smoke test: retry /healthz for up to 40 seconds, require ok, then call /. When the job ends, everything disappears.
      In real companies

      These hidden jobs would live in a central project (include: project: acme/ci-templates, ref: v3) or become versioned CI/CD components. For a real cluster, the deploy job would use the GitLab agent for Kubernetes (station 11) instead of a throwaway container; the cosign verify step stays exactly the same, or moves into a cluster admission policy.

      Docker-in-Docker (services)FreeA side container docker:dind that gives the job a Docker daemon, reachable at the hostname docker.
      Why it exists
      Build and run images inside a job on shared runners.
      In real companies
      The lab's build job; the smoke test that starts the image and calls it on http://docker:8080.
      How to use it
      image: docker:28, services: [docker:28-dind], DOCKER_TLS_CERTDIR: "/certs". Published ports live on the docker host, not on localhost.
      Limits and pitfalls
      Needs a privileged runner (GitLab.com provides one). Rootless alternatives: Buildah or BuildKit (Kaniko is no longer maintained).

      Hands-onBreak the gate on purpose

      In a branch of your fork, change the base image of the final stage to an old Python image with known critical vulnerabilities (for example an end-of-life tag), push, and open a merge request. Which job fails, and what happens to the jobs after it?

      Show a hint

      Look at which jobs need trivy_gate. In a merge request pipeline, sign and deploy are not even added (their rules want the default branch), so watch the gate itself.

      Show the solution

      trivy_gate fails with exit code 1 if a critical, fixable vulnerability is found, and the pipeline turns red. On main, sign needs trivy_gate, so it never runs; without a signature, the deploy jobs cannot verify anything. container_scanning stays green: GitLab's scanner jobs report, they do not block. Revert the change afterwards.

      Supply chain: digests, SBOMs and keyless signatures

      40 min, the trust chain
      By the end of this station you can
      • explain why the lab deploys by digest and never by tag;
      • follow the trust chain from the sign job to the deploy job;
      • configure id_tokens for a cloud or for Sigstore safely.

      Scanning tells you an image looked fine when it was built. It does not tell you that the image you are about to run is that image. A tag like :latest or even :fbdd9f27 is a label someone can move to another image. A digest (sha256:…) is computed from the content itself: change one byte and the digest changes. That is why the lab passes IMAGE_DIGEST from job to job and signs and deploys image@digest.

      Think of it like this

      A tag is the name on a parcel; anyone can write a new name on a different box. A digest is the parcel's fingerprint. A keyless signing certificate is a tamper-proof seal stamped by a notary (Fulcio) who first checked the sender's ID card (GitLab's OIDC token), and every stamp is written in a public ledger (Rekor).

      Ride the trust chain

      No private key to store, rotate or leak: the pipeline's identity is the key. It is the same principle you use to let a pipeline into AWS, Google Cloud, Azure or Vault without a stored password.

      id_tokens (OIDC)FreeJWT tokens signed by GitLab, requested by a job, that describe its identity (project, branch, pipeline, environment).
      Why it exists
      Log in to a cloud, to Vault or to Sigstore without storing any long-lived secret.
      In real companies
      An AWS role that only jobs on main of project X can assume; keyless cosign image signing.
      How to use it
      id_tokens: MY_TOKEN: aud: https://target. The JWT is in $MY_TOKEN. On the cloud side, trust the issuer https://gitlab.com and filter on the sub claim (project_path:…:ref_type:branch:ref:main).
      Limits and pitfalls
      A filter that is too broad (all of gitlab.com) would let any project in. Always filter on project_path and ref.
      Exam tip
      It replaces the removed CI_JOB_JWT. aud is required.
      Container registryFreeThe project's Docker image registry, at registry.gitlab.com/<namespace>/<project>.
      Why it exists
      Store the images built by the pipeline close to the code and its permissions.
      In real companies
      One image per commit (tag = short SHA) plus its cosign signature stored next to it (tag sha256-….sig).
      How to use it
      Deploy, Container registry. In a job, log in with CI_REGISTRY_USER and CI_REGISTRY_PASSWORD (the job's token). Cleanup policies: Settings, Packages and registries.
      Limits and pitfalls
      Without a cleanup policy, storage grows forever. Tags can be moved: deploy by digest, like the lab.
      Container registry with the image tag and its cosign signature tag
      Deploy, Container registry.
      Common pitfall

      Trusting https://gitlab.com as an issuer without filtering the sub claim. Every project on GitLab.com can get a token from that issuer. Always pin the project path and the ref (and, for clouds, the environment).

      ChallengeVerify a signature yourself

      With cosign installed on your machine, verify the image of your fork's latest main pipeline. You need the digest (from the build job log or the registry page) and the same identity the deploy job uses.

      Show the solution
      $ cosign verify registry.gitlab.com/<namespace>/devsecops-gitlab-lab@sha256:<digest> \
          --certificate-identity "https://gitlab.com/<namespace>/devsecops-gitlab-lab//.gitlab-ci.yml@refs/heads/main" \
          --certificate-oidc-issuer "https://gitlab.com"

      If the project is private, log in to the registry first (cosign login or docker login with a token that has read_registry). Try changing the identity to another branch: verification fails, which is the point.

      Reading and debugging a pipeline

      40 min, observe and fix
      By the end of this station you can
      • read the pipeline list, the stage graph, the dependency graph and the Tests tab;
      • find the useful part of a job log quickly;
      • follow a deployment in Environments and understand review apps.

      The pipeline list

      List of pipelines with status, source badges, stages and actions
      Build, Pipelines.

      The graph, grouped by stage

      Pipeline graph grouped by stage with 11 jobs
      A real run of the lab: pipeline #2960878508, grouped by stage. It passed in 2 min 42 s with 11 jobs.

      Notice the test column: the two matrix jobs sit next to semgrep-sast and secret_detection, which the templates put in the test stage. Notice also that the pipeline is green while deploy_production still waits: that is the job-level when: manual at work.

      The Tests tab

      Tests tab with a summary and one row per matrix job
      The Tests tab, fed by artifacts:reports:junit.

      A job log

      Log of the deploy_staging job ending with Job succeeded
      The log of the deploy_staging job.
      Jobs and logsFreeA job page: live log, duration, runner, commit, pipeline, related jobs, and Retry, Cancel, Erase buttons.
      Why it exists
      Understand a failure and rerun precisely what you need.
      In real companies
      The smoke test of deploy_staging prints {"status":"ok"} then the API message: proof that the signed image runs.
      How to use it
      Search in the log, collapsible sections (section_start/section_end), timestamps. Retry reruns just this job with the same configuration.
      Limits and pitfalls
      Masked secrets show as [MASKED], but a transformed value (base64, split) is not masked any more: never print a secret.

      Environments

      Environments page with production waiting and staging deployed
      Operate, Environments.
      EnvironmentsFree (protected environments: Premium)Operate, Environments: each deployment target (staging, production…) with its latest deployment, history and actions.
      Why it exists
      Know what runs where, redeploy an earlier version, start or stop a deployment.
      In real companies
      production shows Waiting while the manual job waits for a click; staging shows Success, latest deployed.
      How to use it
      environment: name, url, deployment_tier, action (start, stop, prepare, verify, access), on_stop, auto_stop_in. Rollback or Re-deploy from the history.
      Limits and pitfalls
      Protected environments (who may deploy) and deployment approvals are Premium. In Free, protection comes from protected branches and manual jobs.
      Exam tip
      Static environment versus dynamic one (review/$CI_COMMIT_REF_SLUG).
      Review appsFreeDynamic environments created per merge request, with a preview URL and automatic stop.
      Why it exists
      Try a merge request in real conditions before merging, and run DAST against it.
      In real companies
      Each merge request deploys review/my-branch into a Kubernetes namespace that is destroyed when the merge request closes.
      How to use it
      environment: name: review/$CI_COMMIT_REF_SLUG, url, on_stop: stop_review, auto_stop_in: 1 week. The "Enable review apps" button generates an example.
      Limits and pitfalls
      They cost resources: always set auto_stop_in and a reliable stop job.
      Debugging fast

      A red job: read the end of the log first (the error), then the beginning (image, runner, variables). Retry reruns that job alone. For a syntax problem, use the pipeline editor's Validate tab, which simulates the whole configuration with includes. To understand rules, temporarily add - echo "$CI_PIPELINE_SOURCE $CI_COMMIT_BRANCH" to the script.

      SymptomMost likely causeWhere to look
      No pipeline at all after a pushworkflow:rules matched nothing or when: never (for example a branch with an open merge request)The merge request's Pipelines tab; Validate in the editor
      A job is missing from the pipelineNo rule matched (often CI_COMMIT_BRANCH in a merge request pipeline)The rules simulator in station 6
      A variable is emptyProtected variable on an unprotected branch, or wrong environment scopeSettings, CI/CD, Variables
      Job stuck in PendingNo runner with matching tags is availableSettings, CI/CD, Runners
      IMAGE_DIGEST empty in a later jobThat job does not need build, so it did not get the dotenv reportThe job's needs
      Cannot connect to the Docker daemonMissing docker:dind service or DOCKER_TLS_CERTDIRThe .docker hidden job

      Hands-onRetry versus run again

      In your fork, open the latest pipeline on main. Retry only deploy_staging. Then start production with the Play button. Where can you see that production is now deployed, and which commit it runs?

      Show the solution

      Operate, Environments: production changes from Waiting to Success and shows the deployed commit SHA. The job log of deploy_production ends with the same smoke test as staging. Retry reused the same pipeline and the same digest: nothing was rebuilt.

      Security: scanners and what they really do

      45 min, the Sec in DevSecOps
      By the end of this station you can
      • say what each GitLab scanner finds and which tier it needs;
      • explain why GitLab's scanners do not block the pipeline in Free, and how the lab adds a real gate;
      • react correctly to a leaked secret.

      Secure, Security configuration lists the scanners, their state on the default branch and the tier they need. A scanner shows as Enabled because the latest pipeline on main produced a valid report for it.

      Security configuration page listing scanners and their status
      Secure, Security configuration.
      ScannerFindsWhen it runsTier
      SAST (Semgrep)Flaws in your source code (injection, weak crypto…)Every pipeline, alongside the testsFree JSON report; Ultimate Advanced SAST, merge request widget
      Secret detectionKeys, tokens, passwords in commitsEvery pipeline; before the push with push protectionFree pipeline; Ultimate push protection
      Container scanning (Trivy)CVEs in the image's packagesAfter the buildFree
      Dependency scanningCVEs in declared dependencies, SBOMEvery pipelineUltimate
      IaC scanningMisconfigurations in Terraform, Kubernetes, DockerfileEvery pipelineFree
      DAST, API fuzzingFlaws in the running applicationAfter a deployment (review app)Ultimate
      In Free, reports block nothing

      GitLab's scanner jobs are non-blocking by design: they publish a report and stay green. That is why the lab adds trivy_gate, which really fails. In Ultimate, blocking is usually done with merge request approval policies instead: a critical finding requires the security team's approval before merge.

      Think of it like this

      SAST reads your recipe looking for dangerous steps. Dependency and container scanning check the ingredients' recall notices. Secret detection looks for the house key you left taped to the recipe. DAST tastes the finished dish.

      SASTFree (Advanced SAST and MR widget: Ultimate)Static analysis of your source code, based on Semgrep and GitLab rules.
      Why it exists
      Find flaws (SQL injection, unsafe deserialisation, weak crypto) before the code runs.
      In real companies
      Runs on every merge request in parallel with the tests; the security team triages results.
      How to use it
      include: template: Jobs/SAST.gitlab-ci.yml (or the components/sast component). The semgrep-sast job writes gl-sast-report.json.
      Limits and pitfalls
      Free: a JSON report artifact only. Ultimate: results in the merge request, vulnerability report, cross-file Advanced SAST.
      Secret detectionFree (push protection: Ultimate)A search for secrets (cloud keys, GitLab tokens, private keys…) in commits.
      Why it exists
      Catch a leaked secret as early as possible so it can be revoked.
      In real companies
      Someone commits a .env file: the secret_detection job reports it, the team revokes the key.
      How to use it
      include: template: Jobs/Secret-Detection.gitlab-ci.yml. Ultimate adds secret push protection, which blocks the push itself.
      Limits and pitfalls
      A detected secret is already compromised: revoke it, do not just delete it from the code (git history keeps it).
      Container scanningFreeA scan of the packages inside the Docker image (OS and language packages) with Trivy.
      Why it exists
      Know the CVEs of what you ship, not only of the code you wrote.
      In real companies
      The lab scans $IMAGE right after the build; a scheduled rebuild catches newly published CVEs.
      How to use it
      include: template: Jobs/Container-Scanning.gitlab-ci.yml and CS_IMAGE. To block, add a Trivy job with --exit-code 1 (trivy_gate).
      Limits and pitfalls
      Many CVEs have no fix yet: --ignore-unfixed avoids blocking on what you cannot fix.
      Dependency scanningUltimateAnalysis of declared dependencies (requirements, package-lock, pom) and generation of an SBOM.
      Why it exists
      Find vulnerable libraries and know what your software is made of.
      In real companies
      An alert on gunicorn==22.0.0 in a merge request; a list of licences for the legal team.
      How to use it
      The components/dependency-scanning component or Auto DevOps. Results in Secure, Dependency list.
      Limits and pitfalls
      Ultimate. In Free the lab relies on Trivy (on the image) and you can add Renovate for updates.
      Vulnerability reportUltimateThe consolidated list of vulnerabilities found on the default branch, each with a status (detected, confirmed, dismissed, resolved).
      Why it exists
      Triage, assign and follow flaws over time, per project or per group.
      In real companies
      Every week the security team reviews new critical findings across the group and opens issues.
      How to use it
      Secure, Vulnerability report; Vulnerability management in Security configuration.
      Limits and pitfalls
      Ultimate. In Free, read the jobs' JSON reports.
      Security policiesUltimateCentral rules: scan execution policies (force scans), pipeline execution policies (force jobs), merge request approval policies (require approval when a flaw appears).
      Why it exists
      Make sure every project in a group scans and follows rules, even if its own .gitlab-ci.yml does not.
      In real companies
      Any merge request that introduces a critical vulnerability needs the security team's approval; secret detection runs everywhere.
      How to use it
      Secure, Policies, stored in a dedicated policy project.
      Limits and pitfalls
      Ultimate.
      Audit eventsPremium (streaming: Ultimate)A log of who did what: role changes, token creation, changes to variables or protected branches.
      Why it exists
      Traceability for compliance and for investigations after an incident.
      In real companies
      Show an auditor who removed the protection on main and when; stream the events to a SIEM.
      How to use it
      Secure, Audit events, filter by date and author.
      Limits and pitfalls
      The menu entry is visible in Free but the feature is limited there.

      Hands-onRead a report without Ultimate

      Open the latest main pipeline of your fork, open the semgrep-sast job and download its artifact. Then open trivy_gate's artifact sbom.cdx.json and find the gunicorn component.

      Show a hint

      Job page, right column, Job artifacts, Download or Browse. The SAST report is gl-sast-report.json; each finding has a severity, a location and identifiers.

      ChallengeA secret was committed. What now?

      A teammate committed a cloud access key in config.py and the secret_detection report caught it. Put the response steps in order.

      Show the solution
      1. Revoke the key at the provider immediately. It is compromised the moment it was pushed.
      2. Check the provider's logs for use of the key since the push.
      3. Store the new key as a masked, protected CI/CD variable (or better, replace it with OIDC).
      4. Remove it from the code in a merge request. Rewriting history is optional and does not undo the exposure.
      5. Prevent a repeat: secret push protection (Ultimate) or a pre-commit hook.

      The settings that matter for DevSecOps

      55 min, hardening
      By the end of this station you can
      • configure CI/CD variables with the right visibility and protection;
      • choose and secure runners;
      • harden main with protected branches and merge checks, and connect a Kubernetes cluster safely.

      Settings, CI/CD

      Settings CI/CD page with collapsible sections
      Settings, CI/CD.
      Auto DevOpsFreeA complete predefined pipeline (build, tests, scans, Kubernetes deployment) used when a project has no .gitlab-ci.yml.
      Why it exists
      Start quickly without writing a pipeline.
      In real companies
      A prototype or a small team without CI skills; a reference to see how GitLab chains its scanners.
      How to use it
      Settings, CI/CD, Auto DevOps. Customisable with variables.
      Limits and pitfalls
      A black box: as soon as needs get specific, an explicit .gitlab-ci.yml (like the lab) is easier to control.
      Pipeline trigger tokensFreeTokens that start a pipeline through an API call (POST /projects/:id/trigger/pipeline).
      Why it exists
      Let an external system (another CI, a release tool) start the pipeline.
      In real companies
      An in-house release tool starts the deployment pipeline with variables.
      How to use it
      Settings, CI/CD, Pipeline trigger tokens. In YAML, CI_PIPELINE_SOURCE is trigger.
      Limits and pitfalls
      The token acts with its creator's permissions: rotate it, and prefer trigger: in CI for internal chains.
      Deploy freezesFreePeriods, written as cron expressions, during which deployments are not allowed.
      Why it exists
      Avoid releasing during holidays or a big commercial event.
      In real companies
      A freeze from 20 December to 2 January.
      How to use it
      Settings, CI/CD, Deploy freezes. The deployment job must check $CI_DEPLOY_FREEZE (rules: - if: $CI_DEPLOY_FREEZE, when: never).
      Limits and pitfalls
      Without that rule in the YAML, the freeze blocks nothing.
      CI_JOB_TOKEN and job token permissionsFreeThe automatic token of every job, valid while the job runs, with the permissions of the user who started the pipeline, limited to some APIs.
      Why it exists
      Authenticate to the registry, to packages, clone another project, with no secret to manage.
      In real companies
      The lab pushes its image with it; an application project downloads a package from the library project.
      How to use it
      Settings, CI/CD, Job token permissions: the list of groups and projects allowed to use THEIR token to access THIS project (an inbound allowlist).
      Limits and pitfalls
      It inherits the user's rights: a Maintainer running a pipeline gives the token more power. Keep the allowlist minimal.
      Exam tip
      Unlike GITHUB_TOKEN, its permissions are not set per job in the YAML.
      Secure filesFreeStorage for sensitive files (Android keystores, Apple profiles, certificates) that jobs can download.
      Why it exists
      Sign mobile apps without putting a binary secret into a variable.
      In real companies
      An iOS pipeline that fetches its provisioning profile at build time.
      How to use it
      Settings, CI/CD, Secure files, then the download-secure-files tool in the job.
      Limits and pitfalls
      Limits on size and number per project; every job of the project can read them.

      CI/CD variables

      Add variable form with type, environments, visibility and flags
      The full Add variable form.
      CI/CD variablesFreeKey/value pairs injected into jobs, defined on a project, group or instance, with a type, an environment scope, a visibility and flags.
      Why it exists
      Give pipelines configuration and secrets without writing them in the repository.
      In real companies
      An API key masked and protected (only on main and tags); a KUBECONFIG of type File scoped to production.
      How to use it
      Settings, CI/CD, Variables, Add variable. Type Variable or File; Environments (scope); Visibility: Visible, Masked, Masked and hidden; Flags: Protect variable, Expand variable reference.
      Limits and pitfalls
      Masked needs a single-line value of at least 8 characters. Masked and hidden can never be read again. A protected variable is absent from pipelines of unprotected branches: a very common cause of "my variable is empty".
      Exam tip
      Protected = only on protected branches and tags. Know the precedence order.

      Variable precedence: who wins?

      The same variable name can be defined in many places. GitLab applies a precedence order, from strongest to weakest. Use the arrows to put the sources in the right order, then check.

      Put the variable sources in order

      Top = wins. Move items with the arrow buttons.

        Runners

        Runners settings with project, group and instance tabs
        Settings, CI/CD, Runners.
        RunnersFreeThe agents that execute jobs: instance runners (provided by GitLab.com), group runners, project runners.
        Why it exists
        Choose where and how jobs run: GitLab's machines, or yours for internal network access, GPUs or compliance.
        In real companies
        Instance runners for everyday CI; a project runner inside the internal network, tagged deploy-prod and protected, for deployments.
        How to use it
        Settings, CI/CD, Runners, Create project runner (token starting with glrt-), install gitlab-runner and register it. In YAML: tags: [deploy-prod].
        Limits and pitfalls
        A shell runner shared between projects can leak data from one job to another: prefer the docker or kubernetes executor, and protect deployment runners.
        Exam tip
        Executors (docker, shell, kubernetes, docker-autoscaler). A job without tags can go to any runner that accepts untagged jobs.

        Protected branches (Settings, Repository)

        Protected branches settings for main
        Settings, Repository, Protected branches.
        Protected branches and tagsFree (code owner approval: Premium)Rules on a branch or pattern: who can merge, who can push, whether force push is allowed.
        Why it exists
        Prevent direct pushes and history rewrites on main and release branches.
        In real companies
        main: Allowed to push and merge = No one, Allowed to merge = Developers + Maintainers, no force push. Every change goes through a merge request.
        How to use it
        Settings, Repository, Protected branches (or Branch rules). Protected tags for v*. The default branch is protected when the project is created.
        Limits and pitfalls
        By default Maintainers can still push directly to main: tighten it to No one if you want merge requests only.
        Exam tip
        Protected variables and protected runners only serve protected branches.
        Deploy tokens and deploy keysFreeDedicated access for one use: a deploy token for registry, packages or repository; a deploy key is an SSH key that can clone.
        Why it exists
        Give a server or a cluster exactly the access it needs, without a human account.
        In real companies
        A Kubernetes cluster pulls images from the registry with a read_registry deploy token.
        How to use it
        Settings, Repository, Deploy tokens / Deploy keys. A deploy token named gitlab-deploy-token is exposed to CI automatically.
        Limits and pitfalls
        Deploy keys do not expire by default: keep an inventory and rotate them.

        Merge checks (Settings, Merge requests)

        Merge checks settings with checkboxes
        Settings, Merge requests, Merge checks.
        Merge checksFreeMandatory conditions to merge: pipeline succeeded, threads resolved, and more depending on your tier.
        Why it exists
        Stop a red merge request, or one with open review comments, from being merged.
        In real companies
        "Pipelines must succeed" and "All threads must be resolved" on every project.
        How to use it
        Settings, Merge requests, Merge checks. Same page: merge method (merge commit, semi-linear, fast-forward) and squash policy.
        Limits and pitfalls
        They are not ticked by default: turn them on. "Skipped pipelines are considered successful" opens a gap if rules skip the whole pipeline.
        Merged results and merge trainsPremiumMerged results: test the result of merging the merge request into its target. Merge train: a queue of merge requests, each tested together with the ones ahead of it.
        Why it exists
        Keep main green even when many merge requests land on the same day.
        In real companies
        Thirty developers on a monorepo: without a merge train, two merge requests that are green separately break main together.
        How to use it
        Settings, Merge requests, Merge options. Pipelines must run on merge_request_event.
        Limits and pitfalls
        Premium.

        The GitLab agent for Kubernetes (Operate, Kubernetes clusters)

        Kubernetes clusters page with Connect a cluster button
        Operate, Kubernetes clusters.
        GitLab agent for KubernetesFreeA component (agentk) installed in your cluster that opens an outbound connection to GitLab. The cluster API is never exposed.
        Why it exists
        Deploy from CI to a private cluster, do GitOps (Flux) and see cluster resources in GitLab.
        In real companies
        The production cluster sits behind a firewall; jobs on main of the application project get a kubectl context through ci_access.
        How to use it
        Operate, Kubernetes clusters, Connect a cluster, install with Helm. Config in .gitlab/agents/<name>/config.yaml (ci_access: projects/groups). In the job: kubectl config use-context <path>:<agent>.
        Limits and pitfalls
        The old certificate-based integration is deprecated: do not use it. Limit ci_access to the right projects and environments.
        Kubernetes in depth

        The third course of the series, Kubernetes + Helm, teaches the cluster side: objects, debugging, administration and Helm charts.

        Hardening checklist for your fork

        These actions touch security and human decisions, so they are worth doing by hand once. The boxes remember your progress in this browser.

        • Forbid direct pushes to main: Settings, Repository, Protected branches, main, "Allowed to push and merge" = No one.
        • Turn on merge checks: Settings, Merge requests, tick "Pipelines must succeed" and "All threads must be resolved".
        • Add a registry cleanup policy: Settings, Packages and registries, Cleanup policies (for example keep the 10 latest tags, remove tags older than 30 days).
        • Protect and create a version tag: protect v*, then Code, Tags, New tag v1.0.0. The tag pipeline builds, scans and signs; deployments stay reserved for main.
        • Schedule a weekly rebuild: Build, Pipeline schedules, cron 0 6 * * 1 on main, to catch newly published CVEs.
        • Deploy production by hand: press Play on deploy_production and check Operate, Environments.

        Integrations, tokens and machine accounts

        35 min, who is acting?
        By the end of this station you can
        • pick the right identity for an automation: job token, project token, service account, OAuth app;
        • connect GitLab to other tools with integrations and signed webhooks;
        • explain what replaces GitHub Apps in GitLab.

        GitLab has no exact equivalent of GitHub Apps (an installable bot identity with fine permissions and short-lived tokens). It spreads that need over several building blocks. The right choice depends on who is acting and for how long.

        IdentityLivesUse it for
        CI_JOB_TOKENFor one jobThe pipeline pushes an image, pulls a package, clones an allowed project
        Project access token / group tokenUp to a year, with a bot userA bot that comments on merge requests, a release tool
        Service accountLong-lived account, several tokensA tool shared across many projects (Renovate)
        OAuth applicationTokens on behalf of a consenting user"Sign in with GitLab", an internal portal
        WebhookNo identity: GitLab calls youReact to events in real time
        Native integrationConfigured by a formSlack, Jira, Teams
        The rule of thumb

        Least privilege, shortest lifetime: CI_JOB_TOKEN first, then a project token, then a service account. Avoid personal access tokens for automation: they carry all of someone's rights and break when that person leaves.

        NeedGitHubGitLab
        The pipeline pushes an image or reads another repositoryGITHUB_TOKENCI_JOB_TOKEN (+ allowlist in job token permissions)
        A bot comments on merge requests, opens ticketsGitHub AppProject or group token, or a service account Premium
        A SaaS app asks for access to an accountGitHub App or OAuth AppOAuth application
        React to an eventApp webhookProject or group webhook
        An external AI agent works in the repositoryApp + Copilot or ClaudeExternal agents Premium, or a CI job that calls the agent

        Project access tokens

        Add new project access token form with name, expiration, role and scopes
        Settings, Access tokens, Add new token (form not submitted).
        Project access tokensPremium on GitLab.comA token bound to the project, with a role, scopes and an expiry date. It creates a bot user that is a member of the project.
        Why it exists
        Automate (API, git, registry) without using a person's account. The closest thing to a simple GitHub App.
        In real companies
        A bot that posts AI review comments on merge requests; a release tool that pushes tags.
        How to use it
        Settings, Access tokens, Add new token: name, description, expiry, role (Guest to Owner), scopes (api, read_api, read_repository, write_repository, read_registry…). The token is shown only once.
        Limits and pitfalls
        On GitLab.com, Premium and Ultimate only (the form is visible in Free). Maximum lifetime 365 days by default. Unlike a GitHub App, no short-lived auto-renewed token.
        Exam tip
        Project, group, personal, deploy token and CI_JOB_TOKEN: know which one fits which need.
        Service accountsPremiumNon-human accounts created at group (or instance) level, which can hold several tokens and be members of several projects.
        Why it exists
        A stable identity for a tool that touches many projects.
        In real companies
        The company's Renovate bot, member of every project of the group, with a token renewed every 90 days.
        How to use it
        Group settings, Service accounts, then add as a member and create tokens.
        Limits and pitfalls
        Premium and Ultimate. Does not use a paid seat.
        OAuth applicationsFreeAn external application registered in GitLab, which gets a token on behalf of a user after consent.
        Why it exists
        "Sign in with GitLab" and delegated API access for a third-party tool.
        In real companies
        An internal portal (Backstage) that shows the merge requests of the signed-in user.
        How to use it
        Avatar, Edit profile, Applications, or group settings, Applications: name, redirect URL, scopes. Authorization code flow (with PKCE).
        Limits and pitfalls
        The application acts with the user's rights within its scopes: ask for the minimum (read_user, read_api).
        Personal access tokensFreeA token that acts as you, with scopes and an expiry date.
        Why it exists
        git over HTTPS, personal scripts, the glab CLI.
        In real companies
        Your laptop: git clone over HTTPS when SSH is blocked.
        How to use it
        Avatar, Edit profile, Access tokens. Minimal scopes, short expiry.
        Limits and pitfalls
        Avoid them for team automation: they carry all your rights and break when you leave.

        Integrations

        Integrations settings listing available integrations
        Settings, Integrations.
        IntegrationsFreeNative connectors to external tools (Slack, Jira, Teams, Jenkins, Asana…), configured with a form.
        Why it exists
        Connect GitLab to the rest of the toolchain without writing code.
        In real companies
        Slack notifications for failed pipelines on main; Jira links in commits and merge requests.
        How to use it
        Settings, Integrations, Add. Also configurable at group or instance level for every project.
        Limits and pitfalls
        If the tool is not listed, use a webhook.

        Webhooks

        Add new webhook form with URL, signing token, secret token and triggers
        Settings, Webhooks, Add new webhook (not submitted).
        WebhooksFreeAn HTTP POST that GitLab sends to your URL on each chosen event (push, tag, merge request, pipeline, job, deployment…).
        Why it exists
        Trigger an external system in real time: a bot, a deployment tool, an audit trail.
        In real companies
        An internal service receives pipeline events and updates a release dashboard.
        How to use it
        Settings, Webhooks, Add new webhook: URL, signing token (webhook-signature header, recommended) or secret token (X-Gitlab-Token), events, test delivery.
        Limits and pitfalls
        Always verify the signature on the receiving side. A webhook that fails too often is disabled automatically.

        ChallengePick the identity

        For each case, choose an identity: (a) the pipeline downloads a package from another project of the same group; (b) a nightly script outside GitLab opens issues in 40 projects; (c) your team's chat must be told when main fails.

        Show the solution

        (a) CI_JOB_TOKEN, after adding the consuming project to the source project's job token allowlist. (b) A service account with a token scoped to api and a short expiry (Premium), or a group access token. (c) The native Slack or Teams integration, or a pipeline-events webhook.

        Publish a website with GitLab Pages

        20 min, this course is the example
        By the end of this station you can
        • publish a static site from a pipeline;
        • explain the pages job line by line;
        • find the site's address and control who can see it.

        The page you are reading is published by the lab's own pipeline. The course lives in docs/; a tiny job copies it into public/, and GitLab Pages serves whatever a job named pages leaves in public/ as an artifact.

        pages:
          stage: deploy
          image: busybox:1.37
          needs: []1
          script:
            - rm -rf public && cp -r docs public2
          artifacts:
            paths: [public]3
          rules:
            - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH4
        1. The site does not depend on the image, so it starts immediately instead of waiting for the six stages.
        2. No build step: the course is plain HTML, CSS and JavaScript. A documentation generator (MkDocs, Hugo) would run here.
        3. The artifact named public is what Pages publishes.
        4. Only the default branch publishes. Merge requests can still preview the site through the job's artifacts.

        After the first successful run, Deploy, Pages shows the address. New projects on GitLab.com often get a unique domain such as devsecops-gitlab-lab-xxxx.gitlab.io; you can switch to namespace.gitlab.io/project on the same page, add a custom domain, or restrict access to project members.

        GitLab PagesFreeHosting for static websites produced by a pipeline job.
        Why it exists
        Publish documentation, a report or a small site without running a server.
        In real companies
        Technical documentation built by MkDocs on every merge to main; a coverage report; this very course.
        How to use it
        A job named pages that leaves a public/ folder as an artifact. The URL appears in Deploy, Pages.
        Limits and pitfalls
        New projects on GitLab.com often get a unique domain (project-xxxx.gitlab.io) rather than namespace.gitlab.io/project. Access control for private sites is available.

        Hands-onPublish your fork's copy

        Push any change to main in your fork. When the pages job is green, open Deploy, Pages and visit your own copy of this course.

        Show a hint

        The first deployment can take a few minutes to become reachable. If the menu entry is missing, check that Pages is enabled in Settings, General, Visibility.

        AI in GitLab: Duo, agents and flows

        35 min, with guard rails
        By the end of this station you can
        • tell Duo Chat, agents and flows apart;
        • explain what the AI features cost and which tier they need;
        • add an AI review job to a pipeline without a paid tier, safely.

        GitLab Duo groups GitLab's AI features. Its newest part, the Agent Platform, runs agents (a role, some tools, a model) and flows (several agents chained for one task: turn an issue into a merge request, fix a pipeline, review code).

        Duo Chat (right panel)

        GitLab Duo Chat open in the right panel
        GitLab Duo Chat in the right panel.
        GitLab Duo ChatPaid add-on or GitLab CreditsA conversational assistant in the right panel that knows the context of the page (project, issue, merge request, job, file).
        Why it exists
        Explain a pipeline, a failing job or a vulnerability; suggest code or configuration; answer questions about GitLab.
        In real companies
        "Why does this job fail?" on a red log; "How do I store a secret in GitLab CI?".
        How to use it
        Icon at the top right (New GitLab Duo Chat); /help lists commands; /explain, /tests, /refactor work best on selected code in an IDE. Model picker in the input field.
        Limits and pitfalls
        Uses GitLab Credits depending on your plan. Never paste a secret into the chat.

        Agent Platform and the CI/CD Catalog

        CI/CD Catalog page with the Duo Agent Platform panel open
        Explore, CI/CD Catalog, next to the Duo Agent Platform panel.
        Duo Agent PlatformCredits (some agents: Premium or Ultimate)GitLab's agent platform: agentic chat, foundational agents (Planner, Security Analyst, Data Analyst, CI Expert…), custom agents and flows.
        Why it exists
        Delegate tasks to agents that read and change the project: plan, analyse vulnerabilities, write a pipeline.
        In real companies
        The Security Analyst triages vulnerabilities; the Planner breaks an epic down; the CI Expert proposes a .gitlab-ci.yml.
        How to use it
        Duo panel, pick an agent and a model. AI, Agents to enable catalog agents in the project.
        Limits and pitfalls
        In Free, agentic features are reached by buying GitLab Credits; external agents need Premium; security features need Ultimate. An agent acts with your rights: review its merge requests.
        FlowsCreditsChains of agents for a multi-step task, executed on GitLab runners.
        Why it exists
        Automate a whole piece of work: issue to merge request (Developer Flow), code review, fixing a broken pipeline, converting a Jenkins pipeline.
        In real companies
        Assign a simple issue to the Developer Flow and review the merge request it produces.
        How to use it
        From an issue or a merge request (Duo buttons) or from the catalog. Custom flows in YAML.
        Limits and pitfalls
        Consumes credits and compute minutes. Keep human review mandatory.

        AI Catalog (AI, Agents)

        AI Catalog listing agents
        AI, Agents, AI Catalog.
        AI CatalogCreditsThe catalog of agents and flows published by GitLab and the community, with visibility, usage and stars.
        Why it exists
        Find and enable a specialised agent in a project instead of writing one.
        In real companies
        Enable the CI Expert Agent in the platform project; publish an internal "Release Scribe" agent for the company.
        How to use it
        AI, Agents, AI Catalog, pick one, Enable. The Managed tab lists yours.
        Limits and pitfalls
        Community agents are not audited by GitLab: read their instructions and tools before enabling them.
        External agents (Claude Code and others)PremiumThird-party agents plugged into the platform, which work in issues and merge requests like GitLab's own agents.
        Why it exists
        Use the coding agent of your choice inside the GitLab flow.
        In real companies
        Mention an external agent in an issue so it opens a merge request.
        How to use it
        Premium/Ultimate, configured at group level. Universal alternative: a CI job that calls the agent (example in this station).
        Limits and pitfalls
        Code and context are sent to the model provider: check your company policy.
        MCP (server and clients)Free (external servers for agents: paid, beta)Model Context Protocol: GitLab exposes an MCP server (issues, merge requests, pipelines) to AI tools, and its agents can call external MCP servers.
        Why it exists
        Let Claude, an IDE or another agent read and act on GitLab in a standard way.
        In real companies
        Claude Code on a laptop queries failed pipelines through GitLab's MCP server.
        How to use it
        Documentation page "GitLab MCP server": server URL plus OAuth authentication.
        Limits and pitfalls
        An MCP client acts with your rights: limit the tools it may use.
        GitLab CreditsUsage-basedThe usage-based billing unit for the AI features of the Agent Platform.
        Why it exists
        Pay for agents and flows by consumption rather than per seat.
        In real companies
        A monthly pool at group level, monitored by an administrator.
        How to use it
        See the documentation "GitLab Credits and usage billing".
        Limits and pitfalls
        Watch consumption: a flow that loops gets expensive quickly.

        An AI review job without a paid tier

        You can ask an AI model to review a merge request from the pipeline itself. The idea is the same on GitHub: a job triggered on merge requests, an API key in a masked and protected variable, and a narrowly scoped GitLab token to post the comment. This example uses Claude Code; adapt it to your provider.

        claude_review:
          stage: test
          image: node:24-alpine
          rules:
            - if: $CI_PIPELINE_SOURCE == "merge_request_event"
          variables:
            GIT_DEPTH: 0
          script:
            - apk add --no-cache git curl
            - npm install -g @anthropic-ai/claude-code
            - git diff origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD > mr.diff
            # ANTHROPIC_API_KEY: masked CI/CD variable (Settings, CI/CD, Variables)
            - claude -p "Review mr.diff. List security risks and bugs in at most 10 lines." --allowedTools "Read" > review.md
            # GITLAB_REVIEW_TOKEN: token with the api scope, masked
            - |
              curl -sS --request POST --header "PRIVATE-TOKEN: $GITLAB_REVIEW_TOKEN" \
                --data-urlencode "body@review.md" \
                "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes"
          allow_failure: true
        Guard rails

        This job runs on code proposed by the merge request. Do not enable it for merge requests from forks without review; limit the agent's tools (--allowedTools); never give the token more than the scope it needs. A protected variable is not available to merge request pipelines of unprotected branches, which is a safety feature here, not a bug. Anthropic documents an official GitLab CI/CD integration for Claude Code: read it before scaling up.

        In real companies

        AI agents act with the rights of whoever runs them. Teams keep human review mandatory on every agent-made merge request, restrict which projects may use agents, and send code to a model provider only if the company's data policy allows it.

        Hands-onAsk Duo about the gate

        If your account has access to Duo, open the chat on the trivy_gate job page and ask: "Explain what this job does and when it fails." Compare the answer with pin 9 of station 7.

        Coming from GitHub: the dictionary

        20 min, reference
        By the end of this station you can
        • translate any GitHub Actions concept into its GitLab equivalent;
        • spot the real design differences, not just the renamed ones.

        This lab has a twin: GitHub Actions DevSecOps, same app, same goals, GitHub version. If you know one platform, this table gets you productive on the other.

        IdeaGitHubGitLab
        Team containerOrganization (one level)Group and subgroups (nested)
        RepositoryRepositoryProject
        Change proposalPull request (#12)Merge request (!12)
        Pipeline file.github/workflows/*.yml (several).gitlab-ci.yml (one, plus include)
        Steps of a jobsteps: with uses: or run:script: (shell), before_script, after_script
        Reusable building blockAction, reusable workflowCI/CD component, include, extends, child pipeline
        CatalogMarketplaceCI/CD Catalog
        OrderEverything in parallel, ordered by needsStages in sequence, needs for a graph
        Matrixstrategy.matrixparallel:matrix
        Conditionson: + if:workflow:rules + rules:
        ExecutorRunner (GitHub-hosted or self-hosted), runs-onRunner (instance, group, project), tags
        Pipeline tokenGITHUB_TOKEN + permissions:CI_JOB_TOKEN (rights of the triggering user, allowlist)
        SecretsSecrets and variables (repository, environment, organization)CI/CD variables: masked, protected, hidden (project, group, instance)
        OIDCpermissions: id-token: writeid_tokens: with aud
        Approval before deploymentEnvironment with required reviewersProtected environment with deployment approvals Premium
        One deployment at a timeconcurrencyresource_group
        Cancel the older runcancel-in-progressinterruptible + auto_cancel
        Manual startworkflow_dispatchRun pipeline + variables or spec:inputs; job when: manual
        Image registryGHCRProject container registry
        Dependency updatesDependabotRenovate (to install); dependency scanning Ultimate
        Code analysisCodeQLSAST (Semgrep), Advanced SAST Ultimate
        Branch protectionRulesets, branch protectionProtected branches, push rules Premium, merge request approvals
        Bot or integrationGitHub AppProject token, service account, OAuth app, webhook
        AICopilot (chat, review, cloud agent)GitLab Duo (chat, agents, flows)
        Web editor, cloud developmentgithub.dev, CodespacesWeb IDE, Workspaces
        ProvenanceArtifact attestationscosign for images; SLSA provenance for artifacts generated by the runner
        The differences that really matter

        GitHub starts from "everything runs in parallel" and adds order; GitLab starts from "stages run in order" and adds a graph. GitHub's token permissions are set per workflow in YAML; GitLab's job token inherits the triggering user's role. GitHub delegates bots to Apps; GitLab spreads them over tokens and accounts.

        Preparing a GitLab certification

        30 min, plan your revision
        By the end of this station you can
        • map what you practised here to the certification domains;
        • build a revision plan around the quizzes and exercises.

        GitLab University offers several certifications. For what this course covers, aim for GitLab Certified CI/CD Associate, then possibly GitLab Certified Security Specialist. The format (questions, practical exam, duration, price) changes over time: check the official exam page before you register.

        DomainTopicsWhere in this course
        Syntax and structurestages, jobs, script, image, services, variables, hidden jobs, extends, YAML anchors, !reference5, 6, 7
        Flow controlworkflow:rules, rules (if, changes, exists), needs, when, allow_failure, merge request pipelines, parent-child, multi-project6, 4
        Data between jobsArtifacts and expire_in, reports (junit, dotenv…), cache and keys, predefined variables, variable precedence5, 11
        RunnersInstance, group, project; tags; executors (docker, shell, kubernetes); protected runners; compute minutes11
        DeliveryEnvironments, deployment_tier, review apps, on_stop, resource_group, registry, releases, Pages9, 13, 3
        SecurityScanning templates and components, reports, protected and masked variables, CI_JOB_TOKEN, id_tokens, protected branches8, 10, 12

        A two-week revision plan

        1. Days 1 to 3: stations 5 and 6 again, plus every simulator answer until you predict them all.
        2. Days 4 to 6: rebuild the lab's pipeline from an empty .gitlab-ci.yml in your fork, without looking.
        3. Days 7 to 9: open every feature sheet that has an exam tip (use the feature index) and explain each tip aloud.
        4. Days 10 to 12: do the challenges at the end of the course.
        5. Days 13 and 14: take the final exam until you score above 90%, then read the official exam guide one last time.

        Final exam and challenges

        40 min, the terminus
        By the end of this station you can
        • prove to yourself that you master the essentials (aim for 80% or more);
        • pick a challenge that turns the lab into your own portfolio project.

        Your result

        Answer every question below to see your score.

        Challenges

        Each challenge extends the lab the way a real team would. Do one, push it to your fork, and you have something concrete to show.

        ChallengeReplace templates with components

        Swap the three include: template: lines for their CI/CD components from the catalog, pinned to exact versions, and pass the image to scan with inputs.

        Show a hint

        Search the CI/CD Catalog for sast, secret-detection and container-scanning; each component's page lists its inputs.

        ChallengeReview apps for every merge request

        Add a deploy_review job with environment: name: review/$CI_COMMIT_REF_SLUG, on_stop and auto_stop_in: 1 day, running only in merge request pipelines. Remember that the sign job does not run there: decide how you will verify the image.

        ChallengeA release on every tag

        Add a job that runs only on tags, after sign, and creates a GitLab release whose description contains the image digest and a link to the SBOM artifact.

        Boss levelDeploy to a real cluster

        Install the GitLab agent for Kubernetes in a local cluster (kind or k3d), grant ci_access to your fork, and change .deploy to kubectl apply a Deployment that uses image@digest. Keep the cosign verify step. The Kubernetes + Helm course has everything you need on the cluster side.

        Ix

        Feature index

        75 features, searchable

        Type a word (runner, variable, token, agent…) to filter. Click a feature to jump to its sheet.

        • Search and command palette Free
          The Search or go to field: global search plus quick navigation to a project, a page or a command.
        • To-Do List and counters Free
          The three counters at the top right: issues assigned to you, merge requests (assigned or waiting for your review) and your To-Do List.
        • Groups and subgroups Free
          A space that contains projects and other groups, with its own members, CI/CD variables, runners and settings.
        • Members and roles Free (custom roles: Ultimate)
          The people who can access a project or group, each with a role: Guest, Planner, Reporter, Developer, Maintainer, Owner.
        • Profile and preferences Free
          The avatar menu: status, profile, preferences (theme, language, editor), subscription, what's new, sign out.
        • Branches, commits and tags Free
          Code, Branches / Commits / Tags: the web view of the project's git history.
        • Project information shortcuts Free
          The right-hand column of a project page: description, statistics and links such as Add LICENSE, Add CHANGELOG, CI/CD configuration, Environments.
        • Work items (issues, tasks, epics) Free (epics: Premium)
          Plan, Work items: the project's tickets. A work item has a type (issue, task, epic, incident…), a status, assignees, labels, a milestone, a weight.
        • Issue boards and milestones Free (scoped labels: Premium)
          A kanban board whose columns are labels, assignees or milestones. Milestones group tickets by due date.
        • Wiki Free
          Markdown documentation stored in a separate git repository attached to the project.
        • Snippets Free
          Versioned fragments of code or configuration, shared at project or personal level.
        • Pipeline schedules Free
          Planned runs of the pipeline (cron) on a branch, with their own variables.
        • Artifacts Free
          Files a job keeps after it ends: binaries, reports, SBOMs. Build, Artifacts lists them for the whole project.
        • Audit events Premium (streaming: Ultimate)
          A log of who did what: role changes, token creation, changes to variables or protected branches.
        • Releases Free
          A published version tied to a git tag, with release notes, links to binaries or images, and evidence.
        • Feature flags Free
          Switches for features, managed in GitLab and compatible with Unleash SDKs.
        • Package registry Free
          A built-in registry for npm, Maven, PyPI, NuGet, Composer, Conan, Helm, Go and generic packages.
        • GitLab Pages Free
          Hosting for static websites produced by a pipeline job.
        • Labels Free (scoped: Premium)
          Coloured tags on work items and merge requests, defined per project or per group.
        • Terraform / OpenTofu states and modules Free
          An HTTP backend for Terraform state managed by GitLab, plus a module registry.
        • Monitor: error tracking, alerts, incidents, Service Desk Free (advanced on-call: paid)
          Operations tools: application errors (Sentry-compatible), incoming alerts, incidents with a timeline, Service Desk (tickets by email).
        • Analyze: value stream, CI/CD analytics, DORA Free (full DORA: Ultimate)
          Dashboards about the delivery flow: time per stage, pipeline success rate and duration, deployment frequency.
        • Clone over SSH or HTTPS Free
          The Code menu gives clone URLs, IDE shortcuts and source downloads.
        • Web IDE Free
          A VS Code editor in the browser, opened on the repository (press . or Code, Web IDE).
        • Workspaces Premium
          Remote development environments (a container plus an IDE) started from GitLab in your own Kubernetes cluster.
        • Merge request Free (security widget: Ultimate)
          A request to merge a source branch into a target branch, with discussion, diff, pipeline, approvals and reports.
        • Merge request approvals Free (required rules: Premium)
          The Approve button and, in paid tiers, mandatory approval rules (how many, who, which groups).
        • CODEOWNERS Premium
          A file that maps paths to owners (people or groups), optionally in sections.
        • Issue and merge request templates Free
          Markdown files offered in the Description list when you create an issue or a merge request.
        • Pipelines Free
          Build, Pipelines: every run, with status, source (branch, merge request, tag), author and stages.
        • rules and workflow:rules Free
          Conditions checked in order; the first one that matches decides (when, allow_failure, variables).
        • needs (DAG) Free
          The list of jobs a job really depends on. It starts as soon as they finish, without waiting for its whole stage.
        • include, extends and hidden jobs Free
          Ways to reuse configuration: assemble files (include) and inherit blocks (extends).
        • CI/CD components and the CI/CD Catalog Free
          Versioned pipeline building blocks with typed parameters (spec:inputs), published in a searchable catalog.
        • Cache Free
          Files kept between pipelines to go faster (pip, npm, Maven downloads).
        • Parent-child and multi-project pipelines Free
          A job with trigger: that starts another pipeline: a child pipeline (same project, other YAML) or another project's pipeline.
        • Predefined variables Free
          Variables GitLab gives every job: CI_COMMIT_SHA, CI_COMMIT_SHORT_SHA, CI_DEFAULT_BRANCH, CI_PIPELINE_SOURCE, CI_REGISTRY_IMAGE, CI_JOB_TOKEN, CI_PROJECT_URL…
        • Pipeline editor Free
          An editor dedicated to .gitlab-ci.yml with live validation, visualisation, simulation and merged configuration.
        • id_tokens (OIDC) Free
          JWT tokens signed by GitLab, requested by a job, that describe its identity (project, branch, pipeline, environment).
        • Docker-in-Docker (services) Free
          A side container docker:dind that gives the job a Docker daemon, reachable at the hostname docker.
        • Jobs and logs Free
          A job page: live log, duration, runner, commit, pipeline, related jobs, and Retry, Cancel, Erase buttons.
        • Container registry Free
          The project's Docker image registry, at registry.gitlab.com/<namespace>/<project>.
        • Environments Free (protected environments: Premium)
          Operate, Environments: each deployment target (staging, production…) with its latest deployment, history and actions.
        • Review apps Free
          Dynamic environments created per merge request, with a preview URL and automatic stop.
        • SAST Free (Advanced SAST and MR widget: Ultimate)
          Static analysis of your source code, based on Semgrep and GitLab rules.
        • Secret detection Free (push protection: Ultimate)
          A search for secrets (cloud keys, GitLab tokens, private keys…) in commits.
        • Container scanning Free
          A scan of the packages inside the Docker image (OS and language packages) with Trivy.
        • Dependency scanning Ultimate
          Analysis of declared dependencies (requirements, package-lock, pom) and generation of an SBOM.
        • Vulnerability report Ultimate
          The consolidated list of vulnerabilities found on the default branch, each with a status (detected, confirmed, dismissed, resolved).
        • Security policies Ultimate
          Central rules: scan execution policies (force scans), pipeline execution policies (force jobs), merge request approval policies (require approval when a flaw appears).
        • Auto DevOps Free
          A complete predefined pipeline (build, tests, scans, Kubernetes deployment) used when a project has no .gitlab-ci.yml.
        • Pipeline trigger tokens Free
          Tokens that start a pipeline through an API call (POST /projects/:id/trigger/pipeline).
        • Deploy freezes Free
          Periods, written as cron expressions, during which deployments are not allowed.
        • CI_JOB_TOKEN and job token permissions Free
          The automatic token of every job, valid while the job runs, with the permissions of the user who started the pipeline, limited to some APIs.
        • Secure files Free
          Storage for sensitive files (Android keystores, Apple profiles, certificates) that jobs can download.
        • CI/CD variables Free
          Key/value pairs injected into jobs, defined on a project, group or instance, with a type, an environment scope, a visibility and flags.
        • Runners Free
          The agents that execute jobs: instance runners (provided by GitLab.com), group runners, project runners.
        • Protected branches and tags Free (code owner approval: Premium)
          Rules on a branch or pattern: who can merge, who can push, whether force push is allowed.
        • Deploy tokens and deploy keys Free
          Dedicated access for one use: a deploy token for registry, packages or repository; a deploy key is an SSH key that can clone.
        • Merge checks Free
          Mandatory conditions to merge: pipeline succeeded, threads resolved, and more depending on your tier.
        • Merged results and merge trains Premium
          Merged results: test the result of merging the merge request into its target. Merge train: a queue of merge requests, each tested together with the ones ahead of it.
        • GitLab agent for Kubernetes Free
          A component (agentk) installed in your cluster that opens an outbound connection to GitLab. The cluster API is never exposed.
        • Project access tokens Premium on GitLab.com
          A token bound to the project, with a role, scopes and an expiry date. It creates a bot user that is a member of the project.
        • Service accounts Premium
          Non-human accounts created at group (or instance) level, which can hold several tokens and be members of several projects.
        • OAuth applications Free
          An external application registered in GitLab, which gets a token on behalf of a user after consent.
        • Personal access tokens Free
          A token that acts as you, with scopes and an expiry date.
        • Integrations Free
          Native connectors to external tools (Slack, Jira, Teams, Jenkins, Asana…), configured with a form.
        • Webhooks Free
          An HTTP POST that GitLab sends to your URL on each chosen event (push, tag, merge request, pipeline, job, deployment…).
        • GitLab Duo Chat Paid add-on or GitLab Credits
          A conversational assistant in the right panel that knows the context of the page (project, issue, merge request, job, file).
        • Duo Agent Platform Credits (some agents: Premium or Ultimate)
          GitLab's agent platform: agentic chat, foundational agents (Planner, Security Analyst, Data Analyst, CI Expert…), custom agents and flows.
        • Flows Credits
          Chains of agents for a multi-step task, executed on GitLab runners.
        • AI Catalog Credits
          The catalog of agents and flows published by GitLab and the community, with visibility, usage and stars.
        • External agents (Claude Code and others) Premium
          Third-party agents plugged into the platform, which work in issues and merge requests like GitLab's own agents.
        • MCP (server and clients) Free (external servers for agents: paid, beta)
          Model Context Protocol: GitLab exposes an MCP server (issues, merge requests, pipelines) to AI tools, and its agents can call external MCP servers.
        • GitLab Credits Usage-based
          The usage-based billing unit for the AI features of the Agent Platform.
        Aa

        Glossary

        Every term used in the course

        Dotted words in the course open these definitions in place.

        +

        Keep learning

        Official references and the rest of the series

        Official documentation

        The DevSecOps Learning Series

        Three courses, one method: a real lab, annotated screenshots, simulators and quizzes.