Building CI/CD Pipelines with GitLab
A comprehensive guide to designing and implementing automated deployment pipelines using GitLab CI/CD, featuring a real-world example with caching, artifacts, and GitLab Pages deployment.
Introduction
GitLab CI/CD is a powerful tool for automating your software development lifecycle. In this guide, I'll walk through a production-ready CI/CD pipeline I built that demonstrates key concepts like Docker build images, caching strategies, artifact management, and automated deployments.
Example Project: gitlab-cicd-pipeline-example
Live Demo: https://gitlab-cicd-pipeline-example-c2a1fe.gitlab.io/
Pipeline Architecture
The pipeline follows a classic three-stage design:
Build → Test → Deploy
Each stage has a specific purpose and builds upon the previous stage's outputs. Let's explore how each stage works.
Stage 1: Build
The build stage is responsible for installing dependencies and compiling the application:
build-job:
stage: build
image: node:18
script:
- echo "=== Build Stage ==="
- echo "Installing dependencies (Jest and other packages)..."
- time npm install
- du -sh node_modules/ || echo "node_modules created"
- echo "Building the project..."
- npm run build
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
policy: push
artifacts:
paths:
- public/
expire_in: 1 hour
only:
- main
- merge_requests
Key Design Decisions:
- Docker Image: Uses
node:18for consistency across all environments - Cache Policy:
pushto savenode_modules/for downstream jobs - Artifacts: Stores the built
public/directory for 1 hour - Branch Rules: Runs on
mainbranch andmerge requests
The build stage creates the foundation for subsequent stages by caching dependencies and producing build artifacts.
Stage 2: Test
The test stage validates code quality and generates coverage reports:
test-job:
stage: test
image: node:18
script:
- echo "=== Test Stage ==="
- echo "Checking if node_modules cache was restored..."
- |
if [ -d "node_modules" ]; then
echo "✅ SUCCESS: node_modules cache was restored!"
du -sh node_modules/
ls -lh node_modules/.bin/jest
else
echo "❌ WARNING: node_modules cache not found"
fi
- npm test
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
policy: pull
coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
paths:
- coverage/
expire_in: 1 week
only:
- main
- merge_requests
Key Design Decisions:
- Cache Policy:
pullonly - reuses cached dependencies without updating them - Coverage Extraction: Uses regex pattern to extract coverage percentage
- Coverage Artifacts: Stores coverage reports in Cobertura format for GitLab integration
- Verification Logic: Checks if cache was successfully restored before running tests
This demonstrates a critical optimization: the test job avoids re-downloading ~50MB of Jest dependencies by pulling from cache.
Stage 3: Deploy
The deploy stage publishes the site to GitLab Pages:
pages:
stage: deploy
image: node:18
script:
- echo "=== Deploy Stage ==="
- echo "Deploying to GitLab Pages..."
- ls -la public/
artifacts:
paths:
- public
only:
- main
Key Design Decisions:
- Job Name: Must be named
pagesfor GitLab Pages deployment - Artifacts: Reuses
public/directory from build stage - Branch Protection: Only deploys from
mainbranch - Minimal Logic: Simple deployment since build artifacts are already prepared
Pipeline Design Patterns
Global Cache Configuration
The pipeline uses a global cache to share node_modules/ across jobs:
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
Why This Matters: Without caching, each job would download dependencies independently, wasting bandwidth and time. The cache key uses ${CI_COMMIT_REF_SLUG} to create separate caches per branch.
Cache Policy Strategy
Different stages use different cache policies:
- Build:
policy: push- creates/updates the cache - Test:
policy: pull- only reads from cache (faster) - Deploy: No cache needed (uses artifacts instead)
This optimization ensures the test stage runs faster by skipping cache updates.
Artifact Management
Build outputs are passed between stages using artifacts:
artifacts:
paths:
- public/
expire_in: 1 hour
The expire_in directive prevents artifact storage from growing indefinitely, saving CI minutes and storage costs.
CI/CD Best Practices Demonstrated
- Docker Image Pinning: Use specific versions (
node:18) instead oflatestfor reproducibility - Branch Rules: Deploy only from protected branches to prevent accidental deployments
- Fail Fast: Test stage runs quickly using cached dependencies
- Artifact Expiration: Set appropriate expiration times to manage storage
- Cache Verification: Test stage validates cache restoration before proceeding
- Coverage Reporting: Integrate code coverage into GitLab's UI using Cobertura format
Pipeline Variables
GitLab provides built-in variables that make pipelines dynamic:
${CI_COMMIT_REF_SLUG}: Sanitized branch name, perfect for cache keys- Jobs execute in isolated Docker containers with clean environments
Monitoring Pipeline Performance
When reviewing your pipeline, check:
- Cache Hit Rate: Verify the test stage successfully pulls cached dependencies
- Stage Duration: Identify bottlenecks in build, test, or deploy stages
- Artifact Size: Monitor storage consumption
- Coverage Trends: Track code coverage over time
Extending the Pipeline
Adding Security Scanning
stages:
- build
- test
- security
- deploy
security-scan:
stage: security
image: node:18
script:
- npm audit
allow_failure: true
only:
- main
- merge_requests
Multi-Environment Deployments
GitLab environments allow you to track deployments across different stages (development, staging, production) with built-in monitoring and rollback capabilities.
deploy-staging:
stage: deploy
script:
- echo "Deploy to staging"
environment:
name: staging
url: https://staging.example.com
only:
- develop
deploy-production:
stage: deploy
script:
- echo "Deploy to production"
environment:
name: production
url: https://production.example.com
only:
- main
Conclusion
This pipeline demonstrates how to build a robust CI/CD workflow with GitLab. The key takeaways:
- Caching dramatically speeds up pipeline execution
- Artifacts enable passing build outputs between stages
- Branch rules provide deployment safety
- Docker containers ensure consistent environments
- Coverage reporting integrates testing into your workflow
Check out the full implementation to see these concepts in action.