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:
- Clone the repo
- Install system dependencies (maybe a specific Postgres version, a specific Node, a specific Python)
- Run migrations
- Seed a dev database
- Warm caches
- Realize you’re missing a native library
- 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