All the concepts from 19 episodes are woven into one real production pipeline. You'll follow the flow from feature commit, multi-arch build, staging deploy, manual approval gates, to production canary with automatic alerts, closing with a production readiness checklist.

Over 19 episodes we built capabilities one by one: rules, Docker, artifacts, security, environments, the Kubernetes agent, and progressive delivery. Now it's time for the final exam: wiring it all into a single pipeline that runs in the real world. This episode walks through an end-to-end production flow for the my-app application — from the first feature commit to the release alert in Slack.
The complete flow: a feature commit passes lint, unit tests, SAST, secret detection, and a review app; merging to main triggers a parent-child pipeline for a multi-arch build with Kaniko and a Trivy scan; the staging deploy is tested with integration and Cypress E2E tests; manual approval in a protected environment opens the road to production; then a 10 percent canary, health check, full promotion, review app cleanup, and an alert close the flow.
The main .gitlab-ci.yml file at the repository root:
include:
- template: Jobs/SAST.gitlab-ci.yml
- template: Jobs/Secret-Detection.gitlab-ci.yml
stages:
- test
- review
- build
- staging
- approval
- production
- cleanup
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
variables:
IMAGE_TAG: $CI_COMMIT_SHAThe include block pulls in GitLab's official SAST and Secret Detection templates. workflow prevents duplicate pipelines for pushes and merge requests.
Every push to a feature branch runs the quality layer:
lint:
stage: test
image: node:22-alpine
script:
- npm ci
- npm run lint
unit_test:
stage: test
image: node:22-alpine
script:
- npm ci
- npm testThe SAST and Secret Detection templates generate the sast and secret_detection jobs automatically. Both are set with allow_failure: false via additional rules, so code vulnerabilities immediately block the merge request.
Every merge request gets a temporary environment:
review_app:
stage: review
image: alpine/helm:latest
variables:
KUBE_CONTEXT: my-group/my-app:prod-agent
KUBE_NAMESPACE: review-$CI_MERGE_REQUEST_IID
script:
- helm upgrade --install review-$CI_MERGE_REQUEST_IID ./chart
--set image.tag=$IMAGE_TAG
environment:
name: review/$CI_MERGE_REQUEST_IID
url: https://review-$CI_MERGE_REQUEST_IID.example.com
on_stop: stop_review_appon_stop links the temporary environment to the cleanup job. Once the MR is merged, GitLab triggers stop_review_app to delete the release.
After the MR is approved and merged, the job below triggers a child pipeline in child/build-ci.yml:
build_images:
stage: build
trigger:
include: child/build-ci.yml
strategy: depend
rules:
- if: $CI_COMMIT_BRANCH == "main"The child pipeline contains the multi-arch build and container scan:
stages:
- build
- scan
kaniko_build:
stage: build
image: gcr.io/kaniko-project/executor:v1.23.2-debug
script:
- /kaniko/executor
--context $CI_PROJECT_DIR
--dockerfile $CI_PROJECT_DIR/Dockerfile
--destination $CI_REGISTRY_IMAGE:${CI_COMMIT_SHA}
--destination $CI_REGISTRY_IMAGE:latest
trivy_scan:
stage: scan
image: aquasec/trivy:0.58.2
script:
- trivy image --exit-code 1 --severity CRITICAL $CI_REGISTRY_IMAGE:${CI_COMMIT_SHA}strategy: depend makes the parent wait for the child to finish — deploy only runs if the build and scan succeed.
deploy_staging:
stage: staging
image: alpine/helm:latest
variables:
KUBE_CONTEXT: my-group/my-app:prod-agent
KUBE_NAMESPACE: staging
script:
- helm upgrade --install my-app ./chart
--set image.tag=$IMAGE_TAG
environment:
name: staging
url: https://staging.example.com
e2e_test:
stage: staging
image: cypress/included:13.6.0
script:
- npm ci
- npm run test:integration
- npm run test:e2eThe E2E job uses the official Cypress image that already includes a browser — nothing to install. Integration and E2E tests exercise the application against real data after the staging deploy finishes.
Compliance demands human approval before production:
approve_production:
stage: approval
script:
- echo "Menunggu persetujuan lead security dan release manager"
when: manual
environment:
name: production
rules:
- if: $CI_COMMIT_BRANCH == "main"The production environment is set up as a protected environment in Settings, so only users with allowed roles can trigger or approve. This job is the gate: without a manual click, jobs in the production stage will never run.
Warning
when: manual without a protected environment is just a formality — anyone with project access can click. Make sure the production environment is registered as protected and only maintainers or release managers are on the access list.
Once approved, the release flows in two steps:
deploy_canary:
stage: production
image: alpine/helm:latest
variables:
KUBE_CONTEXT: my-group/my-app:prod-agent
KUBE_NAMESPACE: production
script:
- helm upgrade --install my-app ./chart
--set canary.weight=10
--set image.tag=$IMAGE_TAG
environment:
name: production/canary
url: https://app.example.com
promote_production:
stage: production
image: alpine/helm:latest
variables:
KUBE_CONTEXT: my-group/my-app:prod-agent
KUBE_NAMESPACE: production
script:
- ./scripts/healthcheck.sh
- helm upgrade --install my-app ./chart
--set canary.weight=100
--set image.tag=$IMAGE_TAG
environment:
name: production
url: https://app.example.comThe healthcheck.sh script checks Prometheus metrics (exactly the episode 17 pattern) and performs an automatic rollback if the error rate spikes. Only after the 10 percent canary is proven healthy is full traffic released.
The pipeline closes with two final jobs:
stop_review_app:
stage: cleanup
image: alpine/helm:latest
variables:
KUBE_CONTEXT: my-group/my-app:prod-agent
script:
- helm uninstall review-$CI_MERGE_REQUEST_IID
environment:
name: review/$CI_MERGE_REQUEST_IID
action: stop
notify_release:
stage: cleanup
image: alpine:latest
script:
- curl -s -X POST -H 'Content-type: application/json'
--data "{\"text\":\"Release ${CI_COMMIT_SHA} sukses ke production\"}"
https://hooks.slack.com/services/T000/B000/XXXXXXXX
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: alwaysThe Slack webhook URL can be swapped for a Telegram bot endpoint for the same notification. With when: always, the alert still goes out even if a previous job failed — the team always knows the release status.
Before turning on a pipeline like this, make sure of the following:
workflow: rules prevents duplicate pipelines on push and merge requests.A complete production pipeline isn't a collection of jobs, but a layered system of trust: quality is tested, security is checked, humans supervise, and machines are ready to roll back mistakes.
With this, the Learn GitLab CI/CD series is complete — you've built a full foundation from basic concepts to a production-grade pipeline. Keep practicing on real projects, and make this pipeline the starting point of your own architecture. See you in the next series!