# GitLab-CI dev→prod pipeline for VNCmail+ — GitOps via ArgoCD. # # Revised after direct inspection of the real infrastructure found ArgoCD # already installed (idle, zero Applications) on the dev-k8s-1/2/3 cluster. # That's more idiomatic than a runner-executes-kubectl design, and it means # this pipeline needs ZERO cluster credentials — CI only ever talks to the # container registry and to this git repo. ArgoCD (which already has # whatever cluster access it needs, set up once when its Applications were # registered — see deploy/argocd/) is what actually applies anything. # # Design: # - One image name, environment lives only in the tag. No more -dev/-beta # name confusion from the old GitHub Actions workflow. # - 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, then commit a # one-line tag-bump into overlays/dev/image-tag/kustomization.yaml # (`[skip ci]`, so this doesn't retrigger itself). ArgoCD's `vncmail-dev` # Application has automated sync — it notices the git change and applies # it. No approval needed, dev always deploys, and this job never touches # the cluster directly. # - 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 same sha- tag dev already deployed). This job just bumps # overlays/prod/image-tag/kustomization.yaml to point at that same tag. # The actual promotion gate is a HUMAN clicking Sync on the `vncmail-prod` ArgoCD # Application (deliberately NOT automated sync) — not a GitLab manual # job, since ArgoCD already provides that exact gate more directly. # Until prod Stalwart/hostname/secrets are real (see VNCMAIL-SETUP.md), # nobody should click that Sync button — but nothing here does it for # you either way. # # Deliberately single-platform (linux/amd64) — this pipeline serves two # known amd64 microk8s clusters, not public multi-arch distribution (that's # what the GHCR release workflows are for, untouched by this file). # # Registry history (so nobody re-litigates this from scratch): GitLab's own # Container Registry was the first choice, hit a dead end (registry_external_url # alone didn't populate $CI_REGISTRY — see git history of this file around # 2026-08-05 for the GHCR detour that followed), then got fixed server-side # (gitlab_rails['registry_enabled'] confirmed set — the project's left sidebar # now shows "Container Registry" under Deploy). Back on GitLab's native # registry since then: it needs zero extra credentials (CI_REGISTRY_* are # predefined GitLab CI variables, always present, scoped to this project # only), which is strictly better than holding a GitHub PAT in GitLab CI/CD # variables just to push images. # # Prerequisite this file assumes (documented in VNCMAIL-SETUP.md, not # something this file can set up itself): # - GitLab Container Registry enabled for this project (confirmed 2026-08-05 # — "Container Registry" appears in the project's left sidebar under # Deploy / Packages and registries). # - A GitLab Runner (any kind — no cluster access needed at all now). # - Either "allow this job token to push to this project" enabled # (Settings → CI/CD → Job token permissions), OR a project access token # with `write_repository` scope in $GITLAB_PUSH_TOKEN. The job below # tries CI_JOB_TOKEN first (see the script). # # deploy/k8s/ca/ (the EJBCA internal CA) is never referenced anywhere below, # and neither ArgoCD Application in deploy/argocd/ points at it — that stays # a fully manual, human-only runbook (see deploy/k8s/ca/README.md). stages: - verify - build - bump-dev - bump-prod variables: # GitLab's own registry, scoped to this project. $CI_REGISTRY_IMAGE is a # predefined GitLab CI variable (populated automatically once the Container # Registry is enabled for the project) — no manual image name to keep in # sync, no external credential. IMAGE: $CI_REGISTRY_IMAGE GIT_STRATEGY: clone # --------------------------------------------------------------------------- # 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), heavier than a fast MR # gate should be. Candidate for a separate scheduled job, not a blocker. # --------------------------------------------------------------------------- # 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"' variables: # docker:27-dind defaults to TLS on :2376 with certs under # /certs/client, which this client image never mounts — the dind # service comes up fine but the docker:27-cli image can't find it, # surfacing as "Cannot connect to the Docker daemon at # unix:///var/run/docker.sock" even though $CI_REGISTRY login already # succeeded (that's a separate connection, straight to the registry, # not through the daemon). Disabling TLS between the two containers of # the same job is standard for GitLab's Kubernetes executor — they # share a pod network namespace, so plaintext here isn't exposed # outside the job. DOCKER_HOST: tcp://docker:2375 DOCKER_TLS_CERTDIR: "" before_script: # $CI_REGISTRY / $CI_REGISTRY_USER / $CI_REGISTRY_PASSWORD are predefined # GitLab CI variables — populated automatically now that this project's # Container Registry is enabled. No CI/CD variable to create by hand. - echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --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" # --------------------------------------------------------------------------- # bump-dev — no cluster access. Commits the just-built tag into the overlay # ArgoCD watches; ArgoCD's automated sync does the actual apply. # --------------------------------------------------------------------------- bump-dev: stage: bump-dev image: alpine/git:2.47.0 rules: - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"' script: - TAG="sha-$CI_COMMIT_SHORT_SHA" - | cat > deploy/k8s/overlays/dev/image-tag/kustomization.yaml < deploy/k8s/overlays/prod/image-tag/kustomization.yaml <