Your CI/CD pipeline is taking too long (45+ minutes) and failing frequently. The team is frustrated with slow feedback, and deployments are delayed. You need to optimize the pipeline for speed and reliability.
# .gitlab-ci.yml (simplified)
stages:
- build
- test
- deploy
build:
stage: build
script:
- npm install
- npm run build
artifacts:
paths:
- dist/
test:
stage: test
script:
- npm install # Installing again!
- npm test
- npm run lint
- npm run e2e
deploy:
stage: deploy
script:
- kubectl apply -f k8s/
only:
- main
npm install runs in both build and test stagesProblem: Dependencies installed every time
Solution:
# Cache node_modules
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .npm/
build:
stage: build
script:
- npm ci # Faster, uses package-lock.json
- npm run build
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
policy: pull-push
test:
stage: test
script:
- npm ci # Uses cache if available
- npm test
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
policy: pull
Benefits:
Problem: Tests run sequentially
Solution:
test:unit:
stage: test
script:
- npm ci
- npm run test:unit
parallel:
matrix:
- TEST_SUITE: [unit, integration, e2e]
# Or separate jobs
test:unit:
stage: test
script:
- npm ci
- npm run test:unit
test:integration:
stage: test
script:
- npm ci
- npm run test:integration
test:e2e:
stage: test
script:
- npm ci
- npm run test:e2e
only:
- main
- merge_requests
Benefits:
Problem: Building Docker images from scratch
Solution:
build:docker:
stage: build
script:
- docker build
--cache-from $CI_REGISTRY_IMAGE:latest
--cache-from $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
-t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
-t $CI_REGISTRY_IMAGE:latest
.
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
Benefits:
Problem: Running all tests for every change
Solution:
test:unit:
stage: test
script:
- npm ci
- npm run test:unit
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request"
- if: $CI_COMMIT_BRANCH == "main"
- changes:
- "src/**/*"
- "*.js"
- "package.json"
test:e2e:
stage: test
script:
- npm ci
- npm run test:e2e
rules:
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_PIPELINE_SOURCE == "merge_request" && $E2E_TESTS == "true"
allow_failure: true # Don't block on E2E failures initially
Benefits:
Problem: Large artifacts slow down pipeline
Solution:
build:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
exclude:
- node_modules/ # Don't include dependencies
expire_in: 1 week
reports:
# Only include test results, not full output
junit: test-results.xml
Benefits:
Problem: Shared runners are slow
Solution:
build:
stage: build
tags:
- docker
- linux
script:
- npm ci
- npm run build
Benefits:
Problem: Monolithic pipeline for all services
Solution:
# Per-service pipelines
# Each service has own .gitlab-ci.yml
# Triggered by changes to that service
build:service-a:
stage: build
script:
- cd services/service-a
- npm ci
- npm run build
only:
changes:
- services/service-a/**/*
Benefits:
Problem: Running same tests repeatedly
Solution:
test:
stage: test
script:
- npm ci
- |
if [ -f test-results-cache.json ]; then
echo "Using cached test results"
else
npm test -- --reporter json > test-results.json
fi
cache:
key: test-results-${CI_COMMIT_REF_SLUG}
paths:
- test-results-cache.json
stages:
- build
- test
- deploy
variables:
NPM_CONFIG_CACHE: .npm
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .npm/
build:
stage: build
image: node:18
script:
- npm ci --cache .npm --prefer-offline
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .npm/
policy: pull-push
test:unit:
stage: test
image: node:18
script:
- npm ci --cache .npm --prefer-offline
- npm run test:unit -- --reporter junit --output-file test-results.xml
artifacts:
reports:
junit: test-results.xml
expire_in: 1 week
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
policy: pull
parallel: 2 # Run 2 parallel jobs
test:integration:
stage: test
image: node:18
script:
- npm ci --cache .npm --prefer-offline
- npm run test:integration
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
policy: pull
rules:
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_PIPELINE_SOURCE == "merge_request"
test:e2e:
stage: test
image: cypress/browsers:latest
script:
- npm ci
- npm run test:e2e
rules:
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_PIPELINE_SOURCE == "merge_request" && $E2E_TESTS == "true"
allow_failure: true
when: manual # Manual trigger for E2E
build:docker:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build
--cache-from $CI_REGISTRY_IMAGE:latest
--cache-from $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
-t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
-t $CI_REGISTRY_IMAGE:latest
.
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
dependencies:
- build
only:
- main
- tags
deploy:staging:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl config use-context staging
- kubectl set image deployment/web-app web-app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- kubectl rollout status deployment/web-app
environment:
name: staging
url: https://staging.example.com
only:
- main
deploy:production:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl config use-context production
- kubectl set image deployment/web-app web-app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- kubectl rollout status deployment/web-app
environment:
name: production
url: https://example.com
when: manual
only:
- main
# Add to pipeline
pipeline:metrics:
stage: .post
script:
- echo "Pipeline duration: $CI_PIPELINE_DURATION"
- echo "Job duration: $CI_JOB_DURATION"
when: always