--- 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)