Skip to main content

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.

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.
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
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:
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.
#!/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"
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.

Step 3: Team members consume it

One command:
vers run-commit my-app:latest --ref --vm-alias my-dev
vers connect my-dev
Or as a shell alias:
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:
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:
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:
# 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