CI/CD for AI Apps
Automate the path from commit to production for AI apps: a pipeline of lint → tests → build image → eval gate → deploy, with the eval suite as a first-class gate, image versioning tied to git, registries, and safe deploy strategies (canary/blue-green) with rollback.
- Describe a CI/CD pipeline for an AI app: lint → tests → build image → eval gate → deploy.
- Add the eval suite as a first-class gate (not just unit tests) so quality regressions block a merge.
- Version images tied to git and push to a registry.
- Deploy safely with canary/blue-green strategies and a fast rollback.
For JavaScript developers: CI/CD here is the same GitHub Actions you'd use for a Node app — a YAML workflow of jobs (lint, test, build, deploy) triggered on push/PR. The AI-specific twist is one extra gate: the eval suite (Track 5) runs alongside unit tests, because a passing test suite doesn't prove the model still answers well.
The pipeline
CI/CD automates commit → production: CI (continuous integration) runs checks on every change; CD (continuous delivery/deployment) ships the artifact that passed. For an AI app the pipeline is:
push/PR → lint → unit tests → build & tag image → EVAL GATE → push to registry → deploy (canary → full)
Everything before deploy is a gate: fail any step and the change doesn't ship. The AI-specific insight is that unit tests are necessary but not sufficient — they verify your code, not your model's output quality. That's what the eval gate adds.
Here's a CI workflow (GitHub Actions) for the lint/test/build stages:
# .github/workflows/ci.yml
name: ci
on: [push, pull_request] # run on every push and PR
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # get the code
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install -r requirements.txt
- run: ruff check . # lint (fast, catches style/errors) — a gate
- run: mypy . # type-check — a gate
- run: pytest # unit tests (Track 1) — a gate
- run: docker build -t app:${{ github.sha }} . # build image, tagged by the commit SHA
↳ Exercise 1 has you write a CI workflow with lint + type-check + tests + build, and confirm a failing test blocks the pipeline — the safety net every deploy sits on.
The eval gate
This is what makes it AI CI/CD. Run the offline eval regression suite (Eval Design, Track 5) as a pipeline step, and fail the build if quality drops below threshold — including on weak slices. A prompt or model change that passes unit tests but tanks quality is caught here, not by users:
eval:
runs-on: ubuntu-latest
needs: test # only run if unit tests passed
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: python run_eval.py --dataset golden_set.jsonl --min-score 0.85 # the eval gate
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} # secret injected by the CI platform, never in code
# run_eval.py exits non-zero (fails the job) if overall or any weak-slice score is below threshold.
# run_eval.py — the script the gate runs
import sys
result = run_eval(feature, load_dataset()) # from Track 5
if result["overall"] < 0.85 or result["by_tag"].get("adversarial", 1) < 0.70:
print(f"EVAL REGRESSION: {result}"); sys.exit(1) # non-zero exit → the CI job fails → merge blocked
print(f"eval passed: {result}")
Eval-gate steps call the model, so they cost tokens and add minutes, and they're non-deterministic —
don't set the threshold so tight that normal variance flaps the build. Use a small, curated golden set, pin the
model version (LLMOps), and set thresholds with a margin. Secrets come from the CI secret store
(${{ secrets.* }}), never committed.
↳ Exercise 2 has you add an eval-gate job that runs after unit tests and blocks the merge on a simulated quality regression — CI enforcing model quality, not just code correctness.
Versioning images & the registry
Every build produces an immutable image tagged to its git commit (app:<sha>), pushed to a container
registry (Docker Hub, GitHub Container Registry, Azure Container Registry). Tagging by SHA means every
deployed artifact traces back to exact code, and any past version can be redeployed — the basis of rollback:
build-push:
runs-on: ubuntu-latest
needs: [test, eval] # only build the deployable image if BOTH gates passed
steps:
- uses: actions/checkout@v4
- run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- run: |
docker build -t ghcr.io/acme/app:${{ github.sha }} -t ghcr.io/acme/app:latest .
docker push ghcr.io/acme/app:${{ github.sha }} # immutable, SHA-tagged
docker push ghcr.io/acme/app:latest # moving 'latest' pointer
Tag by immutable SHA for anything you deploy or roll back to; latest is a convenience pointer, never the
thing you pin in production (same lesson as pinning dated model snapshots in LLMOps).
↳ Exercise 3 has you tag an image by git SHA, push it to a registry, and pull a specific SHA back — demonstrating the traceable, redeployable artifact rollback depends on.
Deploy strategies & rollback
CD ships the passed artifact. Two safe strategies (both from the LLMOps rollout discipline):
- Blue-green — run two environments; deploy to the idle one (green), test it, then switch traffic from blue to green all at once. Instant rollback = switch back to blue.
- Canary — route a small % of traffic to the new version, watch metrics (quality/cost/latency from Observability), then ramp up or roll back.
deploy:
runs-on: ubuntu-latest
needs: build-push
if: github.ref == 'refs/heads/main' # only deploy from main, and only after all gates passed
steps:
- run: ./deploy.sh ghcr.io/acme/app:${{ github.sha }} --canary 10 # deploy this SHA to 10% first
# promotion to 100% (or rollback) is gated on the canary's live metrics
The through-line with LLMOps (Track 5): the pipeline encodes the discipline — gates block bad changes, SHA-tagged images make rollback instant, and canary/blue-green contain a bad deploy. CI/CD is where those principles become automation.
↳ Exercise 4 has you design the deploy stage: choose canary vs. blue-green for a scenario and specify the rollback trigger and mechanism.
Pitfalls
- Unit tests as the only gate. They check code, not model quality — add the eval gate or you ship silent regressions.
- Deploying
latest. Pin the SHA-tagged image so you know (and can roll back to) exactly what's running. - Secrets in the repo. Use the CI platform's secret store; never commit keys or bake them into images.
- Flaky eval gate. Non-deterministic evals with too-tight thresholds block good changes — curate the set, pin the model, add margin.
- No rollback path. A pipeline that can deploy but not instantly roll back turns a bad release into an outage.
Recap
- CI/CD automates commit → production through gates: lint → tests → build/tag image → eval gate → registry → deploy.
- The eval suite is a first-class gate — unit tests verify code, evals verify model quality; fail the build on regression (incl. weak slices).
- Version images by git SHA and push to a registry so every deploy is traceable and any version is redeployable.
- Deploy with canary or blue-green and keep rollback instant (switch back / redeploy the prior SHA).
- CI/CD is where the LLMOps discipline becomes automation — gates, versioning, and safe rollout, enforced.
- Worked examples map to the exercises: a CI workflow (Ex 1), an eval gate (Ex 2), SHA image versioning (Ex 3), a deploy/rollback strategy (Ex 4).
CI/CD for AI Apps — Exercises
Use GitHub Actions (or your CI of choice — the shapes translate). Reuse the eval harness from Track 5 and the Dockerfile from the last topic.
Exercise 1 — A CI workflow with gates
Write .github/workflows/ci.yml that lints, type-checks, runs unit tests, and builds the image. Push a failing
test and confirm the pipeline blocks.
Reference solution
name: ci
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install -r requirements.txt
- run: ruff check .
- run: mypy .
- run: pytest
- run: docker build -t app:${{ github.sha }} .
Each run is a gate — a non-zero exit (a failing lint/type/test) fails the job and blocks the merge. The image
tag ${{ github.sha }} ties the build to the exact commit. This is the safety net every deploy sits on.
Exercise 2 — Add the eval gate
Add an eval job that runs after test and fails if the golden-set score drops below threshold. Simulate a
regression (e.g. a worse prompt) and confirm the gate blocks the merge.
Reference solution
eval:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: python run_eval.py --min-score 0.85
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
# run_eval.py
import sys
r = run_eval(feature, load_dataset())
if r["overall"] < 0.85 or r["by_tag"].get("adversarial", 1) < 0.70:
print("EVAL REGRESSION:", r); sys.exit(1) # non-zero -> job fails -> merge blocked
print("eval passed:", r)
Unit tests would pass a worse prompt (the code still runs); only the eval gate catches the quality drop. Keep the golden set small, pin the model version, and set thresholds with margin so non-deterministic variance doesn't flap the build. The key from the CI secret store, never committed.
Exercise 3 — Version an image by git SHA
Tag an image with the commit SHA, push it to a registry (GHCR), then pull that specific SHA back. Explain why
you deploy the SHA, not latest.
Reference solution
docker build -t ghcr.io/acme/app:$GIT_SHA -t ghcr.io/acme/app:latest .
echo "$REGISTRY_TOKEN" | docker login ghcr.io -u "$USER" --password-stdin
docker push ghcr.io/acme/app:$GIT_SHA
docker push ghcr.io/acme/app:latest
docker pull ghcr.io/acme/app:$GIT_SHA # redeploy any exact past version
latest is a moving pointer — what it references changes with each push, so "roll back to latest" is
meaningless. A SHA-tagged image is immutable and traces to exact code, so you can pin production to a known
version and redeploy any prior SHA instantly. That traceability is what makes rollback real (same lesson as
pinning dated model snapshots).
Exercise 4 — Design the deploy + rollback
For a customer-facing AI support app, choose canary or blue-green, and specify the rollback trigger and mechanism. Justify against the risk.
Reference solution
For a customer-facing app, canary is a strong default:
- Deploy: route ~10% of traffic to the new SHA; keep 90% on the current version.
- Rollback trigger: online metrics from Observability — a drop in the quality signal, or a spike in error rate / p95 latency / cost — versus the current version.
- Mechanism: flip the traffic split back to 100% current (the old version is still running), then investigate. Instant, because you never tore down the previous version.
- Promote: if the canary holds over a set window, ramp to 100%.
Blue-green is the alternative when you can't split traffic granularly: deploy to an idle environment, test, switch all traffic at once, and roll back by switching environments. Either way the principle is the same as LLMOps: keep the previous version instantly reachable so a bad deploy is contained, not an outage.