---
title: GitLab CI/CD
description: The pipeline that verifies, builds and ships a kreogen project.
type: guide
---
# GitLab CI/CD
Generated projects ship a `.gitlab-ci.yml` that verifies every merge request
and builds images from the default branch.
## Stages
**verify** — lint, typecheck and test, all in parallel. This is the same
`bun run verify` you run locally.
**build** — one image each for `app`, `web` and `api`, built in a matrix, on
the default branch only.
This describes the pipeline a generated project gets, from
`templates/project.gitlab-ci.yml`. kreogen's own pipeline is larger — five
stages, including `package` and `release` — because the template also publishes
a CLI to npm and cuts releases. A client project ships images, not a package.
## Caching
Two independent caches:
* **Dependencies**, keyed on `bun.lock`. Reinstalling per job against a warm
cache takes a couple of seconds and is far more reliable than passing
`node_modules` between jobs as artifacts.
* **Turbo**, keyed on the branch with a fallback to the default branch, so a
new branch inherits main's task cache on its first run.
Both live inside `$CI_PROJECT_DIR`, because GitLab cannot cache paths outside
it.
## Images
Built with buildx against a registry-backed cache, tagged `sha-` and
`latest`, and pushed to the project's container registry using
`CI_JOB_TOKEN` — no extra credentials to configure.
Deploy from the `sha-` tag or a digest rather than `latest`, so a rollback is
a matter of pointing at the previous one.
## Adding a deploy stage
The template stops at building images, because how a project deploys varies.
A minimal addition:
```yaml
deploy:
stage: deploy
needs: [docker]
rules:
- if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"
environment:
name: production
url: https://app.example.com
script:
- ./scripts/deploy.sh "$CI_COMMIT_SHORT_SHA"
```
Run migrations before the new image serves traffic, as a separate job or an
init container — not from application startup, where every replica would race.
## Environment variables
Set secrets as masked, protected CI/CD variables in project settings. The
pipeline itself needs none: `SKIP_ENV_VALIDATION=true` is set because builds
are static and validation happens when the container boots.
## Scheduled work
Use a scheduled pipeline calling an authenticated endpoint:
```yaml
cron:
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule" && $CRON_JOB == "digest"'
script:
- curl -fsS -H "Authorization: Bearer $CRON_SECRET" "$API_URL/cron/digest"
```
Runs are logged, retryable and alertable, and the logic stays in the app where
it is typed and testable. Avoid in-app schedulers — they fire once per replica.
Make such endpoints idempotent and guard them with a Redis lock: schedulers
retry.
---
For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)
For an index of all available documentation, see [/llms.txt](/llms.txt)