import process from "node:process" import { pathToFileURL } from "node:url" const RENDER_API_BASE_URL = "https://api.render.com/v1" const SNAPSHOT_ENV_VAR_KEY = "DAYTONA_SNAPSHOT" function requiredValue(value, message) { if (typeof value !== "string" || value.trim().length === 0) { throw new Error(message) } return value.trim() } function validateSnapshotName(snapshotName) { const value = requiredValue( snapshotName, "Missing snapshot name. Pass --snapshot .", ) if (value !== snapshotName || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value)) { throw new Error( `Invalid snapshot name ${JSON.stringify(snapshotName)}. Use only letters, numbers, dots, underscores, and hyphens, with no whitespace.`, ) } return value } function envVarUrl(envGroupId) { const id = requiredValue( envGroupId, "Missing env group id. Pass --env-group-id .", ) return `${RENDER_API_BASE_URL}/env-groups/${encodeURIComponent(id)}/env-vars/${SNAPSHOT_ENV_VAR_KEY}` } function authorizationHeaders(apiKey) { const key = requiredValue( apiKey, "Missing Render API key. Set RENDER_API_KEY before running this command.", ) return { Accept: "application/json", Authorization: `Bearer ${key}`, } } async function responseBody(response) { const body = await response.text() if (!response.ok) { const statusText = response.statusText ? ` ${response.statusText}` : "" throw new Error( `Render API request failed with HTTP ${response.status}${statusText}: ${body}`, ) } return body } export function updateRequest(envGroupId, snapshotName) { const value = validateSnapshotName(snapshotName) return { method: "PUT", url: envVarUrl(envGroupId), body: JSON.stringify({ value }), } } export async function readDaytonaSnapshotPin({ envGroupId, apiKey, fetchImpl = globalThis.fetch, }) { const url = envVarUrl(envGroupId) const response = await fetchImpl(url, { method: "GET", headers: authorizationHeaders(apiKey), }) const body = await responseBody(response) let parsed try { parsed = JSON.parse(body) } catch { throw new Error(`Render API returned invalid JSON for ${SNAPSHOT_ENV_VAR_KEY}: ${body}`) } if ( !parsed || typeof parsed !== "object" || Array.isArray(parsed) || typeof parsed.value !== "string" ) { throw new Error( `Render API response did not contain a string value for ${SNAPSHOT_ENV_VAR_KEY}: ${body}`, ) } return parsed.value } export async function updateDaytonaSnapshotPin({ envGroupId, snapshotName, apiKey, dryRun = false, fetchImpl = globalThis.fetch, log = console.log, }) { const request = updateRequest(envGroupId, snapshotName) authorizationHeaders(apiKey) if (dryRun) { log("dry-run: no request sent") log(`method: ${request.method}`) log(`url: ${request.url}`) log(`body: ${request.body}`) return { status: "dry-run", snapshotName } } const current = await readDaytonaSnapshotPin({ envGroupId, apiKey, fetchImpl, }) if (current === snapshotName) { const message = `unchanged: ${SNAPSHOT_ENV_VAR_KEY} is already ${snapshotName}` log(message) return { status: "unchanged", current, snapshotName, message } } const response = await fetchImpl(request.url, { method: request.method, headers: { ...authorizationHeaders(apiKey), "Content-Type": "application/json", }, body: request.body, }) await responseBody(response) const message = `updated: ${SNAPSHOT_ENV_VAR_KEY} ${current} -> ${snapshotName}` log(message) return { status: "updated", current, snapshotName, message } } export async function compareDaytonaSnapshotPin({ envGroupId, snapshotName, apiKey, fetchImpl = globalThis.fetch, log = console.log, }) { const expected = validateSnapshotName(snapshotName) const current = await readDaytonaSnapshotPin({ envGroupId, apiKey, fetchImpl, }) if (current !== expected) { throw new Error( `Daytona snapshot drift: expected ${SNAPSHOT_ENV_VAR_KEY}=${expected}, found ${SNAPSHOT_ENV_VAR_KEY}=${current}`, ) } const message = `agreement: ${SNAPSHOT_ENV_VAR_KEY}=${expected}` log(message) return { status: "agreement", current, snapshotName: expected, message } } function optionValue(args, index, option) { const value = args[index + 1] if (!value || value.startsWith("--")) { throw new Error(`Missing value for ${option}.`) } return value } export const USAGE = `Update or check the Daytona sandbox snapshot pin (DAYTONA_SNAPSHOT) in a Render env group. Usage: node scripts/update-daytona-snapshot-pin.mjs --env-group-id --snapshot [--dry-run] node scripts/update-daytona-snapshot-pin.mjs --env-group-id --snapshot --compare node scripts/update-daytona-snapshot-pin.mjs --env-group-id --read Options: --env-group-id Render env group holding DAYTONA_SNAPSHOT (e.g. evg-...). --snapshot Desired snapshot name (e.g. openwork-0.18.11). --dry-run Print the request that would be sent; write nothing. --compare Read-only drift check; exits non-zero when the pin differs. --read Print the current snapshot pin; write nothing. -h, --help Show this help. Environment: RENDER_API_KEY Required (except with --help). This mutates which image every cloud sandbox boots. Prefer --dry-run or --compare first.` export function parseArgs(args) { const options = { envGroupId: "", snapshotName: "", dryRun: false, compare: false, read: false, help: false, } for (let index = 0; index < args.length; index += 1) { const argument = args[index] if (argument === "--env-group-id") { options.envGroupId = optionValue(args, index, argument) index += 1 } else if (argument === "--snapshot") { options.snapshotName = optionValue(args, index, argument) index += 1 } else if (argument === "--dry-run") { options.dryRun = true } else if (argument === "--compare") { options.compare = true } else if (argument === "--read") { options.read = true } else if (argument === "--help" || argument === "-h") { options.help = true } else { throw new Error(`Unknown argument: ${argument}`) } } if (options.help) { return options } if (options.dryRun && options.compare) { throw new Error("--dry-run and --compare cannot be used together.") } if (options.read && (options.dryRun || options.compare)) { throw new Error("--read cannot be used with --dry-run or --compare.") } return options } export async function main(args, environment = process.env) { const options = parseArgs(args) if (options.help) { console.log(USAGE) return { status: "help" } } const input = { envGroupId: options.envGroupId, snapshotName: options.snapshotName, apiKey: environment.RENDER_API_KEY, } if (options.read) { const current = await readDaytonaSnapshotPin(input) console.log(current) return { status: "read", current } } if (options.compare) { return compareDaytonaSnapshotPin(input) } return updateDaytonaSnapshotPin({ ...input, dryRun: options.dryRun }) } const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href if (isMain) { main(process.argv.slice(2)).catch((error) => { const message = error instanceof Error ? error.message : String(error) console.error(`Error: ${message}`) process.exitCode = 1 }) }