The Colden pipeline
What the GitLab pipeline runs in each stage, what the runner and the project need, how a project describes its environments and how a deployment and a rollback happen.
GitLab CI is the kit's pipeline of record. It runs on every merge request, on main, on tags and when started by hand, in six stages: build, test, scan, publish, deploy and e2e.
- Builds, tests and scans run on their own, in every merge request and on
main. - Publishing the images and deploying are manual jobs on
mainand on tags, so unmerged code never reaches a registry or a cluster.
.gitlab-ci.yml holds the stages, the pinned images and the includes. Each stage's jobs live in .gitlab/ci/<stage>.yml and the shared templates in .gitlab/ci/templates.yml. Every job runs a command a developer can run locally (the commands table in README.md).
Jobs
| Stage | Job | What it does | Fails when |
|---|---|---|---|
| build | dotnet:build | dotnet build Colden.slnx -c Release, then tools/openapi-check.sh --no-build | the build fails or the committed OpenAPI document is stale |
| build | config:env-example | tools/env-example-check.mjs on infra/compose/.env.example | a secret key has a value |
| build | docs:links | tools/docs-link-check.mjs (pnpm docs:links) on docs/, README.md and every README of the apps, packages, infrastructure, tests and tools | a relative link names a file that does not exist, or an anchor a heading its target does not have |
| build | web:check | In one job, sharing one install: pnpm -r lint (ESLint with the hardcoded-string rule, and the design token rule over pages, components and libraries), the SDK freshness check, the token sheets check, pnpm -r typecheck (message keys included), pnpm -r test --coverage (the line coverage on the job, the Cobertura reports on the merge request), pnpm -r build | any of them fails |
| build | helm:check | tools/helm-check.sh, on the chart's values files and on every environment's in infra/helm/environments/ | the chart's copies drift, a values file sets a secret key, lint, the Kubernetes schema or a chart unit test fails |
| test | dotnet:test | every .NET suite with a Docker-in-Docker service for Testcontainers; JUnit reports in the merge request | a test fails |
| scan | scan:dotnet | dotnet list package --vulnerable --include-transitive | a package, direct or transitive, has a known vulnerability |
| scan | scan:pnpm | pnpm audit --prod --audit-level high | a production npm dependency has a high or critical advisory |
| scan | scan:secrets | Trivy's secret scanner on the checkout | a secret is committed |
| scan | GitLab SAST (semgrep-sast and the analyzers the template picks) | static analysis; findings appear in the merge request's security report | an analyzer fails (findings do not fail it) |
| publish | publish:images | by hand, on main and tags: tools/build-images.sh, then pushes colden-api, colden-worker, colden-web and colden-migrate as sha-<commit> and the branch or tag name. The pipeline waits here until it runs | a Dockerfile fails to build or a push fails |
| publish | publish:image-scan | after the images: Trivy's report of high and critical vulnerabilities in each, kept as an artifact | Trivy cannot read an image (findings are reported, not failing) |
| publish | publish:chart | helm package with the pipeline's tag as the app version, kept as an artifact | packaging fails |
| deploy | deploy:nonprod, deploy:prod | by hand, once the project describes its environments; prod only after nonprod in the same pipeline. See Deploying | see below |
| test | chart:smoke | by hand: kind 0.33.0 (pinned with its node image and checksum) in the job's Docker daemon, the four images built and loaded, throwaway secrets through tools/bootstrap-namespace.sh, then helm install --wait with scheme: http, the in-cluster PostgreSQL and Redis and the network policies, every Deployment available, and /health/ready of the api and of Kratos through a port-forward; the cluster is deleted afterwards | never fails the pipeline; inside the job, a Deployment or a hook Job that does not finish, or a readiness check that does not answer |
| e2e | e2e:nonprod | by hand, after deploy:nonprod: the Playwright journeys in the pinned Playwright image against nonprod, the accessibility journey (axe, WCAG 2.2 AA) among them, reading the codes and the invitation links from the environment's Mailpit (E2E_MAILPIT_URL), the impersonation journey skipped unless E2E_SUPER_ADMIN_COMMAND can make a super-admin there; report and traces as artifacts | never fails the pipeline: a failed journey is reported on an environment already deployed |
| e2e | e2e:lighthouse | by hand, after deploy:nonprod: Lighthouse CI (tests/e2e/lighthouserc.cjs) on nonprod's public pages with the Playwright image's Chromium; the reports as artifacts | never fails the pipeline; inside the job, accessibility below 0.95 or best practices below 0.9 fail it, and performance below 0.8 warns |
The rollback job, failing image scans and the published chart are planned; until then a rollback uses the commands under Rolling back.
What the runner and the project need
- A runner that allows privileged services, for the Docker-in-Docker service of
dotnet:testandpublish:images. - The project's container registry enabled, or another registry (see Images and registry).
- Network access to:
- NuGet and the npm registry;
- GitHub (the chart check downloads kubeconform and helm-unittest with verified checksums);
- the Kubernetes release site (kubectl);
- the Trivy databases;
- Docker Hub and the Microsoft Container Registry (the job images).
- For the deploy jobs, a runner that can reach the cluster's API server.
Images and registry
publish:images pushes to COLDEN_IMAGE_REGISTRY with COLDEN_PUSH_USER and COLDEN_PUSH_PASSWORD. When those are not set, it pushes to the project's own registry ($CI_REGISTRY_IMAGE) with the job's credentials. The deploy jobs set the chart's global.image.registry to the same registry, and its global.image.tag to this pipeline's sha-<commit>.
Deploying
A project describes its environments with these variables; the deploy jobs exist only when COLDEN_DEPLOY is true.
| Variable | Kind | Value |
|---|---|---|
COLDEN_DEPLOY | variable | true creates the deploy and e2e jobs |
COLDEN_NONPROD_NAMESPACE, COLDEN_PROD_NAMESPACE | variable | The release's namespace |
COLDEN_NONPROD_HOST, COLDEN_PROD_HOST | variable | The web's host, shown on the environment page |
COLDEN_NONPROD_VALUES, COLDEN_PROD_VALUES | variable | The environment's values file, in infra/helm/environments/ |
COLDEN_KUBECONFIG (or KUBECONFIG) | file, secret | A kubeconfig with rights in the namespace |
COLDEN_CREATE_NAMESPACE | variable | false where the cluster's operator creates namespaces: the job checks the namespace exists |
COLDEN_REGISTRY_USER, COLDEN_REGISTRY_PASSWORD, COLDEN_PULL_SECRET_NAME | secret, variable | A credential that can read the images, for the pull secret, and the pull secret's name (gitlab-registry by default) |
ConnectionStrings__Postgres, Ory__WebhookSecret, KRATOS_DSN, KRATOS_SECRETS_COOKIE, KRATOS_SECRETS_CIPHER, WEB_COOKIE_SECRET | secret, scoped to the environment | The application Secret's required keys (infra/helm/colden/README.md describes each) |
POSTGRES_PASSWORD, COLDEN_DB_PASSWORD, KRATOS_DB_PASSWORD | secret | Required only when the environment runs the chart's in-cluster PostgreSQL (COLDEN_IN_CLUSTER_POSTGRESQL=true) |
Stripe__SecretKey, Stripe__WebhookSecret, Email__Mailgun__ApiKey, Telemetry__OtlpHeaders and the other optional keys | secret, scoped to the environment | Set the ones the environment uses |
E2E_MAILPIT_URL | variable, scoped to nonprod | The Mailpit nonprod sends its email to, where e2e:nonprod reads the codes |
Mark every secret Protected and Masked, and scope the application Secret's keys to their environment (nonprod or prod). Protected variables reach only main and protected tags, which is why the deploy jobs exist only there.
A deploy job, in its environment's resource_group so two deploys of one environment never overlap:
- Checks its inputs: the kubeconfig is not empty, and the namespace, the host and the values file are set.
- Runs
tools/bootstrap-namespace.sh <namespace>. It checks the variables (a missing required key or a key of the wrong length stops the job without printing a value), then applies the namespace (or checks it exists), the pull secret and the application Secretcolden-secrets, with server-side apply. - Runs
helm upgrade --install colden dist/colden-*.tgz -n <namespace> -f <values> --set global.image.registry=<registry> --set global.image.tag=sha-<commit> --wait --timeout 10m --atomic. The migrations run first as hooks, then the pods roll; a failure rolls the release back to the previous revision. - Restarts the release's Deployments when the Secret changed: the pods read it only when they start.
- Shows
helm historyandkubectl get deploy,job,ingressin the job log.
A developer prepares a namespace the same way with the variables exported in their shell (tools/bootstrap-namespace.sh <namespace>), or checks them first with --dry-run, which lists the keys and needs no cluster.
Rolling back
A failed deploy rolls itself back (--atomic). To return to an earlier image, run the deploy job of the older pipeline whose image you want: it deploys that pipeline's sha-<commit>. To return to an earlier revision of the release, helm -n <namespace> history colden lists them and helm -n <namespace> rollback colden <revision> restores its Deployments and ConfigMaps. Migrations are not undone, so each release keeps the database usable by the previous one.
The cloverheap group
In projects under cloverheap/, .gitlab-ci.yml includes .gitlab/ci/cloverheap.yml, which refines the kit's pipeline for the operator's setup described in cloverheap/ci-cd:
- Every job extends
.use-local-runnersfromcloverheap/ci-cd: it runs on the operator's self-hosted runners and reuses the images already there (pull_policy: if-not-present). The SAST analyzers come from GitLab's template and keep its runner choice. - The images go to the group's registry (
CH_GITLAB_DOCKER_REGISTRY) with the group's registry credentials (CH_RW_REGISTRY_USER,CH_RW_REGISTRY_TOKEN), which also make the cluster's pull secretgitlab-docker-credentials. - The deploy jobs use the group's kubeconfig (
CH_K8S_LOCAL_KUBECONFIG). They check the namespace the operator created, and describe Cloverheap's environmentprod, the live demo. Cloverheap has no nonprod yet:deploy:nonprodis replaced by a stand-in that printssuccess, sodeploy:prodkeeps its place after it. - The application Secret's keys are not GitLab variables there.
.gitlab/ci/cloverheap.ymlincludescloverheap/ci-cd's resolver of Azure Key Vault references (gitlab-templates/azurekv/main.yml,.ch-azurekv), and before step 2 the deploy job resolves the environment's references file (COLDEN_PROD_SECRETS,infra/helm/environments/cloverheap/references/prod.yaml) into its environment, wheretools/bootstrap-namespace.shreads the keys as it reads variables.
docs/deployment.md lists those environments, their secrets and the one-time setup.
Pinned images and tools
.gitlab-ci.yml pins every image by tag and digest:
- the .NET SDK;
- Node.js 22 on Alpine;
- Docker 29 with Docker-in-Docker;
alpine/helm3.22.1;aquasec/trivy0.76.0;- Playwright 1.64.1 on Ubuntu.
kubectl 1.37.1 and, for chart:smoke, kind 0.33.0 are downloaded and checked against their SHA-256; keep kubectl within one minor version of the clusters it deploys to, and kind's node image (kindest/node 1.34, by digest) at the Kubernetes version tools/helm-check.sh validates against. tools/helm-check.sh pins kubeconform and helm-unittest the same way.
Changing the pipeline
- A new job goes into the file of its stage and runs a command from
README.md, so it can be run locally first. - It extends
.colden-job, or.colden-checkfor a build, test or scan job, directly or through a template, so a runner setup reaches it. - A new image is pinned by digest in
.gitlab-ci.ymland named in this guide. - A new secret key of the application Secret goes into the same change in four places:
tools/bootstrap-namespace.sh;- the chart's README;
- the chart's list of secret keys;
- this guide's table.