Skip to content

GitLab CI/CD

This guide explains the GitLab CI/CD starter pipeline generated by haute init. It runs validation and deployment commands. As generated, anyone allowed to merge to main can start the production job; to make a reviewer approve it, see Who can start production. Databricks is the end-to-end service-update adapter; generic container builds/pushes only, and Azure Container Apps, AWS ECS, and GCP Cloud Run build and push the image and leave the service update to you.

New to Git, merge requests, or CI/CD?

This page assumes you understand basic Git concepts like branches, commits, and merge requests. If these are unfamiliar, read Before You Start first - it explains everything in plain English.

Not sure if this is the right page?

If you access your project on gitlab.com (or a company GitLab server), this is the right page. If it's on github.com, go to GitHub Actions. If it's on dev.azure.com, go to Azure DevOps. If you're not sure, ask your IT team or tech lead: "Where is our code repository hosted?" They'll tell you GitHub, GitLab, or Azure DevOps.

What is GitLab CI/CD?

GitLab CI/CD is GitLab's built-in automation tool. It runs scripts (called pipelines) in response to events in your repository - like opening a merge request or pushing to the main branch. If your team uses GitLab instead of GitHub, this is your CI/CD provider.

Who does what on this page?

This page has two parts. As a pricing analyst, you only need to do Step 2 (commit the pipeline file) and Step 3 (test it). Step 1 (adding CI/CD variables) is usually done by your IT team or tech lead - they have the credentials and know the GitLab UI. If you're setting this up yourself, Step 1 walks you through it, but it's perfectly fine to hand it to IT and say: "Can you add these CI/CD variables to our GitLab project?", with the names haute init printed (Step 1 lists them by target).


What Haute generates

When you run haute init --ci gitlab, Haute creates a single .gitlab-ci.yml file in the root of your repository. It defines five stages with ordering dependencies (three for Azure Container Apps, AWS ECS and GCP Cloud Run, which have no smoke-test or impact-analysis stage); whether a stage can create or update a live endpoint depends on the selected target:

Stage When it runs What it does
validate On merge requests and pushes to main Lints, type-checks, tests, validates pipeline, dry-run deploy
deploy-staging On push to main only Runs the target-dependent staging deployment command
smoke-test After staging deploy Scores test quotes against the live staging endpoint
impact-analysis After smoke test Compares new staging premiums vs current production
deploy-production Manual click Runs the target-dependent production deployment command after review

This gives you the same command sequence as GitHub Actions; a live release still depends on the selected target, endpoint readiness, and hosting process:

Merge request → Validation → Merge → Staging → Smoke test → Impact analysis → [You review] → Production

Workbench projects

A workbench project hosts no API: what it releases is the underwriter app. Its .gitlab-ci.yml has three stages instead: validate, which lints, type-checks and runs tests, then builds the app's release, which checks your pipeline and schema and scores your test quotes, on every merge request and on main; publish, on main only, which builds the app's image once, pushes it to the registry [deploy.container] names, rolls the staging app out to it and compares staging with production on saved quotes, keeping impact_report.md as the job's artifact; and production, a manual deploy-production job that rolls the production app out to the release staging runs. Each rollout checks the app and puts the previous release back when the new one fails (The underwriter app), and one rollout of an environment runs at a time. The publish and production jobs reads your host's variables, AZURE_SUBSCRIPTION_ID, AZURE_TENANT_ID, AZURE_CLIENT_ID and AZURE_CLIENT_SECRET, and needs the privileged runner a container target does. Tick Protect variable on all four, not optionally: GitLab gives a project's variables to every job, so only protection keeps them from merge-request pipelines on unprotected branches.


How it works, step by step

1. You make a change and open a merge request

You edit your pipeline, update a model, or change a configuration. You push your changes to a branch and open a merge request (MR) - GitLab's equivalent of a pull request. It's a way of asking your teammate to review your changes before they go live.

2. Validation runs automatically

The validate stage runs immediately on your merge request, as one lint job that runs these checks in order:

  • Lint - checks your code with ruff check
  • Type check - runs mypy to catch type errors
  • Tests - runs pytest
  • Pipeline validation - runs haute lint, which refuses what the editor would refuse to save, and haute deploy --dry-run, which checks the test quotes pass

If any of these fail, the MR gets a red ❌ and you can see what went wrong in the pipeline logs.

3. You merge to main

Once validation passes and your teammate has reviewed the MR, you merge it - which applies your changes to the main version of the project. This triggers the remaining stages.

4. Staging deployment (automatic)

The deploy-staging stage runs haute deploy --endpoint-suffix "-staging". On Databricks it waits until the staging endpoint serves the new version, and fails with Databricks' build log if the model cannot be served; the generic container target does not host it; platform-container targets push the image and leave the service update to you, so their pipeline has no smoke or impact stage and goes straight to the manual deploy-production job.

5. Smoke test (automatic)

If deploy succeeds, the smoke-test stage runs haute smoke --endpoint-suffix "-staging" against an existing live staging endpoint. On Databricks the endpoint is already serving when it starts, since the deploy waited for it. On the generic container target the stage runs haute smoke, which calls the staging address in haute.toml.

6. Impact analysis (automatic)

If smoke succeeds, the impact-analysis stage runs haute impact --endpoint-suffix "-staging" (haute impact, at the addresses in haute.toml, on the generic container target), comparing existing staging and production endpoints on the portfolio sample in the repository, or on your test quotes when no sample is named. The report is saved as a pipeline artifact only when this command runs.

7. You review the impact report

Download the impact_report.md artifact from the pipeline. It shows you exactly what changed, for each output column (the job log shows the same report):

  Output: technical_price
  ────────────────────────────────────────────────────────────
  Staging mean:          548.57     Production mean: 536.12
  Rows changed:           8,420 / 10,000 (84.2%)
  Mean change:            +2.3%
  Median change:          +1.9%
  Premium impact:         +2.3%
  Max increase:          +18.7%
  Max decrease:           -4.2%

  Distribution:  P5=-0.8%  P25=+0.6%  P50=+1.9%  P75=+3.4%  P95=+7.1%

Segment tables follow, one for each text column of the portfolio sample with 2 to 50 values, listing the segments whose prices move most. The report is advisory: it never fails the job, so reading it is your decision point.

8. You promote to production (manual)

If you're happy with the impact report, go to the pipeline in GitLab and click the play button ▶ on the deploy-production job. This is a manual action - it won't run until you explicitly click it, and anyone allowed to merge to main can click it. To make a reviewer approve each run, name an environment in [ci.production] environment and protect it with required approvals, which needs GitLab Premium (Who can start production); Haute does not enforce min_approvers.

The production job runs haute deploy and tags the release in Git. Databricks can update its serving endpoint; the generic container target only packages an image, and the Azure, AWS, and GCP platform-container targets build and push the image for a manual service update. Pushing the tag uses the job's token, which GitLab does not allow to push by default: allow Git pushes with the job token in the project's CI/CD settings, or give the job a token with write_repository.


Setup

Step 1: Add your credentials as CI/CD Variables

This step is usually done by IT

If you're not comfortable navigating GitLab's settings, ask your IT team or tech lead to do this. Give them the variable names haute init printed; the tables below list them by target.

Your pipeline needs credentials to deploy. In GitLab, these are stored as CI/CD Variables.

  1. Go to your project page in GitLab (e.g. gitlab.com/yourcompany/motor-pricing)
  2. In the left sidebar, click Settings (the ⚙️ gear icon near the bottom) → CI/CD
  3. Scroll down to the Variables section and click Expand
  4. Click Add variable for each credential below:

For Databricks:

Variable Value Options
DATABRICKS_MLFLOW_HOST The workspace URL for MLflow and the model registry Mask variable ✓
DATABRICKS_MLFLOW_TOKEN A personal access token scoped for MLflow and the model registry Mask variable ✓
DATABRICKS_RATING_HOST Your workspace URL Mask variable ✓
DATABRICKS_RATING_TOKEN Your personal access token scoped for Model Serving Mask variable ✓

For Docker/Container targets:

Variable Value Options
DOCKER_USERNAME Your registry username Mask variable ✓
DOCKER_PASSWORD Your registry password Mask variable ✓

For AWS ECS:

Variable Value Options
AWS_ACCESS_KEY_ID Your AWS access key Mask variable ✓
AWS_SECRET_ACCESS_KEY Your AWS secret key Mask variable ✓
AWS_DEFAULT_REGION e.g. eu-west-1 -

For Azure Container Apps:

Variable Value Options
AZURE_SUBSCRIPTION_ID Your subscription ID Mask variable ✓
AZURE_TENANT_ID Your tenant ID Mask variable ✓
AZURE_CLIENT_ID Your service principal app ID Mask variable ✓
AZURE_CLIENT_SECRET Your service principal secret Mask variable ✓

For GCP Cloud Run:

Variable Value Options
GCP_PROJECT_ID The Google Cloud project holding the registry and the service -
GCP_SERVICE_ACCOUNT_KEY The JSON key of the service account CI deploys as, on one line Protect variable ✓

The deploy jobs log in to Artifact Registry with the service account's key.

Container targets need a privileged runner

The jobs run in a python image, which has no Docker. For a container target, the deploy-staging and deploy-production jobs extend a hidden .docker job: a Docker-in-Docker service builds and pushes the image, and the job installs the matching Docker CLI before logging in to the registry in haute.toml. Docker-in-Docker needs a privileged runner that shares /certs/client with the service. GitLab.com's instance runners are set up that way; a self-managed runner needs privileged = true and volumes = ["/certs/client", "/cache"] in its config.toml.

Mask variable

Always tick Mask variable for secrets - this prevents them from appearing in pipeline logs.

Protect variable

Optionally tick Protect variable to restrict secrets to protected branches (like main). This means merge request pipelines can't access them, which is more secure.

Step 2: Commit the pipeline file

The .gitlab-ci.yml file was created by haute init in the root of your project. Make sure it's committed:

git add .gitlab-ci.yml pyproject.toml uv.lock
git commit -m "Add GitLab CI/CD pipeline"
git push

Every job installs with uv sync --frozen, which needs the project's uv.lock: commit it with pyproject.toml. uv add creates it; run uv sync once if it is missing.

Step 3: Test it

  1. Create a branch, make a small change, and open a merge request
  2. Go to Build → Pipelines - the validation stage should start automatically
  3. If it passes, merge the MR
  4. Watch the target-dependent pipeline; smoke and impact require a ready staging endpoint and do not run after a failed deploy
  5. Click the ▶ play button on deploy-production when you are ready and any applicable impact report is available

The generated pipeline in detail

You don't need to read or understand the YAML below

The pipeline file was generated by haute init and works out of the box. The sections below are reference only - for troubleshooting or if your IT team wants to understand what’s happening.

.gitlab-ci.yml structure
stages:
  - validate
  - deploy-staging
  - smoke-test
  - impact-analysis
  - deploy-production

variables:
  UV_CACHE_DIR: "$CI_PROJECT_DIR/.cache/uv"

default:
  image: python:3.11  # the Python your project declares; 3.11 when it declares none
  cache:
    key: uv-$CI_COMMIT_REF_SLUG
    paths:
      - .cache/uv
  before_script:
    - pip install "uv>=0.5,<1" && uv sync --frozen
Key features
  • Caching - UV_CACHE_DIR puts uv's package cache in .cache/uv, which the file keeps between runs, so installs are faster after the first run
  • resource_group: deploy - staging, smoke, impact, and production jobs use the same resource group, which prevents two deploys from running at the same time
  • when: manual - the production job requires a manual click, so you always review before going live
  • allow_failure: false - the manual production job blocks the pipeline from showing as “passed” until you run it (so you don’t forget)
  • Artifacts - the impact report is uploaded as a pipeline artifact you can download from the GitLab UI
Stage details
Stage Command Timeout
validate (the lint job) ruff check ., mypy ., pytest -v, haute lint, haute deploy --dry-run 10 min
deploy-staging haute deploy --endpoint-suffix "-staging" 60 min on Databricks, which waits for its endpoint; 15 min on the container targets
smoke-test haute smoke --endpoint-suffix "-staging" (container: haute smoke) 10 min
impact-analysis haute impact --endpoint-suffix "-staging" (container: haute impact) 10 min
deploy-production haute deploy + git tag 60 min on Databricks; 15 min on the container targets

GitLab vs GitHub - key differences

If you're used to the GitHub Actions setup, here's how GitLab differs:

Concept GitHub GitLab
Config file .github/workflows/*.yml (multiple files) .gitlab-ci.yml (single file)
Credentials Repository Secrets CI/CD Variables
Pull request Pull request Merge request
Manual approval Separate workflow + "Run workflow" button when: manual on a job + ▶ play button
Artifacts Upload/download actions Built-in artifacts: keyword
Parallel jobs Separate jobs: in a workflow Separate jobs in the same stage:

Working solo?

If you're the only person on the team:

  1. min_approvers in haute.toml is metadata, not an enforced GitLab approval setting
  2. The generated workflow attempts validation → staging → smoke test → impact analysis; later stages need a ready endpoint and do not run after a failed deploy
  3. Click the ▶ play button yourself after reviewing an impact report, if the impact command ran

Keeping the pipeline file up to date

After upgrading Haute, or changing [ci.staging] endpoint_suffix or [ci.production] environment in haute.toml, run haute ci update and commit .gitlab-ci.yml. It overwrites changes you made to the file by hand: see Keeping the generated files up to date for what it rewrites and removes.


Troubleshooting

Pipeline doesn't trigger

  • On MR: Make sure the MR targets the main branch
  • On merge: Check the pipeline ran - go to Build → Pipelines
  • Pending: If the pipeline is stuck on "pending", your GitLab instance may not have available runners. Check Settings → CI/CD → Runners

"Variable is not set" or "Authentication failed"

Your CI/CD variables are missing or misnamed:

  1. Go to Settings → CI/CD → Variables
  2. Check variable names match exactly (case-sensitive)
  3. If you ticked "Protect variable", make sure the job runs on a protected branch

Deploy job can't access credentials

If validation passes but deploy-staging fails with auth errors, your variables might be protected. Protected variables are only available on protected branches (like main). Since the deploy stages only run on main, this should work - but double-check your branch is protected in Settings → Repository → Protected branches.

"No runners available"

Your GitLab instance needs runners - machines that execute the pipeline. If you're on GitLab.com, shared runners are available by default. For self-hosted GitLab, ask your IT team to set up a runner or enable shared runners.

Impact report not appearing

The impact report is saved as a pipeline artifact. To download it:

  1. Go to the pipeline → click the impact-analysis job
  2. On the right side, click Browse under Job artifacts
  3. Download impact_report.md