# 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 tried twice and does not work on this server: # # Round 1: registry_external_url was unset, so $CI_REGISTRY was empty and # docker login silently fell through to Docker Hub. # Round 2: after the server-side config, the project's sidebar DID show # "Container Registry" and docker login DID succeed - but the push # failed 403. Diagnosed 2026-08-05 by curling the vhost directly: # # $ curl -i https://registry.gitlab.vnc.biz/v2/ # www-authenticate: Bearer realm="http://gitlab.vnc.biz/jwt/auth", # service="dependency_proxy" # x-runtime: 0.020470 # x-gitlab-meta: {"correlation_id":...} # # x-runtime/x-gitlab-meta are RAILS headers, and the service is # "dependency_proxy" - so nginx routes that hostname to the GitLab # Rails app, which reads /v2/ as the dependency proxy (a Docker Hub # pull-through cache), NOT to the registry container. The registry # service was never actually wired behind that vhost. That is why # login worked (Rails issues an unscoped dependency-proxy token) # while a scoped :push request 403'd - the dependency proxy has no # push concept at all. # # Fixing that is an nginx/omnibus change on the GitLab server (registry # service must actually listen behind registry.gitlab.vnc.biz), not something # any .gitlab-ci.yml can reach. Until someone does that, GHCR it is - the # same image the sandbox already pulls, and confirmed public so the cluster # needs no imagePullSecrets (see deploy/k8s/base/deployment.yaml). # # Prerequisite this file assumes (documented in VNCMAIL-SETUP.md, not # something this file can set up itself): # - A GitHub PAT with `write:packages` for ghcr.io/brvncde-dotcom as # $GITLAB_CI_GHCR_TOKEN, plus the matching GitHub username as # $GITLAB_CI_GHCR_USER — both masked+protected CI/CD variables. A GitHub # credential can only come from GitHub; nothing GitLab-native substitutes. # - 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: # GHCR, not GitLab's own registry - see the "Registry history" note in the # header for the curl output proving why. Same image the sandbox already # pulls today. IMAGE: ghcr.io/brvncde-dotcom/vncmail-plus-dev 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, which needs an actual Docker daemon this # runner's Kubernetes executor doesn't provide without privileged mode # (see the build job below). Candidate for a separate scheduled job on a # differently-configured runner, not a blocker on every MR. # --------------------------------------------------------------------------- # build — push to dev only. Builds once; main never rebuilds (see header). # --------------------------------------------------------------------------- build: stage: build # Kaniko builds OCI images without a Docker daemon, so it needs neither a # dind service nor a privileged pod — GitLab's own recommended approach # for the Kubernetes executor specifically. The docker:27-cli + dind # combination that was here before this got as far as a successful # $CI_REGISTRY login, then failed every way it was pointed # (unix:///var/run/docker.sock, tcp://docker:2375, tcp://localhost:2375): # the dind container itself was never actually listening, which on this # executor means it needs `privileged: true` in the runner's own # config.toml — a cluster/GitLab-admin setting outside this file's # control. Kaniko sidesteps that requirement entirely rather than chasing # runner permissions further, and is also the safer default on a shared # cluster (no privileged containers at all). image: name: gcr.io/kaniko-project/executor:v1.23.2-debug entrypoint: [""] rules: - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"' script: # Fail loudly and immediately if the GHCR credentials aren't configured, # rather than letting kaniko get all the way through a full Next.js build # and only then 403 on push (which is exactly how the GitLab-registry # attempt burned several pipeline runs). - | if [ -z "$GITLAB_CI_GHCR_TOKEN" ] || [ -z "$GITLAB_CI_GHCR_USER" ]; then echo "ERROR: \$GITLAB_CI_GHCR_TOKEN and/or \$GITLAB_CI_GHCR_USER are not set." echo "Add both under Settings -> CI/CD -> Variables (masked + protected)." echo "The token is a GitHub PAT with the write:packages scope." exit 1 fi - mkdir -p /kaniko/.docker - | echo "{\"auths\":{\"ghcr.io\":{\"auth\":\"$(printf '%s:%s' "$GITLAB_CI_GHCR_USER" "$GITLAB_CI_GHCR_TOKEN" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json - > /kaniko/executor --context "$CI_PROJECT_DIR" --dockerfile "$CI_PROJECT_DIR/Dockerfile" --build-arg GIT_COMMIT=$CI_COMMIT_SHA --destination "$IMAGE:sha-$CI_COMMIT_SHORT_SHA" --destination "$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 <