mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
feat(digitalocean): Packer nightly snapshot pipeline for fast boot (#2198)
* feat(digitalocean): Packer nightly snapshot pipeline for fast boot
Add pre-built Packer snapshots for DigitalOcean droplets. Instead of
10-20 min cloud-init + agent install on every boot, snapshot-based
droplets boot in ~2-3 min (SSH only, agent pre-installed).
- Packer HCL2 template with parametrized agent/tier builds
- Agent build matrix (packer/agents.json) for all 7 agents
- Tier scripts mirroring cloud-init.ts package tiers
- Nightly GitHub Actions workflow (4 AM UTC, max-parallel: 3)
- Automatic cleanup: keeps only latest snapshot per agent
- CLI: findSpawnSnapshot() looks up pre-built images via DO API
- CLI: waitForSshOnly() skips cloud-init when using snapshots
- CLI: createServer() accepts optional snapshotId, skips user_data
- CLI: main.ts routes to fast path when snapshot detected
- Tests for findSpawnSnapshot() (5 cases, all passing)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(packer): use var-file for install_commands to avoid shell quoting issues
The previous approach passed install_commands as `-var` inline, but
GitHub Actions expands `${{ }}` before shell evaluation — JSON arrays
with `|`, `&&`, and `"` characters break shell quoting.
Fix: generate a `.auto.pkrvars.json` file (auto-loaded by Packer)
using jq with --argjson for safe JSON handling. Also route all
`${{ inputs }}` and `${{ matrix }}` values through env vars to
prevent script injection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3242fa78f1
commit
ed98a59318
11 changed files with 546 additions and 10 deletions
2
packer/.gitignore
vendored
Normal file
2
packer/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*.auto.pkrvars.json
|
||||
.packer.d/
|
||||
45
packer/agents.json
Normal file
45
packer/agents.json
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"claude": {
|
||||
"tier": "minimal",
|
||||
"install": [
|
||||
"curl -fsSL https://claude.ai/install.sh | bash || npm install -g @anthropic-ai/claude-code"
|
||||
]
|
||||
},
|
||||
"codex": {
|
||||
"tier": "node",
|
||||
"install": [
|
||||
"npm install -g @openai/codex"
|
||||
]
|
||||
},
|
||||
"openclaw": {
|
||||
"tier": "full",
|
||||
"install": [
|
||||
"npm install -g openclaw"
|
||||
]
|
||||
},
|
||||
"opencode": {
|
||||
"tier": "minimal",
|
||||
"install": [
|
||||
"curl -fsSL https://opencode.ai/install | bash"
|
||||
]
|
||||
},
|
||||
"kilocode": {
|
||||
"tier": "node",
|
||||
"install": [
|
||||
"npm install -g @kilocode/cli"
|
||||
]
|
||||
},
|
||||
"zeroclaw": {
|
||||
"tier": "minimal",
|
||||
"install": [
|
||||
"fallocate -l 4G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile",
|
||||
"curl -LsSf https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw/a117be64fdaa31779204beadf2942c8aef57d0e5/scripts/bootstrap.sh | bash -s -- --install-rust --install-system-deps --prefer-prebuilt"
|
||||
]
|
||||
},
|
||||
"hermes": {
|
||||
"tier": "minimal",
|
||||
"install": [
|
||||
"curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash"
|
||||
]
|
||||
}
|
||||
}
|
||||
127
packer/digitalocean.pkr.hcl
Normal file
127
packer/digitalocean.pkr.hcl
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
packer {
|
||||
required_plugins {
|
||||
digitalocean = {
|
||||
version = ">= 1.4.0"
|
||||
source = "github.com/digitalocean/digitalocean"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Variables ───────────────────────────────────────────────────────────────
|
||||
|
||||
variable "do_api_token" {
|
||||
type = string
|
||||
sensitive = true
|
||||
description = "DigitalOcean API token"
|
||||
}
|
||||
|
||||
variable "agent_name" {
|
||||
type = string
|
||||
description = "Agent identifier (e.g. claude, codex, openclaw)"
|
||||
}
|
||||
|
||||
variable "cloud_init_tier" {
|
||||
type = string
|
||||
default = "full"
|
||||
description = "Package tier: minimal, node, bun, full"
|
||||
}
|
||||
|
||||
variable "install_commands" {
|
||||
type = list(string)
|
||||
default = []
|
||||
description = "Shell commands to install the agent"
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "nyc3"
|
||||
description = "Build region"
|
||||
}
|
||||
|
||||
variable "size" {
|
||||
type = string
|
||||
default = "s-2vcpu-4gb"
|
||||
description = "Droplet size for the build VM"
|
||||
}
|
||||
|
||||
variable "base_image" {
|
||||
type = string
|
||||
default = "ubuntu-24-04-x64"
|
||||
description = "Base image slug"
|
||||
}
|
||||
|
||||
# ─── Locals ──────────────────────────────────────────────────────────────────
|
||||
|
||||
locals {
|
||||
snapshot_name = "spawn-${var.agent_name}-${formatdate("YYYYMMDD", timestamp())}"
|
||||
}
|
||||
|
||||
# ─── Source ──────────────────────────────────────────────────────────────────
|
||||
|
||||
source "digitalocean" "agent" {
|
||||
api_token = var.do_api_token
|
||||
image = var.base_image
|
||||
region = var.region
|
||||
size = var.size
|
||||
ssh_username = "root"
|
||||
snapshot_name = local.snapshot_name
|
||||
|
||||
snapshot_regions = [
|
||||
"nyc1", "nyc3", "sfo3", "ams3", "sgp1",
|
||||
"lon1", "fra1", "tor1", "blr1", "syd1",
|
||||
]
|
||||
|
||||
tags = ["spawn", "spawn-${var.agent_name}"]
|
||||
}
|
||||
|
||||
# ─── Build ───────────────────────────────────────────────────────────────────
|
||||
|
||||
build {
|
||||
sources = ["source.digitalocean.agent"]
|
||||
|
||||
# 1. System update
|
||||
provisioner "shell" {
|
||||
inline = [
|
||||
"export DEBIAN_FRONTEND=noninteractive",
|
||||
"apt-get update -y",
|
||||
"apt-get upgrade -y -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'",
|
||||
]
|
||||
}
|
||||
|
||||
# 2. Tier packages + runtimes
|
||||
provisioner "shell" {
|
||||
script = "scripts/tier-${var.cloud_init_tier}.sh"
|
||||
}
|
||||
|
||||
# 3. Agent install (15 min timeout, 2 retries via wrapper)
|
||||
provisioner "shell" {
|
||||
inline = var.install_commands
|
||||
timeout = "15m"
|
||||
max_retries = 2
|
||||
expect_disconnect = false
|
||||
environment_vars = [
|
||||
"HOME=/root",
|
||||
"DEBIAN_FRONTEND=noninteractive",
|
||||
]
|
||||
}
|
||||
|
||||
# 4. Marker file + PATH setup
|
||||
provisioner "shell" {
|
||||
inline = [
|
||||
"echo 'agent=${var.agent_name}' > /root/.spawn-snapshot",
|
||||
"echo 'built=${formatdate("YYYY-MM-DD", timestamp())}' >> /root/.spawn-snapshot",
|
||||
"for rc in /root/.bashrc /root/.zshrc; do grep -q '.bun/bin' \"$rc\" 2>/dev/null || echo 'export PATH=\"$HOME/.local/bin:$HOME/.bun/bin:$PATH\"' >> \"$rc\"; done",
|
||||
]
|
||||
}
|
||||
|
||||
# 5. Cleanup
|
||||
provisioner "shell" {
|
||||
inline = [
|
||||
"apt-get clean",
|
||||
"rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*",
|
||||
"rm -f /var/log/cloud-init*.log /var/log/syslog /var/log/auth.log",
|
||||
"truncate -s 0 /var/log/lastlog /var/log/wtmp /var/log/btmp 2>/dev/null || true",
|
||||
"sync",
|
||||
]
|
||||
}
|
||||
}
|
||||
22
packer/scripts/tier-bun.sh
Normal file
22
packer/scripts/tier-bun.sh
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
unzip \
|
||||
git \
|
||||
ca-certificates \
|
||||
zsh
|
||||
|
||||
# Bun
|
||||
if ! command -v bun >/dev/null 2>&1; then
|
||||
curl --proto '=https' -fsSL https://bun.sh/install | bash
|
||||
fi
|
||||
ln -sf /root/.bun/bin/bun /usr/local/bin/bun 2>/dev/null || true
|
||||
|
||||
# PATH setup
|
||||
for rc in /root/.bashrc /root/.zshrc; do
|
||||
grep -q '.bun/bin' "$rc" 2>/dev/null || printf 'export PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH"\n' >> "$rc"
|
||||
done
|
||||
26
packer/scripts/tier-full.sh
Normal file
26
packer/scripts/tier-full.sh
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
unzip \
|
||||
git \
|
||||
ca-certificates \
|
||||
zsh \
|
||||
build-essential
|
||||
|
||||
# Node.js 22 via n
|
||||
curl --proto '=https' -fsSL https://raw.githubusercontent.com/tj/n/master/bin/n | bash -s install 22
|
||||
|
||||
# Bun
|
||||
if ! command -v bun >/dev/null 2>&1; then
|
||||
curl --proto '=https' -fsSL https://bun.sh/install | bash
|
||||
fi
|
||||
ln -sf /root/.bun/bin/bun /usr/local/bin/bun 2>/dev/null || true
|
||||
|
||||
# PATH setup
|
||||
for rc in /root/.bashrc /root/.zshrc; do
|
||||
grep -q '.bun/bin' "$rc" 2>/dev/null || printf 'export PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH"\n' >> "$rc"
|
||||
done
|
||||
10
packer/scripts/tier-minimal.sh
Normal file
10
packer/scripts/tier-minimal.sh
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
unzip \
|
||||
git \
|
||||
ca-certificates
|
||||
20
packer/scripts/tier-node.sh
Normal file
20
packer/scripts/tier-node.sh
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
unzip \
|
||||
git \
|
||||
ca-certificates \
|
||||
zsh \
|
||||
build-essential
|
||||
|
||||
# Node.js 22 via n
|
||||
curl --proto '=https' -fsSL https://raw.githubusercontent.com/tj/n/master/bin/n | bash -s install 22
|
||||
|
||||
# PATH setup
|
||||
for rc in /root/.bashrc /root/.zshrc; do
|
||||
grep -q '.bun/bin' "$rc" 2>/dev/null || printf 'export PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH"\n' >> "$rc"
|
||||
done
|
||||
Loading…
Add table
Add a link
Reference in a new issue