Git Course 0%

Reading and writing .gitlab-ci.yml

Intermediate GitLab UI ≈ 14 min

What you will learn

  • Every keyword you will meet in a small pipeline file
  • How to add a check of your own without breaking anything
  • How to test a change to the file before pushing it

After this lesson you can

  • I can read my project's pipeline file and add a documentation check to it

Why this matters

The file is the pipeline. Reading it tells you exactly what will be checked, which lets you run the same checks locally; writing a small piece of it is a realistic and welcome contribution, because documentation checks are usually nobody's job.

The real file

This site's own .gitlab-ci.yml, entire. It has three jobs: one that checks the committed HTML is up to date, one that checks every link, and one that publishes the site.

.gitlab-ci.yml
stages:
  - check
  - deploy

default:
  image: python:3.13-slim

build-is-current:
  stage: check
  script:
    - pip install --quiet markdown
    - python tools/build.py
    - git diff --exit-code || (echo "The committed HTML is out of date. Run 'python tools/build.py' and commit the result." && exit 1)

links:
  stage: check
  script:
    - pip install --quiet markdown
    - python tools/build.py
    - python tools/check-links.py

pages:
  stage: deploy
  script:
    - pip install --quiet markdown
    - python tools/build.py
    - mkdir -p public
    - cp -r index.html assets playground start reference labs [0-9]* public/
  artifacts:
    paths:
      - public
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Line by line

stages lists the phases in order. Everything in check runs before anything in deploy, and if any check job fails, deploy never starts.

default: with image: sets the container every job starts from, unless a job overrides it. python:3.13-slim is a small official Python image; the slim variants save minutes of download time.

Each top-level name is a job. build-is-current, links and pages are the three here. The names are yours to choose, except that pages is special on GitLab: it is the job whose artifact gets published.

script is a list of shell commands, run in order in a fresh container. If any command exits non-zero, the job fails. These are ordinary commands you can run yourself, which is the most useful thing to know about the whole file.

artifacts: paths: keeps files after the job ends. For pages, the public folder is what GitLab serves.

rules: decide whether a job runs at all. Here, pages runs only when the branch is the project's default branch, so a merge request runs the checks but never publishes.

The clever line is in build-is-current. The site's HTML is committed, so a contributor could edit a lesson and forget to rebuild. The job rebuilds and then asks Git whether anything changed:

Terminal
$ python tools/build.py
$ git diff --exit-code

git diff --exit-code exits 1 when there are differences, which fails the job with a message telling the contributor exactly what to do.

The keywords you will meet elsewhere

Keyword Does
stage Which phase the job belongs to
image The container this job runs in
script The commands
before_script / after_script Commands run around every job's script
rules / only / except When the job runs; rules is the modern form
artifacts Files to keep
cache Files to reuse between runs, such as pip downloads
needs Run this job as soon as named jobs finish, ignoring stage order
variables Values available to the job
when: manual The job waits for a person to press play
allow_failure: true The job can fail without failing the pipeline
include Pull in another YAML file, often a shared template
environment Record this job as a deployment (lesson 14.6)

Adding a check of your own

A documentation check is the natural first contribution. Adding a job is four lines:

YAML
spelling:
  stage: check
  image: node:20
  script:
    - npx --yes cspell "docs/**/*.md"
  allow_failure: true

Two deliberate choices there. The job overrides the default image, because the tool is a Node package. And allow_failure: true means it reports without blocking, which is the polite way to introduce a new check to a team: everyone sees the result for a few weeks, the existing problems get cleaned up, and only then does it become blocking.

Testing a change before pushing

Three levels, in increasing order of effort:

  1. Run the script lines yourself. They are ordinary commands.
  2. Use the platform's linter: Build → Pipeline editor → Validate, which checks the YAML and the structure without running anything.
  3. Push to a branch. A pipeline change is like any other change: it goes on a branch and through a merge request, and the pipeline that runs on that branch is the new one.

The pipeline editor is worth knowing about: it catches indentation mistakes, which are the most common YAML error and the most annoying to debug from a failed run.

How to do it

Read the file, then run what it runs:

Terminal
$ cat .gitlab-ci.yml
$ python tools/build.py
$ python tools/check-links.py

If those pass locally, the equivalent jobs will almost certainly pass on the runner. The remaining differences are versions and uncommitted files (lesson 14.2).

Common mistakes

  • Tabs, or wrong indentation. YAML rejects tabs; nesting decides meaning.
  • Assuming a job runs everywhere. Check its rules.
  • A new check added as blocking, which stops everyone's work until the backlog of existing problems is cleared.
  • Forgetting the job needs its tools installed. A fresh container has nothing but the image.
  • Editing the file directly on the default branch, which is both risky and usually blocked.
  • Expecting the runner to see uncommitted files.

Try it yourself

Goal: add a non-blocking check to your practice project.

  1. Read your project's .gitlab-ci.yml and list the jobs, their stages and their images.
  2. Run one job's script lines in your own terminal and confirm they pass.
  3. On a branch, add a job that checks something about documentation: a linter, a spell checker, or a link checker.
  4. Give it allow_failure: true, push, and read the result on the merge request.
  5. Open the Pipeline editor and use Validate to see what it reports.

Expected result: a new job visible in the pipeline, reporting without blocking, added by you without touching any application code.

Show solution

Step 4's allow_failure: true is the part that makes this a welcome contribution rather than an unwelcome one. Introducing a check that immediately blocks everyone's merges is how good ideas get reverted; introducing it as advisory gets it accepted and then promoted.

Check yourself

1. What does rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH do on a job?
2. What are the script lines in a job?
3. You want to add a new documentation check without blocking the team. What do you add?

Key terms

Pipeline Artifact Environment variable Continuous integration (CI)