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 main and 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

StageJobWhat it doesFails when
builddotnet:builddotnet build Colden.slnx -c Release, then tools/openapi-check.sh --no-buildthe build fails or the committed OpenAPI document is stale
buildconfig:env-exampletools/env-example-check.mjs on infra/compose/.env.examplea secret key has a value
builddocs:linkstools/docs-link-check.mjs (pnpm docs:links) on docs/, README.md and every README of the apps, packages, infrastructure, tests and toolsa relative link names a file that does not exist, or an anchor a heading its target does not have
buildweb:checkIn 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 buildany of them fails
buildhelm:checktools/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
testdotnet:testevery .NET suite with a Docker-in-Docker service for Testcontainers; JUnit reports in the merge requesta test fails
scanscan:dotnetdotnet list package --vulnerable --include-transitivea package, direct or transitive, has a known vulnerability
scanscan:pnpmpnpm audit --prod --audit-level higha production npm dependency has a high or critical advisory
scanscan:secretsTrivy's secret scanner on the checkouta secret is committed
scanGitLab SAST (semgrep-sast and the analyzers the template picks)static analysis; findings appear in the merge request's security reportan analyzer fails (findings do not fail it)
publishpublish:imagesby 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 runsa Dockerfile fails to build or a push fails
publishpublish:image-scanafter the images: Trivy's report of high and critical vulnerabilities in each, kept as an artifactTrivy cannot read an image (findings are reported, not failing)
publishpublish:charthelm package with the pipeline's tag as the app version, kept as an artifactpackaging fails
deploydeploy:nonprod, deploy:prodby hand, once the project describes its environments; prod only after nonprod in the same pipeline. See Deployingsee below
testchart:smokeby 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 afterwardsnever fails the pipeline; inside the job, a Deployment or a hook Job that does not finish, or a readiness check that does not answer
e2ee2e:nonprodby 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 artifactsnever fails the pipeline: a failed journey is reported on an environment already deployed
e2ee2e:lighthouseby hand, after deploy:nonprod: Lighthouse CI (tests/e2e/lighthouserc.cjs) on nonprod's public pages with the Playwright image's Chromium; the reports as artifactsnever 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:test and publish: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.

VariableKindValue
COLDEN_DEPLOYvariabletrue creates the deploy and e2e jobs
COLDEN_NONPROD_NAMESPACE, COLDEN_PROD_NAMESPACEvariableThe release's namespace
COLDEN_NONPROD_HOST, COLDEN_PROD_HOSTvariableThe web's host, shown on the environment page
COLDEN_NONPROD_VALUES, COLDEN_PROD_VALUESvariableThe environment's values file, in infra/helm/environments/
COLDEN_KUBECONFIG (or KUBECONFIG)file, secretA kubeconfig with rights in the namespace
COLDEN_CREATE_NAMESPACEvariablefalse where the cluster's operator creates namespaces: the job checks the namespace exists
COLDEN_REGISTRY_USER, COLDEN_REGISTRY_PASSWORD, COLDEN_PULL_SECRET_NAMEsecret, variableA 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_SECRETsecret, scoped to the environmentThe application Secret's required keys (infra/helm/colden/README.md describes each)
POSTGRES_PASSWORD, COLDEN_DB_PASSWORD, KRATOS_DB_PASSWORDsecretRequired 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 keyssecret, scoped to the environmentSet the ones the environment uses
E2E_MAILPIT_URLvariable, scoped to nonprodThe 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:

  1. Checks its inputs: the kubeconfig is not empty, and the namespace, the host and the values file are set.
  2. 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 Secret colden-secrets, with server-side apply.
  3. 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.
  4. Restarts the release's Deployments when the Secret changed: the pods read it only when they start.
  5. Shows helm history and kubectl get deploy,job,ingress in 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-runners from cloverheap/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 secret gitlab-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 environment prod, the live demo. Cloverheap has no nonprod yet: deploy:nonprod is replaced by a stand-in that prints success, so deploy:prod keeps its place after it.
  • The application Secret's keys are not GitLab variables there. .gitlab/ci/cloverheap.yml includes cloverheap/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, where tools/bootstrap-namespace.sh reads 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/helm 3.22.1;
  • aquasec/trivy 0.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-check for 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.yml and 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.