From 46193747e66c88bc8ddb0edd3952a2dbd706b8b8 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Wed, 11 Feb 2026 17:51:32 -0800 Subject: [PATCH] fix: dynamically discover packages in release script instead of hardcoding - sdk.ts: discoverNpmPackages() + topoSort() for library packages - sdk.ts: discoverCrates() via cargo metadata for workspace crates - sdk.ts: publishNpmLibraries replaces publishNpmCliShared + publishNpmSdk - sdk.ts: publishNpmCli now discovers CLI packages from filesystem - update_version.ts: discovers SDK package.json files via glob - update_version.ts: discovers internal crates from Cargo.toml path deps This prevents packages like persist-*, acp-http-client from being silently skipped during releases. --- scripts/release/main.ts | 19 +- scripts/release/sdk.ts | 364 +++++++++++++++--------------- scripts/release/update_version.ts | 125 +++++----- 3 files changed, 240 insertions(+), 268 deletions(-) diff --git a/scripts/release/main.ts b/scripts/release/main.ts index 1a8c3a3..426b322 100755 --- a/scripts/release/main.ts +++ b/scripts/release/main.ts @@ -13,7 +13,7 @@ import { createGitHubRelease, validateGit, } from "./git"; -import { publishCrates, publishNpmCli, publishNpmCliShared, publishNpmSdk } from "./sdk"; +import { publishCrates, publishNpmCli, publishNpmLibraries } from "./sdk"; import { updateVersion } from "./update_version"; import { assert, assertEquals, fetchGitRef, versionOrCommitToRef } from "./utils"; @@ -282,8 +282,7 @@ const STEPS = [ "run-ci-checks", "build-js-artifacts", "publish-crates", - "publish-npm-cli-shared", - "publish-npm-sdk", + "publish-npm-libraries", "publish-npm-cli", "tag-docker", "promote-artifacts", @@ -324,8 +323,7 @@ const PHASE_MAP: Record = { "complete-ci": [ "update-version", "publish-crates", - "publish-npm-cli-shared", - "publish-npm-sdk", + "publish-npm-libraries", "publish-npm-cli", "tag-docker", "promote-artifacts", @@ -604,14 +602,9 @@ async function main() { await publishCrates(releaseOpts); } - if (shouldRunStep("publish-npm-cli-shared")) { - console.log("==> Publishing NPM CLI Shared"); - await publishNpmCliShared(releaseOpts); - } - - if (shouldRunStep("publish-npm-sdk")) { - console.log("==> Publishing NPM SDK"); - await publishNpmSdk(releaseOpts); + if (shouldRunStep("publish-npm-libraries")) { + console.log("==> Publishing NPM Libraries"); + await publishNpmLibraries(releaseOpts); } if (shouldRunStep("publish-npm-cli")) { diff --git a/scripts/release/sdk.ts b/scripts/release/sdk.ts index 66caa51..aa6ef2f 100644 --- a/scripts/release/sdk.ts +++ b/scripts/release/sdk.ts @@ -1,38 +1,13 @@ import { $ } from "execa"; import * as fs from "node:fs/promises"; -import { join } from "node:path"; +import { dirname, join, relative } from "node:path"; +import { glob } from "glob"; import type { ReleaseOpts } from "./main"; import { downloadFromReleases, PREFIX } from "./utils"; -// Crates to publish in dependency order -const CRATES = [ - "error", - "agent-credentials", - "agent-management", - "opencode-server-manager", - "opencode-adapter", - "acp-http-adapter", - "sandbox-agent", - "gigacode", -] as const; - -// NPM CLI packages -const CLI_PACKAGES = [ - "@sandbox-agent/cli", - "@sandbox-agent/cli-linux-x64", - "@sandbox-agent/cli-linux-arm64", - "@sandbox-agent/cli-win32-x64", - "@sandbox-agent/cli-darwin-x64", - "@sandbox-agent/cli-darwin-arm64", - "@sandbox-agent/gigacode", - "@sandbox-agent/gigacode-linux-x64", - "@sandbox-agent/gigacode-linux-arm64", - "@sandbox-agent/gigacode-win32-x64", - "@sandbox-agent/gigacode-darwin-x64", - "@sandbox-agent/gigacode-darwin-arm64", -] as const; - -// Mapping from npm package name to Rust target and binary extension +// ─── Platform binary mapping (npm package → Rust target) ──────────────────── +// Maps CLI platform packages to their Rust build targets. +// Keep in sync when adding new target platforms. const CLI_PLATFORM_MAP: Record< string, { target: string; binaryExt: string; binaryName: string } @@ -89,6 +64,8 @@ const CLI_PLATFORM_MAP: Record< }, }; +// ─── Shared helpers ───────────────────────────────────────────────────────── + async function npmVersionExists( packageName: string, version: string, @@ -131,7 +108,6 @@ async function crateVersionExists( stdout: "pipe", stderr: "pipe", })`cargo search ${crateName} --limit 1`; - // cargo search output format: "cratename = \"version\" # description" const output = result.stdout; const match = output.match(new RegExp(`^${crateName}\\s*=\\s*"([^"]+)"`)); if (match && match[1] === version) { @@ -139,62 +115,148 @@ async function crateVersionExists( } return false; } catch (error: any) { - // If cargo search fails, assume crate doesn't exist return false; } } +// ─── Package discovery ────────────────────────────────────────────────────── + +interface NpmPackageInfo { + name: string; + dir: string; + hasBuildScript: boolean; + localDeps: string[]; +} + +/** + * Discover non-private npm packages matching the given glob patterns. + */ +async function discoverNpmPackages(root: string, patterns: string[]): Promise { + const packages: NpmPackageInfo[] = []; + + for (const pattern of patterns) { + const matches = await glob(pattern, { cwd: root }); + for (const match of matches) { + const fullPath = join(root, match); + const pkg = JSON.parse(await fs.readFile(fullPath, "utf-8")); + if (pkg.private) continue; + + const allDeps = { ...pkg.dependencies, ...pkg.peerDependencies }; + const localDeps = Object.entries(allDeps || {}) + .filter(([_, v]) => String(v).startsWith("workspace:")) + .map(([k]) => k); + + packages.push({ + name: pkg.name, + dir: dirname(fullPath), + hasBuildScript: !!pkg.scripts?.build, + localDeps, + }); + } + } + + return packages; +} + +/** + * Topologically sort packages so dependencies are published before dependents. + */ +function topoSort(packages: NpmPackageInfo[]): NpmPackageInfo[] { + const byName = new Map(packages.map(p => [p.name, p])); + const visited = new Set(); + const result: NpmPackageInfo[] = []; + + function visit(pkg: NpmPackageInfo) { + if (visited.has(pkg.name)) return; + visited.add(pkg.name); + for (const dep of pkg.localDeps) { + const d = byName.get(dep); + if (d) visit(d); + } + result.push(pkg); + } + + for (const pkg of packages) visit(pkg); + return result; +} + +interface CrateInfo { + name: string; + dir: string; +} + +/** + * Discover workspace crates via `cargo metadata` and return them in dependency order. + */ +async function discoverCrates(root: string): Promise { + const result = await $({ cwd: root, stdout: "pipe" })`cargo metadata --no-deps --format-version 1`; + const metadata = JSON.parse(result.stdout); + + const memberIds = new Set(metadata.workspace_members); + const workspacePackages = metadata.packages.filter((p: any) => memberIds.has(p.id)); + + // Build name→package map for topo sort + const byName = new Map(workspacePackages.map((p: any) => [p.name, p])); + + const visited = new Set(); + const sorted: CrateInfo[] = []; + + function visit(pkg: any) { + if (visited.has(pkg.name)) return; + visited.add(pkg.name); + for (const dep of pkg.dependencies) { + const internal = byName.get(dep.name); + if (internal) visit(internal); + } + sorted.push({ + name: pkg.name, + dir: dirname(pkg.manifest_path), + }); + } + + for (const pkg of workspacePackages) visit(pkg); + return sorted; +} + +// ─── Crate publishing ─────────────────────────────────────────────────────── + export async function publishCrates(opts: ReleaseOpts) { - console.log("==> Publishing crates to crates.io"); + console.log("==> Discovering workspace crates"); + const crates = await discoverCrates(opts.root); - for (const crate of CRATES) { - const cratePath = crate === "gigacode" - ? join(opts.root, "gigacode") - : join(opts.root, "server/packages", crate); + console.log(`Found ${crates.length} crates to publish:`); + for (const c of crates) console.log(` - ${c.name}`); - // Read Cargo.toml to get the actual crate name - const cargoTomlPath = join(cratePath, "Cargo.toml"); - const cargoToml = await fs.readFile(cargoTomlPath, "utf-8"); - const nameMatch = cargoToml.match(/^name\s*=\s*"([^"]+)"/m); - const crateName = nameMatch ? nameMatch[1] : `sandbox-agent-${crate}`; - - // Check if version already exists - const versionExists = await crateVersionExists(crateName, opts.version); + for (const crate of crates) { + const versionExists = await crateVersionExists(crate.name, opts.version); if (versionExists) { console.log( - `Version ${opts.version} of ${crateName} already exists on crates.io. Skipping...`, + `Version ${opts.version} of ${crate.name} already exists on crates.io. Skipping...`, ); continue; } - // Publish - // Use --no-verify to skip the verification step because: - // 1. Code was already built/checked in the setup phase - // 2. Verification downloads published dependencies which may not have the latest - // changes yet (crates.io indexing takes time) - console.log(`==> Publishing to crates.io: ${crateName}@${opts.version}`); + console.log(`==> Publishing to crates.io: ${crate.name}@${opts.version}`); try { await $({ stdout: "pipe", stderr: "pipe", - cwd: cratePath, + cwd: crate.dir, })`cargo publish --allow-dirty --no-verify`; - console.log(`✅ Published ${crateName}@${opts.version}`); + console.log(`✅ Published ${crate.name}@${opts.version}`); } catch (err: any) { - // Check if error is because crate already exists (from a previous partial run) if (err.stderr?.includes("already exists")) { console.log( - `Version ${opts.version} of ${crateName} already exists on crates.io. Skipping...`, + `Version ${opts.version} of ${crate.name} already exists on crates.io. Skipping...`, ); continue; } - console.error(`❌ Failed to publish ${crateName}`); + console.error(`❌ Failed to publish ${crate.name}`); console.error(err.stderr || err.message); throw err; } - // Wait a bit for crates.io to index the new version (needed for dependency resolution) console.log("Waiting for crates.io to index..."); await new Promise((resolve) => setTimeout(resolve, 30000)); } @@ -202,108 +264,68 @@ export async function publishCrates(opts: ReleaseOpts) { console.log("✅ All crates published"); } -export async function publishNpmCliShared(opts: ReleaseOpts) { - const cliSharedPath = join(opts.root, "sdks/cli-shared"); - const packageJsonPath = join(cliSharedPath, "package.json"); - const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8")); - const name = packageJson.name; +// ─── NPM library publishing ──────────────────────────────────────────────── - // Check if version already exists - const versionExists = await npmVersionExists(name, opts.version); - if (versionExists) { - console.log( - `Version ${opts.version} of ${name} already exists. Skipping...`, - ); - return; - } +/** + * Discover and publish all non-private library packages under sdks/. + * Excludes CLI/gigacode wrapper and platform packages (handled by publishNpmCli). + * Publishes in dependency order via topological sort. + */ +export async function publishNpmLibraries(opts: ReleaseOpts) { + console.log("==> Discovering library packages"); + const all = await discoverNpmPackages(opts.root, ["sdks/*/package.json"]); - // Build cli-shared - console.log(`==> Building @sandbox-agent/cli-shared`); - await $({ - stdio: "inherit", - cwd: opts.root, - })`pnpm --filter @sandbox-agent/cli-shared build`; + // Exclude CLI and gigacode directories (handled by publishNpmCli) + const libraries = all.filter(p => { + const rel = relative(opts.root, p.dir); + return !rel.startsWith("sdks/cli") && !rel.startsWith("sdks/gigacode"); + }); - // Publish - console.log(`==> Publishing to NPM: ${name}@${opts.version}`); + const sorted = topoSort(libraries); + + console.log(`Found ${sorted.length} library packages to publish:`); + for (const pkg of sorted) console.log(` - ${pkg.name}`); - // Add --tag flag for release candidates const isReleaseCandidate = opts.version.includes("-rc."); const tag = isReleaseCandidate ? "rc" : (opts.latest ? "latest" : opts.minorVersionChannel); - await $({ - stdio: "inherit", - cwd: cliSharedPath, - })`pnpm publish --access public --tag ${tag} --no-git-checks`; - - console.log(`✅ Published ${name}@${opts.version}`); -} - -export async function publishNpmSdk(opts: ReleaseOpts) { - const isReleaseCandidate = opts.version.includes("-rc."); - const tag = isReleaseCandidate ? "rc" : (opts.latest ? "latest" : opts.minorVersionChannel); - - // Publish acp-http-client (dependency of the SDK) - { - const acpHttpClientPath = join(opts.root, "sdks/acp-http-client"); - const packageJsonPath = join(acpHttpClientPath, "package.json"); - const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8")); - const name = packageJson.name; - - const versionExists = await npmVersionExists(name, opts.version); + for (const pkg of sorted) { + const versionExists = await npmVersionExists(pkg.name, opts.version); if (versionExists) { - console.log( - `Version ${opts.version} of ${name} already exists. Skipping...`, - ); - } else { - console.log(`==> Building acp-http-client`); - await $({ - stdio: "inherit", - cwd: opts.root, - })`pnpm --filter acp-http-client build`; - - console.log(`==> Publishing to NPM: ${name}@${opts.version}`); - await $({ - stdio: "inherit", - cwd: acpHttpClientPath, - })`pnpm publish --access public --tag ${tag} --no-git-checks`; - - console.log(`✅ Published ${name}@${opts.version}`); + console.log(`Version ${opts.version} of ${pkg.name} already exists. Skipping...`); + continue; } + + if (pkg.hasBuildScript) { + console.log(`==> Building ${pkg.name}`); + await $({ stdio: "inherit", cwd: opts.root })`pnpm --filter ${pkg.name} build`; + } + + console.log(`==> Publishing to NPM: ${pkg.name}@${opts.version}`); + await $({ stdio: "inherit", cwd: pkg.dir })`pnpm publish --access public --tag ${tag} --no-git-checks`; + console.log(`✅ Published ${pkg.name}@${opts.version}`); } - // Publish SDK - const sdkPath = join(opts.root, "sdks/typescript"); - const packageJsonPath = join(sdkPath, "package.json"); - const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8")); - const name = packageJson.name; - - const versionExists = await npmVersionExists(name, opts.version); - if (versionExists) { - console.log( - `Version ${opts.version} of ${name} already exists. Skipping...`, - ); - return; - } - - // Build the SDK (cli-shared should already be built by publishNpmCliShared) - console.log(`==> Building TypeScript SDK`); - await $({ - stdio: "inherit", - cwd: opts.root, - })`pnpm --filter sandbox-agent build`; - - console.log(`==> Publishing to NPM: ${name}@${opts.version}`); - await $({ - stdio: "inherit", - cwd: sdkPath, - })`pnpm publish --access public --tag ${tag} --no-git-checks`; - - console.log(`✅ Published ${name}@${opts.version}`); + console.log("✅ All library packages published"); } +// ─── NPM CLI publishing ──────────────────────────────────────────────────── + +/** + * Discover and publish CLI wrapper and platform packages. + * Platform packages get their binaries downloaded from R2 before publishing. + */ export async function publishNpmCli(opts: ReleaseOpts) { - console.log("==> Publishing CLI packages to NPM"); + console.log("==> Discovering CLI packages"); + const packages = await discoverNpmPackages(opts.root, [ + "sdks/cli/package.json", + "sdks/cli/platforms/*/package.json", + "sdks/gigacode/package.json", + "sdks/gigacode/platforms/*/package.json", + ]); + + console.log(`Found ${packages.length} CLI packages to publish:`); + for (const pkg of packages) console.log(` - ${pkg.name}`); // Determine which commit to use for downloading binaries let sourceCommit = opts.commit; @@ -316,65 +338,41 @@ export async function publishNpmCli(opts: ReleaseOpts) { console.log(`Using binaries from commit: ${sourceCommit}`); } - for (const packageName of CLI_PACKAGES) { - // Check if version already exists - const versionExists = await npmVersionExists(packageName, opts.version); + for (const pkg of packages) { + const versionExists = await npmVersionExists(pkg.name, opts.version); if (versionExists) { console.log( - `Version ${opts.version} of ${packageName} already exists. Skipping...`, + `Version ${opts.version} of ${pkg.name} already exists. Skipping...`, ); continue; } - // Determine package path - let packagePath: string; - if (packageName === "@sandbox-agent/cli") { - packagePath = join(opts.root, "sdks/cli"); - } else if (packageName === "@sandbox-agent/gigacode") { - packagePath = join(opts.root, "sdks/gigacode"); - } else if (packageName.startsWith("@sandbox-agent/cli-")) { - // Platform-specific packages: @sandbox-agent/cli-linux-x64 -> sdks/cli/platforms/linux-x64 - const platform = packageName.replace("@sandbox-agent/cli-", ""); - packagePath = join(opts.root, "sdks/cli/platforms", platform); - } else if (packageName.startsWith("@sandbox-agent/gigacode-")) { - // Platform-specific packages: @sandbox-agent/gigacode-linux-x64 -> sdks/gigacode/platforms/linux-x64 - const platform = packageName.replace("@sandbox-agent/gigacode-", ""); - packagePath = join(opts.root, "sdks/gigacode/platforms", platform); - } else { - throw new Error(`Unknown CLI package: ${packageName}`); - } - - // Download binary from R2 for platform-specific packages - const platformInfo = CLI_PLATFORM_MAP[packageName]; + // Download binary for platform-specific packages + const platformInfo = CLI_PLATFORM_MAP[pkg.name]; if (platformInfo) { - const binDir = join(packagePath, "bin"); + const binDir = join(pkg.dir, "bin"); const binaryName = `${platformInfo.binaryName}${platformInfo.binaryExt}`; const localBinaryPath = join(binDir, binaryName); const remoteBinaryPath = `${PREFIX}/${sourceCommit}/binaries/${platformInfo.binaryName}-${platformInfo.target}${platformInfo.binaryExt}`; - console.log(`==> Downloading binary for ${packageName}`); + console.log(`==> Downloading binary for ${pkg.name}`); console.log(` From: ${remoteBinaryPath}`); console.log(` To: ${localBinaryPath}`); - // Create bin directory await fs.mkdir(binDir, { recursive: true }); - - // Download binary await downloadFromReleases(remoteBinaryPath, localBinaryPath); - // Make binary executable (not needed on Windows) if (!platformInfo.binaryExt) { await fs.chmod(localBinaryPath, 0o755); } } // Publish - console.log(`==> Publishing to NPM: ${packageName}@${opts.version}`); + console.log(`==> Publishing to NPM: ${pkg.name}@${opts.version}`); - // Add --tag flag for release candidates const isReleaseCandidate = opts.version.includes("-rc."); const tag = getCliPackageNpmTag({ - packageName, + packageName: pkg.name, isReleaseCandidate, latest: opts.latest, minorVersionChannel: opts.minorVersionChannel, @@ -383,12 +381,11 @@ export async function publishNpmCli(opts: ReleaseOpts) { try { await $({ stdio: "inherit", - cwd: packagePath, + cwd: pkg.dir, })`pnpm publish --access public --tag ${tag} --no-git-checks`; - console.log(`✅ Published ${packageName}@${opts.version}`); - + console.log(`✅ Published ${pkg.name}@${opts.version}`); } catch (err) { - console.error(`❌ Failed to publish ${packageName}`); + console.error(`❌ Failed to publish ${pkg.name}`); throw err; } } @@ -412,4 +409,3 @@ function getCliPackageNpmTag(opts: { return opts.minorVersionChannel; } - diff --git a/scripts/release/update_version.ts b/scripts/release/update_version.ts index 67df56d..a22e0f9 100644 --- a/scripts/release/update_version.ts +++ b/scripts/release/update_version.ts @@ -1,4 +1,5 @@ import * as fs from "node:fs/promises"; +import { join } from "node:path"; import { $ } from "execa"; import { glob } from "glob"; import type { ReleaseOpts } from "./main"; @@ -10,70 +11,36 @@ function assert(condition: any, message?: string): asserts condition { } export async function updateVersion(opts: ReleaseOpts) { - // Define substitutions - const findReplace = [ - { - path: "Cargo.toml", - find: /\[workspace\.package\]\nversion = ".*"/, - replace: `[workspace.package]\nversion = "${opts.version}"`, - }, - { - path: "sdks/cli-shared/package.json", - find: /"version": ".*"/, - replace: `"version": "${opts.version}"`, - }, - { - path: "sdks/acp-http-client/package.json", - find: /"version": ".*"/, - replace: `"version": "${opts.version}"`, - }, - { - path: "sdks/typescript/package.json", - find: /"version": ".*"/, - replace: `"version": "${opts.version}"`, - }, - { - path: "sdks/cli/package.json", - find: /"version": ".*"/, - replace: `"version": "${opts.version}"`, - }, - { - path: "sdks/gigacode/package.json", - find: /"version": ".*"/, - replace: `"version": "${opts.version}"`, - }, - { - path: "sdks/cli/platforms/*/package.json", - find: /"version": ".*"/, - replace: `"version": "${opts.version}"`, - }, - { - path: "sdks/gigacode/platforms/*/package.json", - find: /"version": ".*"/, - replace: `"version": "${opts.version}"`, - }, - ]; - - // Update internal crate versions in workspace dependencies - // These need to match the new version so cargo publish can resolve them correctly - const internalCrates = [ - "sandbox-agent", - "sandbox-agent-error", - "sandbox-agent-agent-management", - "sandbox-agent-agent-credentials", - "sandbox-agent-opencode-adapter", - "sandbox-agent-opencode-server-manager", - "acp-http-adapter", - ]; - - const cargoTomlPath = `${opts.root}/Cargo.toml`; + // 1. Update workspace version and internal crate versions in root Cargo.toml + const cargoTomlPath = join(opts.root, "Cargo.toml"); let cargoContent = await fs.readFile(cargoTomlPath, "utf-8"); + // Update [workspace.package] version + assert( + /\[workspace\.package\]\nversion = ".*"/.test(cargoContent), + "Could not find workspace.package version in Cargo.toml", + ); + 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) { - // Match: crate-name = { version = "x.y.z", path = "..." } const pattern = new RegExp( `(${crate.replace(/-/g, "-")} = \\{ version = ")[^"]+(",)`, - "g" + "g", ); cargoContent = cargoContent.replace(pattern, `$1${opts.version}$2`); } @@ -81,18 +48,34 @@ export async function updateVersion(opts: ReleaseOpts) { await fs.writeFile(cargoTomlPath, cargoContent); await $({ cwd: opts.root })`git add Cargo.toml`; - // Substitute all files - for (const { path: globPath, find, replace } of findReplace) { - const paths = await glob(globPath, { cwd: opts.root }); - assert(paths.length > 0, `no paths matched: ${globPath}`); - for (const path of paths) { - const fullPath = `${opts.root}/${path}`; - const file = await fs.readFile(fullPath, "utf-8"); - assert(find.test(file), `file does not match ${find}: ${fullPath}`); - const newFile = file.replace(find, replace); - await fs.writeFile(fullPath, newFile); + // 2. Discover and update all non-private SDK package.json versions + const packageJsonPaths = await glob("sdks/**/package.json", { + cwd: opts.root, + ignore: ["**/node_modules/**"], + }); - await $({ cwd: opts.root })`git add ${path}`; - } + // 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}`; } }