import * as fs from "node:fs/promises"; import { join } from "node:path"; import { $ } from "execa"; import { glob } from "glob"; import * as semver from "semver"; import type { ReleaseOpts } from "./main"; function assert(condition: any, message?: string): asserts condition { if (!condition) { throw new Error(message || "Assertion failed"); } } // Files containing version references that need channel/image tag updates. // Keep in sync with CLAUDE.md "Install Version References" section. const VERSION_REFERENCE_FILES = [ "README.md", "docs/acp-http-client.mdx", "docs/cli.mdx", "docs/quickstart.mdx", "docs/sdk-overview.mdx", "docs/react-components.mdx", "docs/session-persistence.mdx", "docs/architecture.mdx", "docs/deploy/local.mdx", "docs/deploy/cloudflare.mdx", "docs/deploy/vercel.mdx", "docs/deploy/daytona.mdx", "docs/deploy/agentcomputer.mdx", "docs/deploy/e2b.mdx", "docs/deploy/docker.mdx", "docs/deploy/boxlite.mdx", "docs/deploy/modal.mdx", "docs/deploy/computesdk.mdx", "frontend/packages/website/src/components/GetStarted.tsx", ".claude/commands/post-release-testing.md", "examples/cloudflare/Dockerfile", "examples/boxlite/Dockerfile", "examples/boxlite-python/Dockerfile", "examples/daytona/src/index.ts", "examples/shared/src/docker.ts", "examples/docker/src/index.ts", "examples/e2b/src/index.ts", "examples/vercel/src/index.ts", "sdks/typescript/src/providers/shared.ts", "scripts/release/main.ts", "scripts/release/promote-artifacts.ts", "scripts/release/sdk.ts", ]; export async function updateVersion(opts: ReleaseOpts) { // 1. Read current version from Cargo.toml before overwriting const cargoTomlPath = join(opts.root, "Cargo.toml"); let cargoContent = await fs.readFile(cargoTomlPath, "utf-8"); const oldVersionMatch = cargoContent.match(/\[workspace\.package\]\nversion = "([^"]+)"/); assert(oldVersionMatch, "Could not find workspace.package version in Cargo.toml"); const oldVersion = oldVersionMatch[1]; const oldParsed = semver.parse(oldVersion); assert(oldParsed, `Could not parse old version: ${oldVersion}`); const oldMinorChannel = `${oldParsed.major}.${oldParsed.minor}.x`; // Update [workspace.package] version cargoContent = cargoContent.replace(/\[workspace\.package\]\nversion = ".*"/, `[workspace.package]\nversion = "${opts.version}"`); // Discover internal crates from [workspace.dependencies] by matching // lines with both `version = "..."` and `path = "..."` (internal path deps) const internalCratePattern = /^(\S+)\s*=\s*\{[^}]*version\s*=\s*"[^"]+"\s*,[^}]*path\s*=/gm; let match; const internalCrates: string[] = []; while ((match = internalCratePattern.exec(cargoContent)) !== null) { internalCrates.push(match[1]); } console.log(`Discovered ${internalCrates.length} internal crates to version-bump:`); for (const crate of internalCrates) console.log(` - ${crate}`); for (const crate of internalCrates) { const pattern = new RegExp(`(${crate.replace(/-/g, "-")} = \\{ version = ")[^"]+(",)`, "g"); cargoContent = cargoContent.replace(pattern, `$1${opts.version}$2`); } await fs.writeFile(cargoTomlPath, cargoContent); await $({ cwd: opts.root })`git add Cargo.toml`; // 2. Discover and update all non-private SDK package.json versions const packageJsonPaths = await glob("sdks/**/package.json", { cwd: opts.root, ignore: ["**/node_modules/**"], }); // Filter to non-private packages only const toUpdate: string[] = []; for (const relPath of packageJsonPaths) { const fullPath = join(opts.root, relPath); const content = await fs.readFile(fullPath, "utf-8"); const pkg = JSON.parse(content); if (pkg.private) continue; toUpdate.push(relPath); } console.log(`Discovered ${toUpdate.length} SDK package.json files to version-bump:`); for (const relPath of toUpdate) console.log(` - ${relPath}`); for (const relPath of toUpdate) { const fullPath = join(opts.root, relPath); const content = await fs.readFile(fullPath, "utf-8"); const versionPattern = /"version": ".*"/; assert(versionPattern.test(content), `No version field in ${relPath}`); const updated = content.replace(versionPattern, `"version": "${opts.version}"`); await fs.writeFile(fullPath, updated); await $({ cwd: opts.root })`git add ${relPath}`; } // 3. Update version references across docs, examples, and code await updateVersionReferences(opts, oldVersion, oldMinorChannel); } async function updateVersionReferences(opts: ReleaseOpts, oldVersion: string, oldMinorChannel: string) { const newMinorChannel = opts.minorVersionChannel; // Find old Docker image tags by scanning for rivetdev/sandbox-agent:-full patterns // The old version might be a different patch or RC, so we match any version-full tag const oldDockerTagPattern = /rivetdev\/sandbox-agent:([0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9.]+)?)-full/; console.log(`\nUpdating version references:`); console.log(` Old minor channel: ${oldMinorChannel}`); console.log(` New minor channel: ${newMinorChannel}`); console.log(` New Docker tag: ${opts.version}-full`); const modifiedFiles: string[] = []; for (const relPath of VERSION_REFERENCE_FILES) { const fullPath = join(opts.root, relPath); let content: string; try { content = await fs.readFile(fullPath, "utf-8"); } catch (err: any) { if (err.code === "ENOENT") { console.log(` ⚠️ Skipping ${relPath} (file not found)`); continue; } throw err; } const original = content; // Replace minor channel references (e.g. sandbox-agent@0.5.x -> sandbox-agent@0.5.x) content = content.replaceAll(`sandbox-agent@${oldMinorChannel}`, `sandbox-agent@${newMinorChannel}`); content = content.replaceAll(`@sandbox-agent/cli@${oldMinorChannel}`, `@sandbox-agent/cli@${newMinorChannel}`); content = content.replaceAll(`@sandbox-agent/react@${oldMinorChannel}`, `@sandbox-agent/react@${newMinorChannel}`); // Replace install script URL channel content = content.replaceAll(`releases.rivet.dev/sandbox-agent/${oldMinorChannel}/`, `releases.rivet.dev/sandbox-agent/${newMinorChannel}/`); // If references drifted (for example Cargo.toml version was bumped without updating docs), // normalize any other pinned minor-channel references to the release's channel. content = content.replaceAll(/sandbox-agent@0\.\d+\.x/g, `sandbox-agent@${newMinorChannel}`); content = content.replaceAll(/@sandbox-agent\/cli@0\.\d+\.x/g, `@sandbox-agent/cli@${newMinorChannel}`); content = content.replaceAll(/@sandbox-agent\/react@0\.\d+\.x/g, `@sandbox-agent/react@${newMinorChannel}`); content = content.replaceAll(/releases\.rivet\.dev\/sandbox-agent\/0\.\d+\.x\//g, `releases.rivet.dev/sandbox-agent/${newMinorChannel}/`); // Replace Docker image tags (rivetdev/sandbox-agent:-full -> rivetdev/sandbox-agent:-full) content = content.replaceAll( new RegExp(`rivetdev/sandbox-agent:[0-9]+\\.[0-9]+\\.[0-9]+(?:-[a-zA-Z0-9.]+)?-full`, "g"), `rivetdev/sandbox-agent:${opts.version}-full`, ); // Replace standalone version-full references in prose (e.g. "The `0.3.2-full` tag pins...") // Match backtick-wrapped version-full patterns content = content.replaceAll(new RegExp("`[0-9]+\\.[0-9]+\\.[0-9]+(?:-[a-zA-Z0-9.]+)?-full`", "g"), `\`${opts.version}-full\``); if (content !== original) { await fs.writeFile(fullPath, content); modifiedFiles.push(relPath); console.log(` ✅ ${relPath}`); } } if (modifiedFiles.length > 0) { await $({ cwd: opts.root })`git add -f ${modifiedFiles}`; console.log(`\nUpdated ${modifiedFiles.length} files with version references.`); } else { console.log(`\nNo version reference files needed updates.`); } }