Full CI/CD Pipeline β Build β Scan β Package β Deploy
This is the complete GitLab CI pipeline that covers the entire deploy lifecycle: build the Docker image, scan it, package the Helm chart, push both to Harbor, then update the GitOps repo to trigger ArgoCD.
Pipeline Stagesβ
build β docker build + push image to Harbor
test β unit tests inside container
scan β Trivy vulnerability scan (fail on CRITICAL)
package β helm package + push chart to Harbor OCI
promote β bump chart version in gitops repo β ArgoCD takes over
Complete .gitlab-ci.ymlβ
# .gitlab-ci.yml
stages:
- build
- test
- scan
- package
- promote
variables:
HARBOR_URL: harbor.local
HARBOR_PROJECT: myteam
APP_NAME: myapp
CHART_PATH: ./chart
GITOPS_REPO: https://gitlab.local/platform/gitops-repo.git
IMAGE: $HARBOR_URL/$HARBOR_PROJECT/$APP_NAME
# ββ Stage 1: Build ββββββββββββββββββββββββββββββββββββββββββββββββββ
build-image:
stage: build
image: docker:24
services:
- docker:24-dind
before_script:
- docker login $HARBOR_URL -u $HARBOR_USER -p $HARBOR_PASSWORD
script:
- |
docker build \
--label "git.commit=$CI_COMMIT_SHA" \
--label "git.branch=$CI_COMMIT_BRANCH" \
--label "build.date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
-t $IMAGE:$CI_COMMIT_SHORT_SHA \
-t $IMAGE:latest-$CI_COMMIT_BRANCH \
.
- docker push $IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $IMAGE:latest-$CI_COMMIT_BRANCH
only:
- main
- /^release\/.*/
# ββ Stage 2: Test βββββββββββββββββββββββββββββββββββββββββββββββββββ
unit-tests:
stage: test
image: $IMAGE:$CI_COMMIT_SHORT_SHA
script:
- npm test # or pytest, go test, etc.
coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
artifacts:
reports:
junit: test-results/junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
# ββ Stage 3: Scan βββββββββββββββββββββββββββββββββββββββββββββββββββ
trivy-scan:
stage: scan
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy image \
--exit-code 1 \
--severity CRITICAL \
--no-progress \
--format table \
$IMAGE:$CI_COMMIT_SHORT_SHA
# Fail the pipeline if CRITICAL vulnerabilities are found
allow_failure: false
only:
- main
- /^release\/.*/
# ββ Stage 4: Package Helm Chart βββββββββββββββββββββββββββββββββββββ
package-chart:
stage: package
image: alpine/helm:3.14.0
before_script:
- helm registry login $HARBOR_URL -u $HARBOR_USER -p $HARBOR_PASSWORD
script:
# Set chart version = app version = semver from git tag or commit
- export CHART_VERSION=${CI_COMMIT_TAG:-"0.0.0-$CI_COMMIT_SHORT_SHA"}
- |
# Update Chart.yaml versions
sed -i "s/^version:.*/version: $CHART_VERSION/" $CHART_PATH/Chart.yaml
sed -i "s/^appVersion:.*/appVersion: \"$CHART_VERSION\"/" $CHART_PATH/Chart.yaml
# Update default image tag in values.yaml
sed -i "s/tag:.*/tag: \"$CI_COMMIT_SHORT_SHA\"/" $CHART_PATH/values.yaml
- helm dependency update $CHART_PATH
- helm package $CHART_PATH --destination ./dist
- helm push ./dist/$APP_NAME-$CHART_VERSION.tgz oci://$HARBOR_URL/charts
artifacts:
paths:
- dist/
only:
- main
- /^release\/.*/
# ββ Stage 5: Promote to GitOps Repo βββββββββββββββββββββββββββββββββ
promote-staging:
stage: promote
image: alpine/git:latest
before_script:
- git config --global user.email "ci@platform.local"
- git config --global user.name "GitLab CI"
script:
- export CHART_VERSION=${CI_COMMIT_TAG:-"0.0.0-$CI_COMMIT_SHORT_SHA"}
# Clone gitops repo
- git clone https://ci-token:$GITOPS_TOKEN@gitlab.local/platform/gitops-repo.git /tmp/gitops
- cd /tmp/gitops
# Bump chart version in staging values
- |
sed -i "s/targetRevision:.*/targetRevision: \"$CHART_VERSION\"/" \
apps/$APP_NAME/application-staging.yaml
- |
sed -i "s/tag:.*/tag: \"$CI_COMMIT_SHORT_SHA\"/" \
apps/$APP_NAME/values-staging.yaml
- git add .
- git commit -m "chore: bump $APP_NAME staging to $CHART_VERSION [skip ci]"
- git push origin main
only:
- main
environment:
name: staging
url: https://$APP_NAME.staging.yourdomain.com
promote-production:
stage: promote
image: alpine/git:latest
before_script:
- git config --global user.email "ci@platform.local"
- git config --global user.name "GitLab CI"
script:
- export CHART_VERSION=$CI_COMMIT_TAG
- git clone https://ci-token:$GITOPS_TOKEN@gitlab.local/platform/gitops-repo.git /tmp/gitops
- cd /tmp/gitops
- |
sed -i "s/targetRevision:.*/targetRevision: \"$CHART_VERSION\"/" \
apps/$APP_NAME/application-prod.yaml
- |
sed -i "s/tag:.*/tag: \"$CI_COMMIT_SHORT_SHA\"/" \
apps/$APP_NAME/values-prod.yaml
- git add .
- git commit -m "chore: bump $APP_NAME production to $CHART_VERSION [skip ci]"
- git push origin main
only:
- /^v\d+\.\d+\.\d+$/ # only on semver tags: v1.2.3
when: manual # human approval before production
environment:
name: production
url: https://$APP_NAME.yourdomain.com
Required CI/CD Variablesβ
Set these in GitLab β Settings β CI/CD β Variables:
| Variable | Value | Protected | Masked |
|---|---|---|---|
HARBOR_USER | ci-robot | β | β |
HARBOR_PASSWORD | <robot token> | β | β |
GITOPS_TOKEN | GitLab deploy token | β | β |
Release Workflow (Semantic Versioning)β
# Feature ready for production
git tag v1.3.0
git push origin v1.3.0
# GitLab CI triggers:
# build β test β scan β package β promote-production (manual)
v1.3.0 tag pushed
β
CI builds image: harbor.local/myteam/myapp:<sha>
CI packages chart: harbor.local/charts/myapp:1.3.0
CI updates gitops-repo β values-prod.yaml
β
ArgoCD detects change (OutOfSync)
Release manager clicks SYNC in ArgoCD UI
β
Production deploys v1.3.0
Pipeline Visualizationβ
build-image βββ unit-tests βββ trivy-scan βββ package-chart βββ promote-staging
β
(on tag v*) β
promote-production
[manual approval]
Done Whenβ
β Pipeline runs green on push to main
β Image appears in Harbor with correct tag
β Helm chart appears in Harbor OCI charts
β gitops-repo staging values updated automatically
β ArgoCD picks up change and syncs staging
β Production deploy requires git tag + manual approval