Skip to main content

GitLab CI

GitLab CI is GitLab's integrated CI/CD system defined in a .gitlab-ci.yml file at the repository root. It runs jobs on registered runners, tracks deployments to named environments, and integrates tightly with GitLab's merge request workflow for inline pipeline status.

Understanding GitLab CI

What Is GitLab CI in Simple Terms

GitLab CI is the CI/CD system built into GitLab. Like GitHub Actions for GitHub, it requires no external tools — define your pipeline in a YAML file, push it, and GitLab runs it. It is particularly strong at environment tracking (showing which version is deployed where across all your environments) and merge request integration (pipeline status shown inline on every MR).

Organisations running self-hosted GitLab for compliance — common in Indian financial services and regulated industries — use GitLab CI because it stays entirely within their infrastructure.

How It Works

◈ DIAGRAM
+------------------------------------------+
| .gitlab-ci.yml in repository root |
+------------------------------------------+
|
push or MR event
|
v
+------------------------------------------+
| GitLab pipeline created |
| Jobs assigned to stages |
| Dependency graph (DAG) resolved |
+------------------------------------------+
|
v
+------------------------------------------+
| Runner picks up job from queue |
| |
| Shared runner (GitLab SaaS) |
| OR |
| Specific runner (your infrastructure) |
+------------------------------------------+
|
v
+------------------------------------------+
| Job executes in Docker container |
| Results: artifacts, reports, logs |
| Environment deployment tracked |
+------------------------------------------+

Complete .gitlab-ci.yml:

YAML
## .gitlab-ci.yml
## Define the order of stages
stages:
- build
- test
- scan
- deploy
## Variables available to all jobs
variables:
IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
DOCKER_DRIVER: overlay2
## Reusable configuration with YAML anchors
.node-base: &node-base
image: node:20-alpine
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- node_modules/
build-image:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $IMAGE_NAME .
- docker push $IMAGE_NAME
unit-tests:
stage: test
<<: *node-base ## merge node-base config
script:
- npm ci
- npm test -- --coverage --reporter=junit
## Publish JUnit test results to GitLab
artifacts:
reports:
junit: junit.xml
paths:
- coverage/
expire_in: 1 week
security-scan:
stage: scan
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy image --exit-code 1 --severity HIGH,CRITICAL $IMAGE_NAME
deploy-staging:
stage: deploy
script:
- kubectl set image deployment/payment-api payment-api=$IMAGE_NAME -n staging
environment:
name: staging
url: https://staging.payment.internal
## Rules control when this job runs
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
deploy-production:
stage: deploy
script:
- kubectl set image deployment/payment-api payment-api=$IMAGE_NAME -n production
environment:
name: production
url: https://api.payment.razorpay.com
## Manual trigger only on main branch
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
needs:
- deploy-staging ## must deploy to staging first

Practical Commands

Bash
## GitLab CLI -- view pipeline status
glab ci view
glab ci status
## View specific job logs
glab ci trace JOB_ID
## Retry a failed job
glab ci retry JOB_ID
## Cancel a pipeline
glab ci cancel PIPELINE_ID
## Trigger a pipeline via API
curl -X POST \
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"https://gitlab.com/api/v4/projects/PROJECT_ID/pipeline" \
-d "ref=main"

Troubleshooting

Symptom Check What to Look For
Job stuck pending Runner availability No runners with matching tags
YAML syntax error CI Lint tool in GitLab UI Indentation and key names
Rules not matching rules:if conditions Variable names and values
Cache not working Cache key and paths Key must be consistent across runs
Tip

Use GitLab's built-in CI Lint tool (Settings > CI/CD > CI Lint) to validate your .gitlab-ci.yml before pushing. It catches YAML syntax errors, invalid keywords, and rule logic errors — much faster than push-and-see.

Remember

GitLab CI predefined variables are your friends. $CI_COMMIT_SHA is the full commit hash. $CI_COMMIT_SHORT_SHA is the short version. $CI_REGISTRY_IMAGE is the full path to your project's container registry. $CI_ENVIRONMENT_NAME is the name of the environment being deployed to. Use these instead of hardcoding values.

Security

Protect your production environment in GitLab (Settings > CI/CD > Environments > Edit > Protected). Protected environments restrict which branches can deploy and which users can manually trigger deployments — preventing a developer branch from accidentally deploying to production.

Common Mistake

Using when: always on deployment jobs. This deploys on every pipeline run regardless of branch, test results, or manual conditions. Use rules: blocks to explicitly define when deployment jobs should run — they should only run on specific branches (main, release/*) and after required stages pass.

Frequently Asked Questions

What's the practical benefit of GitLab CI's environments feature tying directly into merge requests?

Because GitLab owns both the source control and CI system, a merge request can show live deployment status, environment URLs, and even a 'Review App' link for a temporary environment spun up just for that branch — all inline without leaving the MR page. This tight coupling is harder to replicate with an external CI tool bolted onto a separately-hosted Git provider, where environment state has to be surfaced via API integration rather than being native.

What's a common mistake when structuring .gitlab-ci.yml stages and jobs?

Not using `rules:` or `only/except` conditions to scope jobs correctly causes every job to run on every pipeline trigger, including expensive deploy jobs firing on feature-branch pushes that should never deploy anywhere. A cleaner pattern is defining reusable job templates with `extends` or `!reference` tags and gating environment-specific jobs explicitly by branch or trigger type, keeping feature-branch pipelines fast and deploy-only jobs isolated to protected branches.