Evolve your CI/CD pipeline. Covers maturity levels from manual builds to fully automated deployment, pipeline optimization, testing strategies, release management, and developer experience.
A CI/CD Pipeline Maturity Model is a framework for evaluating and evolving a team’s continuous integration and continuous delivery practices. It guides teams from ad-hoc builds and manual deployments to fully automated, self-healing, observability-driven pipelines that scale across multiple services and environments. Use this model when assessing pipeline health, planning incremental improvements, or aligning engineering practices across teams.
The first stage of maturity is a pipeline that reliably builds code and runs tests on every change. This is where teams move from local development to shared, repeatable processes.
Every commit to main triggers a job that:
git clone --depth=1 --branch=main https://git.example.com/myapp.gitnpm ci (not npm install), which uses package-lock.json exactly.npm test -- --coverage --watch=false.--reporters=junit.A job failure is signaled by a non-zero exit code from the test runner. The pipeline logs include:
npm test -- --coverage --watch=false
> [email protected] test /app
> jest --coverage --watch=false
FAIL src/auth/login.test.ts
● Login with valid credentials
Error: Timeout of 5000ms exceeded. Ensure the callback is invoked in the test.
✕ Login with valid credentials (5003ms)
● Login with invalid credentials
Error: Expected 401, got 400
✕ Login with invalid credentials (123ms)
✓ Login with empty credentials (57ms)
Test Suites: 2 failed, 3 passed, 5 total
Tests: 5 failed, 12 passed, 17 total
Snapshots: 0 total
Time: 5.8s
Ran all test suites.
The pipeline stores artifacts using tar cfz build.tar.gz dist/ coverage/. These are archived and linked to the build, with the artifacts directory containing test-results.xml, coverage/lcov.info, and dist/app.js.
With a reliable build, the next step is automatic deployment to staging. This requires a configuration layer and a deployment mechanism.
The pipeline deploys using kubectl apply -f manifests/staging/ and waits for all pods to be ready:
kubectl rollout status deployment/myapp --timeout=60s
If the rollout fails, it logs:
error: rollout status timed out after 60s
waiting for deployment myapp to rollout
Deployment is accompanied by a deploy.yaml file in the repository that defines:
pipeline:
stages:
- name: deploy-staging
steps:
- run: kubectl apply -f manifests/staging/
- run: kubectl rollout status deployment/myapp --timeout=60s
- run: curl -s -f http://staging.myapp.com/health
The health check is critical: it must respond with HTTP 200 and {"status": "ok"} within 5 seconds. If not, the pipeline fails and rolls back:
kubectl rollout undo deployment/myapp
Rollback is triggered by a rollback.sh script that:
kubectl rollout history deployment/myapp)DEPLOYED_VERSION file in the repository-d '{"message": "Rollback triggered", "version": "v1.2.3"}' \ https://webhook.example.com/deployments
## Feature-Branch Driven Development
At this stage, teams use feature branches with pull requests. The pipeline now integrates with the pull request lifecycle.
When a PR is opened:
- The pipeline runs `npm test`, `npm run lint`, and `docker build --tag myapp:pr-$PR_NUM .`
- The build artifact is uploaded to a temporary registry (`docker push myapp:pr-$PR_NUM`)
- A deployment to a preview environment is triggered:
```bash
kubectl apply -f manifests/preview.yaml --namespace=preview-$PR_NUM
The preview URL is generated from the branch name:
echo "https://preview-$PR_NUM.myapp.com"
A PR_STATUS file is created:
{
"branch": "feature/user-profile",
"commit": "a1b2c3d",
"status": "success",
"artifacts": {
"docker": "myapp:pr-42",
"preview_url": "https://preview-42.myapp.com",
"test_results": "https://artifacts.example.com/pr-42/test-results.xml"
}
}
The pipeline fails if:
npm run lint returns non-zero exit codedocker build fails with ERROR: failed to solve: failed to create build cache: no such file or directorykubectl apply returns Error from server (NotFound): service "myapp" not foundThe pipeline does not fail silently: each step logs its command, stdout, and stderr, with output like:
Running: docker build --tag myapp:pr-42 .
Sending build context to Docker daemon 28.7MB
Step 1/4 : FROM node:18-alpine
...
Step 4/4 : COPY . .
---> Using cache
---> 3f4e5d6c7b8a
Successfully built 3f4e5d6c7b8a
Successfully tagged myapp:pr-42
The pipeline now supports canary deployments, where new versions are rolled out gradually.
The deployment job includes:
- name: deploy-canary
steps:
- run: kubectl apply -f manifests/canary.yaml
- run: kubectl rollout status deployment/myapp-canary --timeout=30s
- run: |
echo "Shifting 10% traffic to canary"
kubectl patch service/myapp \
-p '{"spec":{"selector":{"version":"canary"}}}' \
--type=merge
- run: |
echo "Wait 5 minutes for canary to stabilize"
sleep 300
- run: |
echo "Check metrics"
curl -f http://metrics.example.com/api/v1/query?query=deployments%7Bjob%3D%22myapp%22%2Cphase%3D%22canary%22%7D
Traffic shifting is done by patching the myapp service to route 10% of traffic to pods with version: canary. The pipeline waits for a health check and metrics.
The canary job fails if:
kubectl rollout status times outcurl -f returns 404 or 500metrics.example.com returns an empty resultcanary.yaml file is malformed, causing Error: unable to decode: json: cannot unmarshal string into Go structThe pipeline updates the canary-status.json file:
{
"canary_version": "v1.3.0",
"traffic_percentage": 10,
"deployment_started": "2023-08-17T10:00:00Z",
"status": "progressing",
"metrics": {
"error_rate": 0.02,
"latency_p95": 450
}
}
At the highest maturity, the pipeline is self-healing and observability-driven.
The pipeline monitors the production deployment:
- name: monitor-production
steps:
- run: |
while true; do
curl -s http://prod.myapp.com/health | jq -e '.status == "ok"'
if [ $? -ne 0 ]; then
echo "Health check failed. Triggering rollback."
kubectl rollout undo deployment/myapp --to-revision=$(kubectl rollout history deployment/myapp --output=json | jq -r '.[0].revision')
break
fi
sleep 30
done
The pipeline checks for:
curl -s http://metrics.example.com/api/v1/query?query=rate%28http_errors%7Bjob%3D%22myapp%22%7D%5B1m%5D%29 > error_rate.txtcurl -s http://metrics.example.com/api/v1/query?query=histogram_quantile%280.95%2Csum%28rate%28http_request_duration_seconds_bucket%7Bjob%3D%22myapp%22%7D%5B5m%5D%29%29%29 > latency_p95.txtIf error rate exceeds 0.01 or latency p95 exceeds 500ms, the pipeline triggers a canary rollout with increased traffic:
kubectl patch service/myapp \
-p '{"spec":{"selector":{"version":"canary"}}}' \
--type=merge
The pipeline records deployment events in a deployments.log file:
2023-08-17T10:05:00Z: Deployed v1.3.0 to production
2023-08-17T10:05:00Z: Traffic shifted to canary (10%)
2023-08-17T10:10:00Z: Canaries stable. Increasing traffic to 30%
2023-08-17T10:15:00Z: Canaries stable. Increasing traffic to 60%
2023-08-17T10:25:00Z: Canaries stable. Promoting to production
2023-08-17T10:25:00Z: Finalizing deployment
The pipeline handles failure modes:
kubectl rollout status times out, it logs the rollout history and diffs the deployment spec before and after.curl -f fails, it saves the raw response body to health-check-failure.json.jq -e returns non-zero, the pipeline logs jq: error: expected boolean, but got string.The pipeline’s success hinges on subtle but critical details:
npm ci uses package-lock.json, but npm install does not, leading to inconsistent dependencies between local and CI environments.kubectl rollout status with --timeout=60s may miss a slow rollout if the deployment takes 61 seconds. A better approach is to loop until readiness.curl -f fails on 4xx or 5xx, but not on {"status": "error"}. The pipeline must validate the JSON body as well.kubectl rollout undo without --to-revision may rollback to the wrong version, especially in fast-paced environments.artifacts.tar.gz is uploaded and available for each build, with a consistent naming scheme.test-results.xml, coverage/lcov.info, deployments.log, canary-status.json.A mature CI/CD pipeline is not a sequence of jobs—it is a living system that evolves with the code, infrastructure, and team.
This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy