Learn GitLab CI/CD - Writing Your First .gitlab-ci.yml & Basic Pipeline Syntax
Episode 2 of 21

Learn GitLab CI/CD - Writing Your First .gitlab-ci.yml & Basic Pipeline Syntax

Dissecting the anatomy of the .gitlab-ci.yml file from global keywords to stages definitions, then writing your first job with inline and multi-line scripts, handling errors with allow_failure, and assembling a complete hello-world pipeline with build and test stages.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

In episode 1 you understood the GitLab CI/CD architecture: the GitLab Server triggers, the Runner executes, and .gitlab-ci.yml describes everything. Now it's time to write your first pipeline code. This episode covers the anatomy of the .gitlab-ci.yml file from scratch — from global keywords and stages definitions, to your first job running build and test, complete with error handling.

Mastering this basic syntax is an absolute foundation: every upcoming episode — rules, variables, Docker, deployments — is built on top of the structure you'll learn now.

Main Discussion

Anatomy of the .gitlab-ci.yml File

.gitlab-ci.yml is a YAML file placed at the repository root. The filename is sacred — GitLab automatically detects it every time there's a push, merge request, or tag. If the file exists, GitLab creates a pipeline and displays it in the project's CI/CD tab.

Before writing jobs, there are several global keywords that govern pipeline behavior overall:

  • default — settings that apply to all jobs, such as image, before_script, or timeout.
  • stages — defines the sequence of pipeline execution phases.
  • variables — global variables (covered in depth in episode 5).
  • workflow — controls when pipelines are created (episode 4).

If you don't define stages at all, GitLab uses its built-in default: .pre, build, test, deploy, .post.

StagePurpose
.preRuns earliest, before all stages
buildCompiles and produces artifacts
testUnit tests, lint, integration tests
deployDeployment to environments
.postRuns last, for cleanup or notifications

Stages with a leading dot (.pre and .post) are special stages: you can't mention them in the stages list, but every job inside them still runs in the appropriate order. For a more explicit pipeline, define your own stages:

Defining global stages
stages:
  - build
  - test
  - deploy

The order in this list is the execution order. Jobs using a stage that isn't registered will be rejected by GitLab when creating the pipeline.

Writing Your First Job

A job is a YAML block that starts with a job name and contains at least stage and script. Job names are free-form (letters, numbers, underscores), as long as they're unique within the file.

Job structure: name, stage, script
unit_test:
  stage: test
  script:
    - npm test
  • Job name (unit_test) — the job identifier in the pipeline UI.
  • stage — determines which phase the job runs in.
  • script — the commands the runner executes.

Inline Commands

If a job runs only a single command, write it directly as the script value:

Single-line script (inline)
unit_test:
  stage: test
  script: npm test

Multi-line Commands

For several sequential commands, use a YAML list under script:

Multi-line script with a command list
unit_test:
  stage: test
  script:
    - echo "Menjalankan unit test"
    - npm ci
    - npm run lint
    - npm test

Each line is executed as a separate command within one shared shell session — variables set on the first line remain usable on later lines. If any command returns a non-zero exit code, the job fails immediately and subsequent commands are not executed.

Handling Errors with allow_failure

Sometimes you have jobs that may fail without stopping the pipeline — for example non-blocking linting or a test that's currently flaky. Use allow_failure: true:

allow_failure: job is allowed to fail
lint:
  stage: test
  script:
    - npm run lint
  allow_failure: true

If the lint job fails, the pipeline still continues to the next stage. In the UI, a job that fails with allow_failure: true still appears orange (warning) — so the problem stays visible, but it doesn't block releases.

Warning

allow_failure: true is an exception, not a habit. Enabling it on almost every job keeps your pipeline permanently "green" even when the code is broken — and that destroys the core value of CI/CD: honest feedback. Use it only for jobs that are truly non-critical or currently stabilizing.

A Complete Hello-World Pipeline

Now let's assemble everything into a real pipeline for a Node.js project:

.gitlab-ci.yml hello-world: build and test stages
stages:
  - build
  - test
 
before_script:
  - npm ci
 
build_app:
  stage: build
  script:
    - npm run build
 
unit_test:
  stage: test
  script:
    - npm test
 
e2e_test:
  stage: test
  script:
    - npx playwright test

There are several important things in the example above:

  • before_script runs before script in every job — here it's used to install dependencies once for all jobs.
  • The unit_test and e2e_test jobs are in the same test stage, so they both run in parallel after the build stage finishes.
  • Execution flow: build_app finishes first → then unit_test and e2e_test start.

After this file is pushed, you can monitor the pipeline directly from the terminal with glab ci status:

Example glab ci status output
$ glab ci status
build_app   build    running
unit_test   test     pending
e2e_test    test     pending

Common Mistakes

  1. File not at the repository root. .gitlab-ci.yml must be at the root; if it's placed in a subdirectory, GitLab won't detect it and no pipeline is ever created.
  2. Stage not defined. A job using stage: deploy when stages doesn't include it will be rejected by GitLab with the error chosen stage does not exist.
  3. Wrong indentation. YAML rejects tabs and mixed indentation. Always use 2 spaces per level, and make sure script, stage, and allow_failure are at the same indentation level (aligned).

Closing

In this episode 2, you've written a pipeline that actually runs:

  • Anatomy of .gitlab-ci.yml: the global keywords default, stages, variables, and workflow.
  • The built-in default stages .pre, build, test, deploy, .post, and how to define your own stage order.
  • Job structure: job name, stage, and script, plus the difference between inline commands and multi-line commands.
  • allow_failure: true as a way to handle non-critical jobs without stopping the pipeline.

Your pipeline is now alive. In episode 3 we'll dissect who actually runs the pipeline — the GitLab Runner. We'll cover the runner types (shared, group, specific), executor types (shell, docker, kubernetes), how to register a runner with a token, and how to route jobs to a specific runner using tags. See you in episode 3!