# GitLab-CI dev→prod pipeline for VNCmail+. # # Design (see the approved plan for full rationale): # - One image name, environment lives only in the tag. No more -dev/-beta # name confusion. # - MR into `dev`: verify only (typecheck/lint/unit test/build check). No # push, no deploy — this is the multi-developer merge gate. # - Push to `dev`: build+push an immutable `sha-` tag, auto-deploy it # to the vncmail (sandbox) namespace. No approval needed — dev always # deploys. # - Push to `main`: NEVER rebuilds. `main` only ever advances via # `git merge --ff-only dev`, so main's HEAD commit already has a built # image. The `promote` job retags that exact digest (registry-side copy, # same primitive the old docker-publish.yml GHA workflow already used for # its multi-arch manifest-list merge) and applies it to prod. `when: # manual` + a protected `production` GitLab environment is the approval # gate — nobody but an authorized user can click it, and nothing here # runs automatically on main. # # Deliberately single-platform (linux/amd64) for the cluster build — this # pipeline's job is deploying to a known amd64 microk8s cluster, not public # multi-arch distribution (that's what the GHCR release workflows are for, # and they're untouched by this file). # # Prerequisites this pipeline assumes are already in place (see the plan's # "Split of responsibility" — these are admin/infra actions, not something # this file can set up): # - GitLab Container Registry enabled for this project (CI_REGISTRY_* vars # are then provided automatically — no manual credential setup needed). # - A GitLab Runner with the Kubernetes executor, whose deploy-stage jobs # run as a `gitlab-deployer` ServiceAccount scoped (namespaced Role, not # cluster-admin) to the `vncmail` namespace (and later `vncmail-prod`). # kubectl auto-detects in-cluster config from that ServiceAccount's # mounted token — no KUBECONFIG variable required. # # deploy/k8s/ca/ (the EJBCA internal CA) is never referenced anywhere below — # that stays a fully manual, human-only runbook (see deploy/k8s/ca/README.md). stages: - verify - build - deploy-dev - promote variables: IMAGE: $CI_REGISTRY_IMAGE/vncmail-plus DEV_NAMESPACE: vncmail PROD_NAMESPACE: vncmail-prod # --------------------------------------------------------------------------- # verify — required check on every MR into dev. No registry, no cluster. # --------------------------------------------------------------------------- verify: stage: verify image: node:24-alpine rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' script: - npm ci - npm run typecheck - npm run lint - npm run test:translations - npm run build # test:integration is deliberately NOT here — it spins up a real Stalwart # fixture via docker-compose (Docker-in-Docker), which is heavier than a # fast MR gate should be. Candidate for a separate scheduled/optional job # later, not a blocker for this pipeline's first cut. # --------------------------------------------------------------------------- # build — push to dev only. Builds once; main never rebuilds (see header). # --------------------------------------------------------------------------- build: stage: build image: docker:27-cli services: - docker:27-dind rules: - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"' before_script: - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin script: - docker build --build-arg GIT_COMMIT=$CI_COMMIT_SHA -t "$IMAGE:sha-$CI_COMMIT_SHORT_SHA" -t "$IMAGE:dev-latest" . - docker push "$IMAGE:sha-$CI_COMMIT_SHORT_SHA" - docker push "$IMAGE:dev-latest" # --------------------------------------------------------------------------- # deploy-dev — automatic, no approval. Deploys the immutable sha tag, never # the moving dev-latest pointer, so what's running always matches one commit. # --------------------------------------------------------------------------- deploy-dev: stage: deploy-dev image: bitnami/kubectl:1.31 environment: name: dev url: https://vncmail.sandbox.vnc.de rules: - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"' script: # Apply the manifests first (structure/config), then set the exact image # this pipeline just built — imperative `set image`, not a kustomize-file # edit, so overlays/dev never needs a commit to change what's deployed. - kubectl apply -k deploy/k8s/overlays/dev - kubectl -n $DEV_NAMESPACE set image deployment/vncmail-plus vncmail-plus="$IMAGE:sha-$CI_COMMIT_SHORT_SHA" - kubectl -n $DEV_NAMESPACE rollout status deploy/vncmail-plus --timeout=120s # --------------------------------------------------------------------------- # promote — manual, protected `production` environment. No docker build here # — retags the exact digest already deployed to dev, then applies prod # pinned to that digest (never a mutable tag). # --------------------------------------------------------------------------- promote: stage: promote image: docker:27-cli services: - docker:27-dind environment: name: production url: https://vncmail.CHANGEME.invalid # placeholder until the real prod host is decided rules: # `when: manual` lives inside the rule (not as a top-level job key) — # required syntax once `rules:` is used at all. - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"' when: manual before_script: - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin script: - echo "Retagging the image already built+deployed for dev commit $CI_COMMIT_SHA — no rebuild." - docker buildx imagetools create --tag "$IMAGE:prod-latest" "$IMAGE:sha-$CI_COMMIT_SHORT_SHA" - DIGEST=$(docker buildx imagetools inspect "$IMAGE:sha-$CI_COMMIT_SHORT_SHA" | awk '/^Digest:/{print $2}') - echo "Resolved digest for prod = $IMAGE@$DIGEST" - > echo "STOPPING HERE ON PURPOSE: deploy/k8s/overlays/prod is still scaffolded/inactive (placeholder hostname, placeholder JMAP_SERVER_URL — no prod Stalwart exists yet). Once both are real (Phase D in the pipeline plan / VNCMAIL-SETUP.md), replace this echo with the same pattern deploy-dev uses, against a bitnami/kubectl image and \$PROD_NAMESPACE: kubectl apply -k deploy/k8s/overlays/prod && kubectl -n \$PROD_NAMESPACE set image deployment/vncmail-plus vncmail-plus=$IMAGE@$DIGEST" # Deliberately does NOT run `kubectl apply -k overlays/prod` yet — prod # namespace/hostname/Stalwart don't exist (Phase C/D in the plan). Once # they do, replace the placeholder echo above with the same # `kubectl apply -k .` + `set image ...@$DIGEST` pattern deploy-dev uses, # against $PROD_NAMESPACE, using the bitnami/kubectl image.