> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vers.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# CI: maintaining a `latest` image

> Keep a pre-baked dev VM in sync with your main branch, so anyone on the team can boot today's code in seconds

A common team workflow: on every merge to `main`, your CI builds a fresh VM with the latest code, dependencies, and migrations applied. It moves `my-app:latest` to point at this new build. Everyone on the team runs one command to get a fully warm dev environment that matches trunk.

This guide shows how to wire that up end to end.

## Why this pattern

Without it, every developer's first interaction with a fresh checkout is:

1. Clone the repo
2. Install system dependencies (maybe a specific Postgres version, a specific Node, a specific Python)
3. Run migrations
4. Seed a dev database
5. Warm caches
6. Realize you're missing a native library
7. Debug

That's 20 minutes on a good day, much longer when setup docs drift from reality. Branch → commit → tag solves it: CI does that work once per merge, the team pulls the result.

## The moving pieces

```
main branch merge
     │
     ▼
 CI runner  ─────▶  fork or branch from previous my-app:latest
     │                        │
     │                        ▼
     │                 apply code updates (git pull)
     │                 reinstall deps, run migrations
     │                        │
     │                        ▼
     │                   vers commit  →  new commit ID
     │                        │
     │                        ▼
     │            vers repo tag update my-app latest --commit <new>
     ▼
team members
     │
     ▼
  vers run-commit my-app:latest --ref
     →  fresh VM matching trunk, in seconds
```

## Prerequisites

* Vers CLI installed on the CI runner (or the Vers SDK in whatever language CI uses)
* A Vers API key stored as a CI secret
* A first commit to seed the repo (see below)

## Step 1: Seed the repo (one time)

Do this once, locally. Get a VM fully set up for your app (code cloned, deps installed, DB migrated, caches warm) and commit it.

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers run --vm-alias my-app-seed
vers connect

# Inside the VM
apt-get update && apt-get install -y git postgresql-client …
cd /root
git clone https://github.com/acme/my-app.git
cd my-app
npm install
npm run build
npx prisma migrate deploy
# … whatever else you need to warm up
exit
```

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers commit my-app-seed
# ✓ Commit c0000001-… created

vers repo create my-app -d "Pre-baked dev VMs for my-app, refreshed per main merge"
vers repo tag create my-app latest c0000001-… -d "Seeded 2026-04-20"
```

Sanity-check by restoring it:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers run-commit c0000001-… --vm-alias smoke
vers connect smoke
# Inside: cd /root/my-app && npm test
```

## Step 2: Write the CI update script

Every time `main` changes, CI runs roughly this. The script is plain bash so it translates cleanly to GitHub Actions, GitLab CI, Buildkite, whatever.

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
#!/usr/bin/env bash
set -euo pipefail

REPO="my-app"
TAG="latest"
BUILD_LABEL="ci-$(date -u +%Y%m%dT%H%M%S)-${GITHUB_SHA:0:8}"

# 1. Start a fresh VM from the current latest tag.
echo "Branching from $REPO:$TAG"
vers run-commit "$REPO:$TAG" --ref --vm-alias "$BUILD_LABEL" --wait

# 2. Update the VM to match main.
vers execute "$BUILD_LABEL" "cd /root/my-app && git fetch origin && git reset --hard origin/main"
vers execute "$BUILD_LABEL" "cd /root/my-app && npm ci"
vers execute "$BUILD_LABEL" "cd /root/my-app && npm run build"
vers execute "$BUILD_LABEL" "cd /root/my-app && npx prisma migrate deploy"

# 3. Optional: run a smoke test so a broken build doesn't become the new latest.
vers execute "$BUILD_LABEL" "cd /root/my-app && npm run test:smoke"

# 4. Snapshot the warm VM.
NEW_COMMIT=$(vers commit "$BUILD_LABEL" --format json | jq -r .commit_id)
echo "New commit: $NEW_COMMIT"

# 5. Move the `latest` tag to the new commit.
vers repo tag update "$REPO" "$TAG" --commit "$NEW_COMMIT" \
    --description "main @ ${GITHUB_SHA:0:8}, built $(date -u +%Y-%m-%dT%H:%M:%SZ)"

# 6. Optionally tag a dated snapshot so you can roll back.
vers repo tag create "$REPO" "build-$(date -u +%Y-%m-%d)" "$NEW_COMMIT" \
    -d "main @ ${GITHUB_SHA:0:8}" || true   # ignore if the tag already exists

# 7. Clean up the build VM.
vers kill "$BUILD_LABEL"
```

<Info>
  If starting from a fresh root VM is closer to what you want (say, you changed a system package), swap step 1 for `vers run --vm-alias "$BUILD_LABEL"` and run a full bootstrap in step 2. The pattern is otherwise identical.
</Info>

## Step 3: Team members consume it

One command:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers run-commit my-app:latest --ref --vm-alias my-dev
vers connect my-dev
```

Or as a shell alias:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
alias vers-dev='vers run-commit my-app:latest --ref --vm-alias my-dev && vers connect my-dev'
```

Programmatically (CI/CD, scripts, or non-Go SDKs), hit the API directly:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
VM_ID=$(
  curl -sS -X POST "https://api.vers.sh/api/v1/vm/from_commit" \
    -H "Authorization: Bearer $VERS_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"ref":"my-app:latest"}' \
  | jq -r .vm_id
)
```

Either way, they land on a VM that already has today's `main` built, migrations applied, caches warm. No apt-get. No "did you install libpq?". No "have you rebased?".

## Rollback

A bad build that somehow made it past smoke tests doesn't require a new CI run; just move `latest` back:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
OLD=$(vers repo tag get my-app build-2026-04-19 --format json | jq -r .commit_id)
vers repo tag update my-app latest --commit "$OLD" \
    -d "Rolled back from 2026-04-20 build due to issue #4713"
```

The old build's commit is still in place because you created a dated tag for it in step 6. Keep those dated tags as long as you want a rollback window, and delete the oldest ones periodically with `vers repo tag delete`.

## Scaling considerations

A few things to decide as this workflow grows:

* **How long to retain dated snapshots.** Each commit uses real storage. Keep a sliding window of recent builds (maybe 7 days) and a handful of blessed historical versions (`v1.0`, `v2.0`, quarter-end snapshots).
* **Whether to run CI builds in parallel for different configurations.** Every combination that matters gets its own repo or its own tag. `my-app:latest-x86`, `my-app:latest-arm64`. `my-app:staging`, `my-app:production`.
* **Who can push to `latest`.** All org members can update any tag in the org's repos right now; there's no per-tag ACL. Lock down which API keys CI uses and protect them like you would a deploy key.
* **Whether to publish.** If your team includes contractors with separate Vers orgs, making the repo public and having them `vers repo fork` is the mechanism. The fork is copy-on-write so it's still seconds.

## Hybrid with a supported image

If your dev VM is really "a supported pi-agent or language image plus our repo," you can fork once at seed time and let CI take it from there:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
# One-time seed
vers repo fork vers/pi-agent:latest --repo-name my-app --tag-name latest
# Or, for a language-specific base:
# vers repo fork vers/node:latest --repo-name my-app --tag-name latest
# Then fold in your code, deps, migrations as above, and overwrite latest.
```

After that first fork, CI treats `my-app:latest` exactly the same as if you'd hand-bootstrapped it.

## See Also

* [Repositories and Tags](/repositories) - the full model
* [`vers repo tag update`](/cli-reference/repo-tag-update)
* [`vers execute`](/cli-reference/execute) - run commands on a VM without SSH
* [Forking a supported image](/examples/fork-supported-image) - seed your repo from a Vers-maintained one
