Gitlab CI, Zero to Hero
.gitlab-ci.yml file is the entry point of GitLab CI/CD, must be placed in your repository root.
Job, Stage, Pipeline
Job: Individual tasks that run commands, build blocks of Gitlab CI pipeline.
Pipeline: The top-level component containing all jobs. However there is no way to directly define a pipeline, user can only define jobs, Gitlab constructs pipeline on:
- Which jobs match the trigger conditions
- How those jobs are related
Common Patterns for pipeline separation:
- by branch
- by pipeline source
- by variables
Stage: groups of jobs that run in sequence
Stages are a grouping mechanism for jobs They define execution sequence
GitLab have a default stages definition. Or you can define stages in .gitlab-ci.yml. GitLab just enforces the execution order you specify.
# If you omit the stages: declaration
# GitLab uses these DEFAULT stages:
# - .pre
# - build
# - test
# - deploy
# - .post
stages:
- prepare
- build
- test
- deploy
GitLab Runners
Gitlab runner is a software (agent) that execute your CI/CD jobs
Listener/worker, that constantly polls GitLab for jobs, execute job, report result.
Gitlab Runners are installed on VM/container/server/kubernetes Pod
Type of runners:
- Shared Runners: Available to all projects
- Group Runners: Available to all projects in a group
- Specific Runners: Dedicated to specific projects
Runners use Executors to run your job.
Executor Types:
- shell-job
- docker-job
- k8s-job
default is docker executor
each job runs in a brand new container. for the following reasons:
- isolation
- predictability
Pipeline Start
↓
[build-job]
├─ Pull node:16 image
├─ Create new container
├─ Run scripts
└─ Destroy container ❌
↓
[test-job]
├─ Pull node:16 image (cached)
├─ Create NEW container
├─ Run scripts (no files from build!)
└─ Destroy container ❌
↓
Pipeline End
Sharing data between jobs: artifacts
Gitlabe automatically clones your repository before any job.
stages:
- install
- build
- test
- deploy
install-deps:
stage: install
image: node:16
script:
- npm ci
- npm audit
artifacts:
paths:
- node_modules/
expire_in: 1 day
build-app:
stage: build
image: node:16
script:
- npm run build
dependencies:
- install-deps # Gets node_modules/
artifacts:
paths:
- dist/
- node_modules/
test-unit:
stage: test
image: node:16 # Fresh container!
script:
- npm test
dependencies:
- build-app # Gets dist/ and node_modules/
test-e2e:
stage: test
image: cypress/included:10 # Different image, fresh container
services:
- name: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
alias: app
script:
- cypress run --config baseUrl=http://app:3000
dependencies:
- build-app
deploy:
stage: deploy
image: alpine:latest # Fresh container
script:
- apk add --no-cache curl
- curl -X POST $DEPLOY_WEBHOOK
dependencies:
- build-app # Only needs dist/
Cache: Speed optimization (might exist, might not)
Artifacts: guaranteed data transfer between jobs
If Job B needs Job A → B gets A's artifacts If Job B doesn't reference A → B can NEVER get A's artifacts
Cache is a way to preserve files between pipeline runs to speed up the jobs. cache is about preserving data across different pipelines.
Basic Job Configuration
Script types:
- script: the only required field in a job. containing main job logic
- commands run in sequence
- if any command fails, job fails
- runs in the project directory
- has access to all CI variables
- before_script: run before the main script, Perfect for environment setup
- after_script: run after the main script, even if the job fails
why we need three scripts:
- separation of concerns
- guaranteed cleanup
- error handling simplicity
Configurations control when jobs run
-
only/except: legacy, but still in use, specify the branch
-
when: specify the execution timing, can be used standalone or within rules
- on_success: run if all previous succeeded
-
rules (recomended):
- changes: File Changes:
- exists: File existence
- allow_failure: in rules
- variables: Set variables in rules
-
trigger: use to start a completely separate pipeline. either in the same project or a different project
job:
rules:
# Branch conditions
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_COMMIT_BRANCH != "main"'
- if: '$CI_COMMIT_BRANCH =~ /^feature-/'
# Tag conditions
- if: '$CI_COMMIT_TAG'
- if: '$CI_COMMIT_TAG =~ /^v\d+/'
# Pipeline source
- if: '$CI_PIPELINE_SOURCE == "push"'
- if: '$CI_PIPELINE_SOURCE == "web"'
- if: '$CI_PIPELINE_SOURCE == "schedule"'
# Complex conditions
- if: '$CI_COMMIT_BRANCH == "main" && $DEPLOY_ENABLED == "true"'
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main" || $CI_MERGE_REQUEST_LABELS =~ /urgent/'
Keywords:
stage: image: specify which docker image to use services: Helper containers variables: environment variables artifacts: save job output dependencies: control artifact flow needs: DAG (Directed Acyclic Graph) rules: smart conditions (better than only / except) when: execution conditions cache: speed up pipelines retry: handle flaky jobs timeout: prevent hanging allow_failure: non-blocking failures coverage: extract coverage environment: track deployments parallel: multiple instances trigger: pipeline control only
tags
Job Status Types:
pending/running/success/failed/skipped/manual
failed: script returned non-zero exit code skipped means the didn't meet conditions manual: waiting for manual trigger
CI/CD variables:
key-value pairs available during job execution. Environment variables on steroids
- Predefined variables (GitLab Provides)
- CI_PROJECT_NAME
- CI_COMMIT_BRANCH
- CI_COMMIT_SHA
- CI_COMMIT_MESSAGE
- CI_PIPELINE_ID
- CI_PIPELINE_SOURCE
- CI_JOB_ID
- CI_JOB_NAME
- CI_RUNNER_ID
- CI_RUNNER_TAGS
- CI_PROJECT_URL
- CI_PIPELINE_URL
- CI_JOB_URL
deploy:
script:
# Different behavior for different triggers
- |
if [ "$CI_PIPELINE_SOURCE" == "merge_request_event" ]; then
echo "This is a merge request"
fi
# Use commit info
- docker build -t myapp:$CI_COMMIT_SHORT_SHA .
# Conditional logic
- |
if [ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]; then
echo "On default branch (usually main)"
fi
Variables can be defined at multiple levels, Higher level wins.
Job -> Pipeline -> Project -> Group -> Instance -> Predefined
- protected variables
- masked variables
- file variables
- variable expansion
build:
variables:
VERSION: "1.0.0"
IMAGE_TAG: "myapp:$VERSION" # Expands to myapp:1.0.0
Conditional variables
deploy:
script:
- echo "Deploying to $ENVIRONMENT"
rules:
- if: $CI_COMMIT_BRANCH == "main"
variables:
ENVIRONMENT: production
REPLICAS: "5"
- if: $CI_COMMIT_BRANCH == "develop"
variables:
ENVIRONMENT: staging
REPLICAS: "2"
Dynamic variables
build:
script:
# Create variables during job
- export BUILD_TIME=$(date +%Y%m%d-%H%M%S)
- echo "VERSION=$CI_COMMIT_SHORT_SHA-$BUILD_TIME" >> build.env
artifacts:
reports:
dotenv: build.env # Pass to next jobs
test:
needs: [build]
script:
- echo "Testing version: $VERSION" # From build.env
Needs
let you create DAG instead of rigid stage-based pipelines.