GitLab CI/CD
The pipeline that verifies, builds and ships a kreogen project.
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 passingnode_modulesbetween 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-<short> 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:
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:
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.