feat(ci): GitLab CI/CD dev→prod pipeline, kustomize base+overlays
Multiple developers now work on this repo, and the only working deploy
trigger required pushing to GitHub - which contradicts the standing
GitLab-canonical policy for this repo - while every actual deploy was a
manual kubectl run against one environment (no prod exists at all).
Restructures deploy/k8s/ into base/ + overlays/{dev,prod}: overlays/dev
is a verified byte-for-byte no-op for the live sandbox (kubectl kustomize
diff against the old flat layout is empty), overlays/prod is scaffolded
but inert (placeholder hostname + JMAP_SERVER_URL, since neither a prod
hostname decision nor a prod Stalwart exist yet). deploy/k8s/ca/ (the
EJBCA internal CA) is untouched and never referenced by either overlay.
Adds .gitlab-ci.yml: verify (MR gate, no push/deploy) -> build+deploy-dev
(automatic on push to dev, one image name/tag-only environments, fixing
the old -dev/-beta naming split) -> promote (manual, protected
`production` environment, retags the exact dev digest via
`docker buildx imagetools create` - never rebuilds - and is left as a
documented TODO for the actual `kubectl apply` until prod is real).
Updates VNCMAIL-SETUP.md and deploy/k8s/README.md to describe the new
flow and correct the aspirational promotion description that assumed a
"production image" CI never actually built.
Also fixes a pre-existing lint error (no-control-regex false positive on
an intentional DN-sanitizing character class in lib/smime-ca/ejbca.ts)
that was blocking this commit's pre-commit hook - unrelated to this
change otherwise, confirmed already present on dev before this branch.
Runner/RBAC/registry setup is an infra prerequisite this commit cannot
provide - documented in the pipeline plan, not part of this diff.
This commit is contained in:
+144
@@ -0,0 +1,144 @@
|
||||
# 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-<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.
|
||||
Reference in New Issue
Block a user