| | |

GitLab 09 🦊 Writing Your First .gitlab-ci.yml Pipeline Configuration

⏱️ Estimated read: 10 min | |

The previous chapter explained the architecture: stages, jobs, runners, and the needs keyword. This chapter builds the actual .gitlab-ci.yml file, line by line. The file is the single source of truth for the pipeline. It lives at the repository root, it is YAML, and it is parsed before any job runs. A syntax error prevents the pipeline from being created. The exam tests the essential keywords and the debugging commands. The critical distinction is between the global keywords (stages, variables, default) and the job keywords (stage, script, image, artifacts, rules). A job without a script is invalid. A job with a stage that is not in the stages list causes an error. A pipeline with no runners stays in the pending state.

Key point: The .gitlab-ci.yml file is validated with the CI Lint tool (CI/CD → Editor) before it is committed. The minimum valid file has one job with a script. The stages keyword is optional; if omitted, the default stages (build, test, deploy) are used. The file must be named exactly .gitlab-ci.yml and placed at the repository root.


Why .gitlab-ci.yml requires careful authoring

The YAML problem. YAML is whitespace-sensitive. A tab character in place of spaces causes a parse error. An unquoted colon in a string is interpreted as a key-value separator. A missing dash in a list is a syntax error. The pipeline editor in GitLab validates the syntax in real time, and it is the correct place to test the file before committing it .

The keyword problem. GitLab’s CI syntax has global keywords and job keywords. Placing a job keyword at the global level causes an error. Placing a global keyword inside a job causes it to be ignored. The script keyword is mandatory for every job. The stage keyword must match a stage in the stages list.

The stage-order problem. If a job declares stage: deploy but the stages list does not include deploy, the pipeline fails with a configuration error. The stages list defines the order. The stage keyword assigns a job to a stage. The two must agree.

The artifact problem. Artifacts are files produced by a job that are passed to subsequent jobs. The artifacts keyword specifies the paths. Without it, the file is available only within the job that created it. The dependencies keyword (or needs) controls which jobs receive the artifacts. The artifacts are uploaded after the job succeeds and downloaded before the dependent job starts .

The rules problem. The rules keyword replaces the older only and except keywords. It evaluates conditions in order. The first condition that matches determines whether the job runs. A job with no matching rule does not run. This is the mechanism for running jobs only on specific branches, tags, or merge requests.

The trade-off. A minimal file with one job and a simple script is easy to write. A file with multiple stages, artifacts, rules, and dependencies is more complex but necessary for a real pipeline. The exam tests the basic structure, not the advanced features.


a. The minimal valid pipeline

The smallest valid .gitlab-ci.yml has a single job with a script. Everything else is optional.

# .gitlab-ci.yml
job:
  script:
    - echo "Hello, GitLab"

The job name is job. The script is a list of shell commands. When committed to the repository, GitLab detects the file and creates a pipeline. The pipeline contains one job. The runner picks up the job, clones the repository, and executes the script. The job log shows the output .

The stages keyword is optional. If omitted, the default stages are build, test, and deploy. A job without a stage keyword is assigned to the test stage by default. This is a common source of confusion: a job that is intended to build is actually running in the test stage because the stage keyword was omitted .

# A job without a stage goes to "test"
build-job:
  script:
    - make build

The stage keyword makes the assignment explicit.

stages:
  - build
  - test

build-job:
  stage: build
  script:
    - make build

test-job:
  stage: test
  script:
    - make test

The pipeline runs build-job first, then test-job. If build-job fails, test-job does not run.


b. Images, artifacts, and variables

The image keyword specifies the container environment. The runner pulls the image and executes the script inside it. The before_script runs before the script. The after_script runs after the script, even if the job fails.

build-job:
  stage: build
  image: node:20
  before_script:
    - npm ci
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week

The artifacts keyword saves the dist/ directory and makes it available to subsequent jobs. The expire_in setting controls how long GitLab retains the artifacts. The default is 30 days. The paths is a list of files or directories relative to the project directory .

The variables keyword defines environment variables. Global variables are available to every job. Job-level variables override global variables.

variables:
  NODE_ENV: "production"

build-job:
  stage: build
  variables:
    NODE_ENV: "staging"
  script:
    - echo "Environment: $NODE_ENV"

The job-level NODE_ENV overrides the global value. The script prints staging, not production.

The dependencies keyword (or needs) controls which jobs receive the artifacts. Without it, a job receives artifacts from the previous stage. With needs, it receives only the artifacts from the specified jobs.

test-job:
  stage: test
  needs: [build-job]
  script:
    - npm test

The test-job receives the dist/ directory from build-job. The other artifacts from the build stage are not downloaded.


c. Rules and conditional execution

The rules keyword determines whether a job runs. It is a list of conditions. Each condition has an if clause and an optional when clause. The first matching condition determines the outcome.

deploy-job:
  stage: deploy
  script:
    - ./deploy.sh
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: always
    - when: never

The job runs if the commit is on the main branch. For any other branch, the second rule matches (when: never), and the job does not run. The when: never fallback is important: without it, a job with rules that do not match is excluded from the pipeline.

The rules keyword can also check the pipeline source: $CI_PIPELINE_SOURCE == "merge_request_event" runs the job only on merge request pipelines. The $CI_COMMIT_TAG variable matches tag pipelines.

test-mr:
  script:
    - npm test
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

deploy-tag:
  script:
    - ./deploy.sh
  rules:
    - if: $CI_COMMIT_TAG

The test-mr job runs only on merge request events. The deploy-tag job runs only on tag pipelines. Neither job runs on a regular push.


Complete Example Session

# ============================================
# PART 1: MINIMAL PIPELINE
# ============================================

cat > .gitlab-ci.yml <<'EOF'
job:
  script:
    - echo "Hello, GitLab"
EOF

git add .gitlab-ci.yml
git commit -m "Add minimal CI"
git push origin main
# ============================================
# PART 2: VIEW THE PIPELINE
# ============================================

# Build → Pipelines
# The pipeline shows one job: "job".
# Click the job to see the log.
# ============================================
# PART 3: ADD STAGES
# ============================================

cat > .gitlab-ci.yml <<'EOF'
stages:
  - build
  - test

build-job:
  stage: build
  script:
    - echo "Building..."

test-job:
  stage: test
  script:
    - echo "Testing..."
EOF
# ============================================
# PART 4: ADD AN IMAGE
# ============================================

build-job:
  stage: build
  image: node:20
  script:
    - npm ci
    - npm run build
# ============================================
# PART 5: ADD ARTIFACTS
# ============================================

build-job:
  stage: build
  script:
    - make build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week
# ============================================
# PART 6: ADD VARIABLES
# ============================================

variables:
  NODE_ENV: "production"

build-job:
  variables:
    NODE_ENV: "staging"
  script:
    - echo "Environment: $NODE_ENV"
# ============================================
# PART 7: ADD RULES
# ============================================

deploy-job:
  stage: deploy
  script:
    - ./deploy.sh
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - when: never
# ============================================
# PART 8: VALIDATE IN THE EDITOR
# ============================================

# Build → Pipeline editor
# Paste the YAML.
# The editor shows syntax errors and a visualization.
# ============================================
# PART 9: COMMIT AND VERIFY
# ============================================

git add .gitlab-ci.yml
git commit -m "Add complete CI pipeline"
git push origin main
# The pipeline runs. Check the status.

The nine parts trace the configuration: minimal file, view, stages, image, artifacts, variables, rules, validation, and commit.


Quick Reference

Essential Keywords

KeywordLevelPurpose
stagesGlobalDefine stage order
variablesGlobal or jobEnvironment variables
defaultGlobalDefault job settings
jobJobJob name
stageJobAssign to stage
scriptJobCommands to run
imageJobContainer environment
artifactsJobFiles to save
needsJobJob dependencies
rulesJobConditional execution

Artifact Options

OptionPurpose
pathsFiles or directories to save
expire_inRetention time
whenon_success, on_failure, always

Rules Conditions

ConditionMeaning
if: $CI_COMMIT_BRANCH == "main"Branch match
if: $CI_COMMIT_TAGTag pipeline
if: $CI_PIPELINE_SOURCE == "merge_request_event"MR event
when: neverNever run

Best Practices

✅ Do This:

stages:
  - build
  - test
  • ✅ Define the stage order explicitly
artifacts:
  paths:
    - dist/
  • ✅ Pass artifacts to subsequent jobs
rules:
  - if: $CI_COMMIT_BRANCH == "main"
  • ✅ Gate deployment by branch

❌ Don’t Do This:

# Omit the stage keyword
<!-- Jobs go to "test" by default -->               # ❌
# Hardcode secrets in .gitlab-ci.yml
<!-- Use protected variables -->                     # ❌
# Rely on the default stages without checking
<!-- The default is build, test, deploy -->          # ❌

Common Pitfalls

PitfallWhy It HappensFix
Pipeline fails to createYAML syntax errorUse the pipeline editor
Job in wrong stagestage omittedAdd stage keyword
Artifact not availableartifacts not setAdd artifacts.paths
Job not runningrules excludes itCheck the if condition
Variable emptyNot defined or protectedCheck project/group settings

Real-World Examples

1. Minimal Job

job:
  script:
    - echo "Hello"

2. Staged Pipeline

stages:
  - build
  - test
build:
  stage: build
  script: make
test:
  stage: test
  script: make test

3. Image

build:
  image: node:20
  script: npm run build

4. Artifacts

build:
  script: make build
  artifacts:
    paths:
      - dist/

5. Global Variable

variables:
  NODE_ENV: production

6. Job Variable

job:
  variables:
    NODE_ENV: staging

7. Rule for Main

deploy:
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

8. Rule for Tag

deploy:
  rules:
    - if: $CI_COMMIT_TAG

9. Needs

test:
  needs: [build]

10. Validate in Editor

Build → Pipeline editor

Visual

Pipeline Structure

┌──────────────────────────────────────────────┐
│  .gitlab-ci.yml                              │
│                                              │
│  stages: [build, test, deploy]               │
│                                              │
│  build-job (stage: build)                    │
│    ├── image: node:20                        │
│    ├── script: npm run build                 │
│    └── artifacts: dist/                      │
│                                              │
│  test-job (stage: test)                      │
│    ├── needs: [build-job]                    │
│    └── script: npm test                      │
│                                              │
│  deploy-job (stage: deploy)                  │
│    ├── rules: main branch only               │
│    └── script: ./deploy.sh                   │
└──────────────────────────────────────────────┘

Stage Flow

┌──────────────────────────────────────────────┐
│  build-job                                   │
│  Produces artifacts (dist/)                  │
└──────────────────┬───────────────────────────┘
                   │ artifacts
                   ▼
┌──────────────────────────────────────────────┐
│  test-job                                    │
│  Receives artifacts                          │
└──────────────────┬───────────────────────────┘
                   │ needs
                   ▼
┌──────────────────────────────────────────────┐
│  deploy-job (main only)                      │
└──────────────────────────────────────────────┘

Summary

ItemValue
File locationRepository root
File name.gitlab-ci.yml
Mandatory keywordscript
Default stagesbuild, test, deploy
Default stagetest (if omitted)
Artifact retention30 days (default)
Rules fallbackwhen: never
ValidationPipeline editor

Key takeaways:

  • The .gitlab-ci.yml is the pipeline definition. It must be at the repository root, and it must be valid YAML. A syntax error prevents the pipeline from being created .
  • The script is mandatory. A job without a script is invalid. The stage keyword assigns the job to a stage; if omitted, the job goes to test .
  • Artifacts pass files between jobs. The artifacts keyword saves files from a job. The needs or dependencies keyword controls which jobs receive them .
  • Rules control execution. The rules keyword evaluates conditions. A job with no matching rule does not run. The when: never fallback excludes the job for unmatched branches.
  • The pipeline editor validates. The CI Lint tool in the editor catches syntax errors before the commit.

Remember: The .gitlab-ci.yml is the configuration. The pipeline is the execution. The stages are the order. The jobs are the work. The artifacts are the handoff. The rules are the gate. Write the file, validate it, commit it, and check the pipeline. The file is the only artifact that matters.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!