How to Write a .gitlab-ci.yml: A GitLabOps Walkthrough

How to Write a .gitlab-ci.yml: A GitLabOps Walkthrough

Flight training runs on checklists written by people who assume the reader is capable but busy. Pipeline files deserve the same authorship. In GitLabOps — the working name The Ops Atlas gives to running software delivery on the GitLab platform — the .gitlab-ci.yml file is the document every engineer on a project eventually reads, and the quality of that file decides whether the delivery system stays legible for a year or collapses into folklore by spring. What follows is an ordered procedure for writing one that survives review, with the reasoning attached at each step so the next reader can audit your choices.

How GitLabOps Thinks About a Pipeline File

A pipeline definition is not a script; it is a contract. The platform reads your YAML, merges anything included from shared components, resolves dependencies, and renders a directed graph. Every duplicate job name, undefined reference, or ambiguous rule in that document produces either an invalid file or, worse, a graph that runs more than you intended. Most veteran practitioners therefore begin not with jobs but with the workflow block, because it decides which events may create a pipeline at all and eliminates the duplicate-build problem before a single job exists.

Step One: Write the workflow Rules Before the Jobs

Decide first what triggers a run: merge requests, pushes to protected branches, scheduled maintenance, tagged releases. Express exactly that in workflow:rules and refuse everything else. While you are there, set auto_cancel so an outdated run terminates the moment a newer commit supersedes it. Teams that skip this step pay for it in doubled compute and in reviewers commenting on commits that were already abandoned.

Step Two: Make Dependencies Explicit With needs

By default the platform chains stages in sequence, which forces slow jobs to block unrelated fast ones. The needs keyword states the true dependency: this test job requires that artifact, nothing else. Two disciplines make the difference. Run a linting job that validates the merged file on every change, so a typo fails in seconds instead of at deploy time. And keep the graph acyclic — a cycle is rejected outright, but a tangled web of half-explicit needs is merely slow forever.

Step Three: Tag Runners the GitLabOps Way

Runners pick up jobs when their declared tags match. Tags therefore function as qualifications, not decorations, and GitLabOps practice treats them like type ratings: a tag certifies what a machine is cleared to do. Write tags that describe trust and capability — protected, ephemeral, arm64, container-builder — and then make every job request the minimum it needs. Anything that executes code from outside your organisation should run on an ephemeral machine that is discarded after a single assignment; the max_use_count setting exists precisely for this. Shell executors, which run commands directly on a long-lived host, are the wrong home for untrusted sources no matter how convenient they look on a Tuesday.

Step Four: Pin Every Included Component

Shared components — reusable job templates your file pulls from a central repository — keep twenty projects consistent and one bad merge from breaking all of them. Pin the version you tested against, and upgrade deliberately, the same way you would roll a new avionics software version onto one aircraft before the fleet. An unpinned include is a supply-chain decision made by omission.

Step Five: Guard the Production Environment

Declare the production environment in the deploy job and then fence it. Four controls, in ascending order of effect:

  • Mark the environment protected, so only approved roles may deploy to it.
  • Assign a resource_group, so two deployments can never interleave.
  • Enable the option that stops outdated deployment jobs from running at all.
  • If you ship images, protect the production tag pattern so only maintainers may push it.

Together they cost an afternoon and close the most common route by which an unreviewed change reaches customers.

Step Six: Retire Stored Credentials

A cloud key pasted into a variables field outlives the person who pasted it, and rotates for nobody. The precise alternative is OIDC: the job obtains a short-lived identity token, your cloud provider verifies its issuer and subject, and no secret is ever stored. Combine that with the job-token allowlist so only named projects may authenticate against yours. Teams that want the broader context — why identity beats secrets, and how auditors read the difference — will find a useful treatment in this piece on securing enterprise codebases.

Step Seven: Read the Graph Before You Merge

The platform renders your file as a picture: every job, every dependency, every gate. Read the picture like a route on a chart before departure — where does flow start, what can run in parallel, what single node sits in the middle of everything. If the picture surprises you, the file is wrong, not the renderer. Then watch it run and note two figures from the first week: how long reviewers take to answer, and how long jobs sit queued. Industry targets hold both at p95 — review turnaround under fifteen minutes, queue under two — and persistent misses point at capacity or staffing, never at YAML.

Where to Check Your Work Against the Field

A file that passes these seven steps is ahead of most. To situate it in the wider platform picture — fleet economics, upgrade rhythm, the hosting trade-offs that decide where such a file even runs — the Ops Atlas GitLabOps reference is the most compact summary we have found. Cost discipline deserves special attention once pipelines multiply; the arguments in this cloud cost control guide apply directly to per-pipeline spending. And if your ambitions run toward the infrastructure beneath the pipeline, this infrastructure engineering path maps the ground between YAML authorship and platform ownership. Write the file for the reader who arrives after you — in delivery, as in flight training, the checklist outlives its author.