mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
fix: snapshot cleanup + claude install (name-prefix filter) (#2273)
* fix: claude snapshot build — remove npm fallback from install command
The native install (curl | bash) succeeds but exits non-zero due to a
PATH warning. The || fallback then tries `npm install` which doesn't
exist on the "minimal" tier → exit 127.
Fix: replace npm fallback with binary existence check (same pattern
as hermes agent). If install exits non-zero but ~/.local/bin/claude
exists, the build succeeds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: snapshot cleanup and lookup — use name prefix instead of tags
DO Packer builder `tags` only apply to the temporary build droplet,
not the resulting snapshot image. Both the workflow cleanup step and
the CLI's findSpawnSnapshot() were querying by `tag_name` which
returned nothing — old snapshots piled up and the CLI couldn't find
existing snapshots.
Fix: filter by snapshot name prefix (`spawn-{agent}-`) instead of
tags, in both the workflow and the CLI. Remove misleading `tags`
from the Packer template. Add test cases for name-prefix filtering.
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
c3cb98daab
commit
d77a067aa4
6 changed files with 64 additions and 15 deletions
8
.github/workflows/packer-snapshots.yml
vendored
8
.github/workflows/packer-snapshots.yml
vendored
|
|
@ -90,10 +90,12 @@ jobs:
|
|||
- name: Cleanup old snapshots
|
||||
if: success()
|
||||
run: |
|
||||
# Keep only the latest snapshot per agent
|
||||
# DO snapshots don't support tags — filter by name prefix instead
|
||||
PREFIX="spawn-${AGENT_NAME}-"
|
||||
SNAPSHOTS=$(curl -s -H "Authorization: Bearer ${DO_API_TOKEN}" \
|
||||
"https://api.digitalocean.com/v2/images?private=true&per_page=100&tag_name=spawn-${AGENT_NAME}" \
|
||||
| jq -r '.images | sort_by(.created_at) | reverse | .[1:] | .[].id')
|
||||
"https://api.digitalocean.com/v2/images?private=true&per_page=100" \
|
||||
| jq -r --arg prefix "$PREFIX" \
|
||||
'[.images[] | select(.name | startswith($prefix))] | sort_by(.created_at) | reverse | .[1:] | .[].id')
|
||||
|
||||
for ID in $SNAPSHOTS; do
|
||||
echo "Deleting old snapshot: ${ID}"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.15.6",
|
||||
"version": "0.15.7",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* do-snapshot.test.ts — Tests for findSpawnSnapshot().
|
||||
*
|
||||
* Verifies snapshot lookup: happy path, empty results, API errors,
|
||||
* invalid IDs, and network failures all return correct values.
|
||||
* invalid IDs, name filtering, and network failures.
|
||||
*/
|
||||
|
||||
import { afterAll, afterEach, describe, expect, it, mock } from "bun:test";
|
||||
|
|
@ -37,14 +37,17 @@ describe("findSpawnSnapshot", () => {
|
|||
images: [
|
||||
{
|
||||
id: 100,
|
||||
name: "spawn-claude-20260101-0000",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
id: 200,
|
||||
name: "spawn-claude-20260301-0000",
|
||||
created_at: "2026-03-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
id: 150,
|
||||
name: "spawn-claude-20260201-0000",
|
||||
created_at: "2026-02-01T00:00:00Z",
|
||||
},
|
||||
],
|
||||
|
|
@ -57,6 +60,32 @@ describe("findSpawnSnapshot", () => {
|
|||
expect(result).toBe("200");
|
||||
});
|
||||
|
||||
it("filters by name prefix — ignores other agents", async () => {
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
images: [
|
||||
{
|
||||
id: 300,
|
||||
name: "spawn-codex-20260301-0000",
|
||||
created_at: "2026-03-01T00:00:00Z",
|
||||
},
|
||||
{
|
||||
id: 400,
|
||||
name: "spawn-claude-20260201-0000",
|
||||
created_at: "2026-02-01T00:00:00Z",
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const result = await findSpawnSnapshot("claude");
|
||||
expect(result).toBe("400");
|
||||
});
|
||||
|
||||
it("returns null when no images are found", async () => {
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve(
|
||||
|
|
@ -72,6 +101,27 @@ describe("findSpawnSnapshot", () => {
|
|||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when no images match the agent name", async () => {
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
images: [
|
||||
{
|
||||
id: 100,
|
||||
name: "spawn-codex-20260101-0000",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const result = await findSpawnSnapshot("claude");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null on API error response", async () => {
|
||||
globalThis.fetch = mock(() =>
|
||||
Promise.resolve(
|
||||
|
|
@ -93,6 +143,7 @@ describe("findSpawnSnapshot", () => {
|
|||
images: [
|
||||
{
|
||||
id: "not-a-number",
|
||||
name: "spawn-claude-20260101-0000",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -887,14 +887,12 @@ async function waitForDropletActive(dropletId: string, maxAttempts = 60): Promis
|
|||
|
||||
export async function findSpawnSnapshot(agentName: string): Promise<string | null> {
|
||||
try {
|
||||
const text = await doApi(
|
||||
"GET",
|
||||
`/images?private=true&per_page=50&tag_name=spawn-${encodeURIComponent(agentName)}`,
|
||||
undefined,
|
||||
1,
|
||||
);
|
||||
// DO snapshots don't support tags — filter by name prefix instead
|
||||
const prefix = `spawn-${agentName}-`;
|
||||
const text = await doApi("GET", "/images?private=true&per_page=100", undefined, 1);
|
||||
const data = parseJsonObj(text);
|
||||
const images = toObjectArray(data?.images);
|
||||
const allImages = toObjectArray(data?.images);
|
||||
const images = allImages.filter((img) => isString(img.name) && img.name.startsWith(prefix));
|
||||
if (images.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"claude": {
|
||||
"tier": "minimal",
|
||||
"install": [
|
||||
"curl -fsSL https://claude.ai/install.sh | bash || mkdir -p ~/.npm-global/bin && npm install -g --prefix ~/.npm-global @anthropic-ai/claude-code"
|
||||
"curl -fsSL https://claude.ai/install.sh | bash || [ -f /root/.local/bin/claude ]"
|
||||
]
|
||||
},
|
||||
"codex": {
|
||||
|
|
|
|||
|
|
@ -45,8 +45,6 @@ source "digitalocean" "spawn" {
|
|||
"nyc1", "nyc3", "sfo3", "tor1", "ams3",
|
||||
"lon1", "fra1", "blr1", "sgp1", "syd1",
|
||||
]
|
||||
|
||||
tags = ["spawn", "spawn-${var.agent_name}"]
|
||||
}
|
||||
|
||||
build {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue