mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-19 20:00:48 +00:00
chore: copy and adapt rivet release scripts for sandbox-agent
This commit is contained in:
parent
29b159ca20
commit
2a922ef562
14 changed files with 1284 additions and 1263 deletions
|
|
@ -1,117 +0,0 @@
|
||||||
import * as fs from "node:fs/promises";
|
|
||||||
import * as path from "node:path";
|
|
||||||
import { $ } from "execa";
|
|
||||||
import type { ReleaseOpts } from "./main.js";
|
|
||||||
import {
|
|
||||||
assertDirExists,
|
|
||||||
copyReleasesPath,
|
|
||||||
deleteReleasesPath,
|
|
||||||
listReleasesObjects,
|
|
||||||
uploadContentToReleases,
|
|
||||||
uploadDirToReleases,
|
|
||||||
uploadFileToReleases,
|
|
||||||
} from "./utils.js";
|
|
||||||
|
|
||||||
const PREFIX = "sandbox-agent";
|
|
||||||
|
|
||||||
const BINARY_FILES = [
|
|
||||||
"sandbox-agent-x86_64-unknown-linux-musl",
|
|
||||||
"sandbox-agent-x86_64-pc-windows-gnu.exe",
|
|
||||||
"sandbox-agent-x86_64-apple-darwin",
|
|
||||||
"sandbox-agent-aarch64-apple-darwin",
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build TypeScript SDK and upload to commit directory.
|
|
||||||
* This is called during setup-ci phase.
|
|
||||||
*/
|
|
||||||
export async function buildAndUploadArtifacts(opts: ReleaseOpts) {
|
|
||||||
console.log("==> Building TypeScript SDK");
|
|
||||||
const sdkDir = path.join(opts.root, "sdks", "typescript");
|
|
||||||
await $({ stdio: "inherit", cwd: sdkDir })`pnpm install`;
|
|
||||||
await $({ stdio: "inherit", cwd: sdkDir })`pnpm run build`;
|
|
||||||
|
|
||||||
const distPath = path.join(sdkDir, "dist");
|
|
||||||
await assertDirExists(distPath);
|
|
||||||
|
|
||||||
console.log(`==> Uploading TypeScript SDK to ${PREFIX}/${opts.commit}/typescript/`);
|
|
||||||
await uploadDirToReleases(distPath, `${PREFIX}/${opts.commit}/typescript/`);
|
|
||||||
|
|
||||||
console.log("✅ TypeScript SDK artifacts uploaded");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Promote artifacts from commit directory to version directory.
|
|
||||||
* This is called during complete-ci phase.
|
|
||||||
*/
|
|
||||||
export async function promoteArtifacts(opts: ReleaseOpts) {
|
|
||||||
// Promote TypeScript SDK
|
|
||||||
await promotePath(opts, "typescript");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function promotePath(opts: ReleaseOpts, name: string) {
|
|
||||||
console.log(`==> Promoting ${name} artifacts`);
|
|
||||||
|
|
||||||
const sourcePrefix = `${PREFIX}/${opts.commit}/${name}/`;
|
|
||||||
const commitFiles = await listReleasesObjects(sourcePrefix);
|
|
||||||
if (!Array.isArray(commitFiles?.Contents) || commitFiles.Contents.length === 0) {
|
|
||||||
throw new Error(`No files found under ${sourcePrefix}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await copyPath(sourcePrefix, `${PREFIX}/${opts.version}/${name}/`);
|
|
||||||
if (opts.latest) {
|
|
||||||
await copyPath(sourcePrefix, `${PREFIX}/latest/${name}/`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function copyPath(sourcePrefix: string, targetPrefix: string) {
|
|
||||||
console.log(`Copying ${sourcePrefix} -> ${targetPrefix}`);
|
|
||||||
await deleteReleasesPath(targetPrefix);
|
|
||||||
await copyReleasesPath(sourcePrefix, targetPrefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload install script with version substitution.
|
|
||||||
*/
|
|
||||||
export async function uploadInstallScripts(opts: ReleaseOpts) {
|
|
||||||
const installPath = path.join(opts.root, "scripts", "release", "static", "install.sh");
|
|
||||||
let installContent = await fs.readFile(installPath, "utf8");
|
|
||||||
|
|
||||||
const uploadForVersion = async (versionValue: string, remoteVersion: string) => {
|
|
||||||
const content = installContent.replace(/__VERSION__/g, versionValue);
|
|
||||||
const uploadKey = `${PREFIX}/${remoteVersion}/install.sh`;
|
|
||||||
console.log(`Uploading install script: ${uploadKey}`);
|
|
||||||
await uploadContentToReleases(content, uploadKey);
|
|
||||||
};
|
|
||||||
|
|
||||||
await uploadForVersion(opts.version, opts.version);
|
|
||||||
if (opts.latest) {
|
|
||||||
await uploadForVersion("latest", "latest");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload compiled binaries from dist/ directory.
|
|
||||||
*/
|
|
||||||
export async function uploadBinaries(opts: ReleaseOpts) {
|
|
||||||
const distDir = path.join(opts.root, "dist");
|
|
||||||
await assertDirExists(distDir);
|
|
||||||
|
|
||||||
for (const fileName of BINARY_FILES) {
|
|
||||||
const localPath = path.join(distDir, fileName);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.access(localPath);
|
|
||||||
} catch {
|
|
||||||
throw new Error(`Missing binary: ${localPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Uploading binary: ${fileName}`);
|
|
||||||
await uploadFileToReleases(localPath, `${PREFIX}/${opts.version}/${fileName}`);
|
|
||||||
if (opts.latest) {
|
|
||||||
await uploadFileToReleases(localPath, `${PREFIX}/latest/${fileName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ Binaries uploaded");
|
|
||||||
}
|
|
||||||
36
scripts/release/build-artifacts.ts
Normal file
36
scripts/release/build-artifacts.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { $ } from "execa";
|
||||||
|
import type { ReleaseOpts } from "./main";
|
||||||
|
import { assertDirExists, PREFIX, uploadDirToReleases } from "./utils";
|
||||||
|
|
||||||
|
export async function buildJsArtifacts(opts: ReleaseOpts) {
|
||||||
|
await buildAndUploadTypescriptSdk(opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildAndUploadTypescriptSdk(opts: ReleaseOpts) {
|
||||||
|
console.log(`==> Building TypeScript SDK`);
|
||||||
|
|
||||||
|
// Build TypeScript SDK
|
||||||
|
await $({
|
||||||
|
stdio: "inherit",
|
||||||
|
cwd: opts.root,
|
||||||
|
})`pnpm --filter sandbox-agent build`;
|
||||||
|
|
||||||
|
console.log(`✅ TypeScript SDK built successfully`);
|
||||||
|
|
||||||
|
// Upload TypeScript SDK to R2
|
||||||
|
console.log(`==> Uploading TypeScript SDK Artifacts`);
|
||||||
|
|
||||||
|
const sdkDistPath = path.resolve(
|
||||||
|
opts.root,
|
||||||
|
"sdks/typescript/dist",
|
||||||
|
);
|
||||||
|
|
||||||
|
await assertDirExists(sdkDistPath);
|
||||||
|
|
||||||
|
// Upload to commit directory
|
||||||
|
console.log(`Uploading TypeScript SDK to ${PREFIX}/${opts.commit}/typescript/`);
|
||||||
|
await uploadDirToReleases(sdkDistPath, `${PREFIX}/${opts.commit}/typescript/`);
|
||||||
|
|
||||||
|
console.log(`✅ TypeScript SDK artifacts uploaded successfully`);
|
||||||
|
}
|
||||||
49
scripts/release/docker.ts
Normal file
49
scripts/release/docker.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { $ } from "execa";
|
||||||
|
import type { ReleaseOpts } from "./main";
|
||||||
|
import { fetchGitRef, versionOrCommitToRef } from "./utils";
|
||||||
|
|
||||||
|
const IMAGE = "rivetdev/sandbox-agent";
|
||||||
|
|
||||||
|
export async function tagDocker(opts: ReleaseOpts) {
|
||||||
|
// Determine which commit to use for source images
|
||||||
|
let sourceCommit = opts.commit;
|
||||||
|
if (opts.reuseEngineVersion) {
|
||||||
|
console.log(`==> Reusing Docker images from ${opts.reuseEngineVersion}`);
|
||||||
|
const ref = versionOrCommitToRef(opts.reuseEngineVersion);
|
||||||
|
await fetchGitRef(ref);
|
||||||
|
const result = await $`git rev-parse ${ref}`;
|
||||||
|
sourceCommit = result.stdout.trim().slice(0, 7);
|
||||||
|
console.log(`==> Source commit: ${sourceCommit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check both architecture images exist using manifest inspect
|
||||||
|
console.log(`==> Checking images exist: ${IMAGE}:${sourceCommit}-{amd64,arm64}`);
|
||||||
|
try {
|
||||||
|
console.log(`==> Inspecting ${IMAGE}:${sourceCommit}-amd64`);
|
||||||
|
await $({ stdio: "inherit" })`docker manifest inspect ${IMAGE}:${sourceCommit}-amd64`;
|
||||||
|
console.log(`==> Inspecting ${IMAGE}:${sourceCommit}-arm64`);
|
||||||
|
await $({ stdio: "inherit" })`docker manifest inspect ${IMAGE}:${sourceCommit}-arm64`;
|
||||||
|
console.log(`==> Both images exist`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`==> Error inspecting images:`, error);
|
||||||
|
throw new Error(
|
||||||
|
`Images ${IMAGE}:${sourceCommit}-{amd64,arm64} do not exist on Docker Hub. Error: ${error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and push manifest with version
|
||||||
|
await createManifest(sourceCommit, opts.version);
|
||||||
|
|
||||||
|
// Create and push manifest with latest
|
||||||
|
if (opts.latest) {
|
||||||
|
await createManifest(sourceCommit, "latest");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createManifest(from: string, to: string) {
|
||||||
|
console.log(`==> Creating manifest: ${IMAGE}:${to} from ${IMAGE}:${from}-{amd64,arm64}`);
|
||||||
|
|
||||||
|
// Use buildx imagetools to create and push multi-arch manifest
|
||||||
|
// This works with manifest lists as inputs (unlike docker manifest create)
|
||||||
|
await $({ stdio: "inherit" })`docker buildx imagetools create --tag ${IMAGE}:${to} ${IMAGE}:${from}-amd64 ${IMAGE}:${from}-arm64`;
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { $ } from "execa";
|
import { $ } from "execa";
|
||||||
import * as semver from "semver";
|
import type { ReleaseOpts } from "./main";
|
||||||
import type { ReleaseOpts } from "./main.js";
|
|
||||||
|
|
||||||
export async function validateGit(_opts: ReleaseOpts) {
|
export async function validateGit(_opts: ReleaseOpts) {
|
||||||
|
// Validate there's no uncommitted changes
|
||||||
const result = await $`git status --porcelain`;
|
const result = await $`git status --porcelain`;
|
||||||
const status = result.stdout;
|
const status = result.stdout;
|
||||||
if (status.trim().length > 0) {
|
if (status.trim().length > 0) {
|
||||||
|
|
@ -15,8 +15,12 @@ export async function validateGit(_opts: ReleaseOpts) {
|
||||||
export async function createAndPushTag(opts: ReleaseOpts) {
|
export async function createAndPushTag(opts: ReleaseOpts) {
|
||||||
console.log(`Creating tag v${opts.version}...`);
|
console.log(`Creating tag v${opts.version}...`);
|
||||||
try {
|
try {
|
||||||
|
// Create tag and force update if it exists
|
||||||
await $({ stdio: "inherit", cwd: opts.root })`git tag -f v${opts.version}`;
|
await $({ stdio: "inherit", cwd: opts.root })`git tag -f v${opts.version}`;
|
||||||
|
|
||||||
|
// Push tag with force to ensure it's updated
|
||||||
await $({ stdio: "inherit", cwd: opts.root })`git push origin v${opts.version} -f`;
|
await $({ stdio: "inherit", cwd: opts.root })`git push origin v${opts.version} -f`;
|
||||||
|
|
||||||
console.log(`✅ Tag v${opts.version} created and pushed`);
|
console.log(`✅ Tag v${opts.version} created and pushed`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ Failed to create or push tag");
|
console.error("❌ Failed to create or push tag");
|
||||||
|
|
@ -28,40 +32,46 @@ export async function createGitHubRelease(opts: ReleaseOpts) {
|
||||||
console.log("Creating GitHub release...");
|
console.log("Creating GitHub release...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Get the current tag name (should be the tag created during the release process)
|
||||||
|
const { stdout: currentTag } = await $({
|
||||||
|
cwd: opts.root,
|
||||||
|
})`git describe --tags --exact-match`;
|
||||||
|
const tagName = currentTag.trim();
|
||||||
|
|
||||||
console.log(`Looking for existing release for ${opts.version}`);
|
console.log(`Looking for existing release for ${opts.version}`);
|
||||||
|
|
||||||
|
// Check if a release with this version name already exists
|
||||||
const { stdout: releaseJson } = await $({
|
const { stdout: releaseJson } = await $({
|
||||||
cwd: opts.root,
|
cwd: opts.root,
|
||||||
})`gh release list --json name,tagName`;
|
})`gh release list --json name,tagName`;
|
||||||
const releases = JSON.parse(releaseJson);
|
const releases = JSON.parse(releaseJson);
|
||||||
const existingRelease = releases.find(
|
const existingRelease = releases.find(
|
||||||
(r: { name: string }) => r.name === opts.version,
|
(r: any) => r.name === opts.version,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingRelease) {
|
if (existingRelease) {
|
||||||
console.log(
|
console.log(
|
||||||
`Updating release ${opts.version} to point to tag v${opts.version}`,
|
`Updating release ${opts.version} to point to new tag ${tagName}`,
|
||||||
);
|
);
|
||||||
await $({
|
await $({
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
cwd: opts.root,
|
cwd: opts.root,
|
||||||
})`gh release edit ${existingRelease.tagName} --tag v${opts.version}`;
|
})`gh release edit ${existingRelease.tagName} --tag ${tagName}`;
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
`Creating new release ${opts.version} pointing to tag v${opts.version}`,
|
`Creating new release ${opts.version} pointing to tag ${tagName}`,
|
||||||
);
|
);
|
||||||
await $({
|
await $({
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
cwd: opts.root,
|
cwd: opts.root,
|
||||||
})`gh release create v${opts.version} --title ${opts.version} --generate-notes`;
|
})`gh release create ${tagName} --title ${opts.version} --generate-notes`;
|
||||||
|
|
||||||
// Mark as prerelease if needed
|
// Check if this is a pre-release (contains -rc. or similar)
|
||||||
const parsed = semver.parse(opts.version);
|
if (opts.version.includes("-")) {
|
||||||
if (parsed && parsed.prerelease.length > 0) {
|
|
||||||
await $({
|
await $({
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
cwd: opts.root,
|
cwd: opts.root,
|
||||||
})`gh release edit v${opts.version} --prerelease`;
|
})`gh release edit ${tagName} --prerelease`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,18 +1,22 @@
|
||||||
{
|
{
|
||||||
"name": "release",
|
"name": "release",
|
||||||
"version": "0.1.0",
|
"version": "2.0.21",
|
||||||
"private": true,
|
"description": "",
|
||||||
"type": "module",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"packageManager": "pnpm@10.13.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^24.3.0",
|
||||||
"@types/semver": "^7.5.8"
|
"@types/semver": "^7.5.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": "^12.1.0",
|
"commander": "^12.1.0",
|
||||||
"execa": "^9.5.0",
|
"execa": "^8.0.1",
|
||||||
"glob": "^10.3.10",
|
"glob": "^10.3.10",
|
||||||
"semver": "^7.6.0"
|
"semver": "^7.6.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
78
scripts/release/promote-artifacts.ts
Normal file
78
scripts/release/promote-artifacts.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import * as fs from "node:fs/promises";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import { $ } from "execa";
|
||||||
|
import type { ReleaseOpts } from "./main";
|
||||||
|
import {
|
||||||
|
copyReleasesPath,
|
||||||
|
deleteReleasesPath,
|
||||||
|
fetchGitRef,
|
||||||
|
listReleasesObjects,
|
||||||
|
PREFIX,
|
||||||
|
uploadContentToReleases,
|
||||||
|
versionOrCommitToRef,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
export async function promoteArtifacts(opts: ReleaseOpts) {
|
||||||
|
// Determine which commit to use for source artifacts
|
||||||
|
let sourceCommit = opts.commit;
|
||||||
|
if (opts.reuseEngineVersion) {
|
||||||
|
console.log(`==> Reusing artifacts from ${opts.reuseEngineVersion}`);
|
||||||
|
const ref = versionOrCommitToRef(opts.reuseEngineVersion);
|
||||||
|
await fetchGitRef(ref);
|
||||||
|
const result = await $`git rev-parse ${ref}`;
|
||||||
|
sourceCommit = result.stdout.trim().slice(0, 7);
|
||||||
|
console.log(`==> Source commit: ${sourceCommit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Promote TypeScript SDK artifacts (uploaded by build-artifacts.ts to sandbox-agent/{commit}/typescript/)
|
||||||
|
await promotePath(opts, sourceCommit, "typescript");
|
||||||
|
|
||||||
|
// Promote binary artifacts (uploaded by CI in release.yaml to sandbox-agent/{commit}/binaries/)
|
||||||
|
await promotePath(opts, sourceCommit, "binaries");
|
||||||
|
|
||||||
|
// Upload install scripts
|
||||||
|
await uploadInstallScripts(opts, opts.version);
|
||||||
|
if (opts.latest) {
|
||||||
|
await uploadInstallScripts(opts, "latest");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function uploadInstallScripts(opts: ReleaseOpts, version: string) {
|
||||||
|
const installScriptPaths = [
|
||||||
|
path.resolve(opts.root, "scripts/release/static/install.sh"),
|
||||||
|
path.resolve(opts.root, "scripts/release/static/install.ps1"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const scriptPath of installScriptPaths) {
|
||||||
|
let scriptContent = await fs.readFile(scriptPath, "utf-8");
|
||||||
|
scriptContent = scriptContent.replace(/__VERSION__/g, version);
|
||||||
|
|
||||||
|
const uploadKey = `${PREFIX}/${version}/${scriptPath.split("/").pop() ?? ""}`;
|
||||||
|
|
||||||
|
console.log(`Uploading install script: ${uploadKey}`);
|
||||||
|
await uploadContentToReleases(scriptContent, uploadKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyPath(sourcePrefix: string, targetPrefix: string) {
|
||||||
|
console.log(`Copying ${sourcePrefix} -> ${targetPrefix}`);
|
||||||
|
await deleteReleasesPath(targetPrefix);
|
||||||
|
await copyReleasesPath(sourcePrefix, targetPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** S3-to-S3 copy from sandbox-agent/{commit}/{name}/ to sandbox-agent/{version}/{name}/ */
|
||||||
|
async function promotePath(opts: ReleaseOpts, sourceCommit: string, name: string) {
|
||||||
|
console.log(`==> Promoting ${name} artifacts`);
|
||||||
|
|
||||||
|
const sourcePrefix = `${PREFIX}/${sourceCommit}/${name}/`;
|
||||||
|
const commitFiles = await listReleasesObjects(sourcePrefix);
|
||||||
|
if (!Array.isArray(commitFiles?.Contents) || commitFiles.Contents.length === 0) {
|
||||||
|
throw new Error(`No files found under ${sourcePrefix}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await copyPath(sourcePrefix, `${PREFIX}/${opts.version}/${name}/`);
|
||||||
|
if (opts.latest) {
|
||||||
|
await copyPath(sourcePrefix, `${PREFIX}/latest/${name}/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,166 +0,0 @@
|
||||||
import * as fs from "node:fs/promises";
|
|
||||||
import * as path from "node:path";
|
|
||||||
import { $ } from "execa";
|
|
||||||
import * as semver from "semver";
|
|
||||||
import type { ReleaseOpts } from "./main.js";
|
|
||||||
|
|
||||||
const CRATE_ORDER = [
|
|
||||||
"error",
|
|
||||||
"agent-credentials",
|
|
||||||
"agent-schema",
|
|
||||||
"universal-agent-schema",
|
|
||||||
"agent-management",
|
|
||||||
"sandbox-agent",
|
|
||||||
];
|
|
||||||
|
|
||||||
const PLATFORM_MAP: Record<string, { pkg: string; os: string; cpu: string; ext: string }> = {
|
|
||||||
"x86_64-unknown-linux-musl": { pkg: "linux-x64", os: "linux", cpu: "x64", ext: "" },
|
|
||||||
"x86_64-pc-windows-gnu": { pkg: "win32-x64", os: "win32", cpu: "x64", ext: ".exe" },
|
|
||||||
"x86_64-apple-darwin": { pkg: "darwin-x64", os: "darwin", cpu: "x64", ext: "" },
|
|
||||||
"aarch64-apple-darwin": { pkg: "darwin-arm64", os: "darwin", cpu: "arm64", ext: "" },
|
|
||||||
};
|
|
||||||
|
|
||||||
async function npmVersionExists(packageName: string, version: string): Promise<boolean> {
|
|
||||||
console.log(`Checking if ${packageName}@${version} exists on npm...`);
|
|
||||||
try {
|
|
||||||
await $({
|
|
||||||
stdout: "ignore",
|
|
||||||
stderr: "pipe",
|
|
||||||
})`npm view ${packageName}@${version} version`;
|
|
||||||
return true;
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const stderr = error && typeof error === "object" && "stderr" in error
|
|
||||||
? String(error.stderr)
|
|
||||||
: "";
|
|
||||||
if (
|
|
||||||
stderr.includes(`No match found for version ${version}`) ||
|
|
||||||
stderr.includes(`'${packageName}@${version}' is not in this registry`)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Unexpected error, assume not exists to allow publish attempt
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function crateVersionExists(crateName: string, version: string): Promise<boolean> {
|
|
||||||
console.log(`Checking if ${crateName}@${version} exists on crates.io...`);
|
|
||||||
try {
|
|
||||||
const result = await $`cargo search ${crateName} --limit 1`;
|
|
||||||
const output = result.stdout || "";
|
|
||||||
const match = output.match(new RegExp(`^${crateName}\\s*=\\s*"([^"]+)"`));
|
|
||||||
return !!(match && match[1] === version);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNpmTag(version: string, latest: boolean): string | null {
|
|
||||||
if (latest) return null;
|
|
||||||
const parsed = semver.parse(version);
|
|
||||||
if (!parsed) throw new Error(`Invalid version: ${version}`);
|
|
||||||
|
|
||||||
if (parsed.prerelease.length === 0) {
|
|
||||||
return "next";
|
|
||||||
}
|
|
||||||
const hasRc = parsed.prerelease.some((part) =>
|
|
||||||
String(part).toLowerCase().startsWith("rc")
|
|
||||||
);
|
|
||||||
if (hasRc) {
|
|
||||||
return "rc";
|
|
||||||
}
|
|
||||||
throw new Error(`Prerelease versions must use rc tag when not latest: ${version}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function publishCrates(opts: ReleaseOpts) {
|
|
||||||
for (const crate of CRATE_ORDER) {
|
|
||||||
const crateName = `sandbox-agent-${crate}`;
|
|
||||||
|
|
||||||
if (await crateVersionExists(crateName, opts.version)) {
|
|
||||||
console.log(`==> Skipping ${crateName}@${opts.version} (already published)`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`==> Publishing ${crateName}@${opts.version}`);
|
|
||||||
const crateDir = path.join(opts.root, "server", "packages", crate);
|
|
||||||
await $({ stdio: "inherit", cwd: crateDir })`cargo publish --allow-dirty`;
|
|
||||||
|
|
||||||
console.log("Waiting 30s for crates.io index...");
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 30000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function publishNpmSdk(opts: ReleaseOpts) {
|
|
||||||
const sdkDir = path.join(opts.root, "sdks", "typescript");
|
|
||||||
const packageName = "sandbox-agent";
|
|
||||||
|
|
||||||
if (await npmVersionExists(packageName, opts.version)) {
|
|
||||||
console.log(`==> Skipping ${packageName}@${opts.version} (already published)`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`==> Publishing ${packageName}@${opts.version}`);
|
|
||||||
const npmTag = getNpmTag(opts.version, opts.latest);
|
|
||||||
|
|
||||||
await $({ stdio: "inherit", cwd: sdkDir })`npm version ${opts.version} --no-git-tag-version --allow-same-version`;
|
|
||||||
await $({ stdio: "inherit", cwd: sdkDir })`pnpm install`;
|
|
||||||
await $({ stdio: "inherit", cwd: sdkDir })`pnpm run build`;
|
|
||||||
|
|
||||||
const publishArgs = ["publish", "--access", "public"];
|
|
||||||
if (npmTag) publishArgs.push("--tag", npmTag);
|
|
||||||
await $({ stdio: "inherit", cwd: sdkDir })`npm ${publishArgs}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function publishNpmCli(opts: ReleaseOpts) {
|
|
||||||
const cliDir = path.join(opts.root, "sdks", "cli");
|
|
||||||
const distDir = path.join(opts.root, "dist");
|
|
||||||
const npmTag = getNpmTag(opts.version, opts.latest);
|
|
||||||
|
|
||||||
// Publish platform-specific packages
|
|
||||||
for (const [target, info] of Object.entries(PLATFORM_MAP)) {
|
|
||||||
const packageName = `@sandbox-agent/cli-${info.pkg}`;
|
|
||||||
|
|
||||||
if (await npmVersionExists(packageName, opts.version)) {
|
|
||||||
console.log(`==> Skipping ${packageName}@${opts.version} (already published)`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const platformDir = path.join(cliDir, "platforms", info.pkg);
|
|
||||||
const binDir = path.join(platformDir, "bin");
|
|
||||||
await fs.mkdir(binDir, { recursive: true });
|
|
||||||
|
|
||||||
const srcBinary = path.join(distDir, `sandbox-agent-${target}${info.ext}`);
|
|
||||||
const dstBinary = path.join(binDir, `sandbox-agent${info.ext}`);
|
|
||||||
await fs.copyFile(srcBinary, dstBinary);
|
|
||||||
if (info.ext !== ".exe") {
|
|
||||||
await fs.chmod(dstBinary, 0o755);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`==> Publishing ${packageName}@${opts.version}`);
|
|
||||||
await $({ stdio: "inherit", cwd: platformDir })`npm version ${opts.version} --no-git-tag-version --allow-same-version`;
|
|
||||||
|
|
||||||
const publishArgs = ["publish", "--access", "public"];
|
|
||||||
if (npmTag) publishArgs.push("--tag", npmTag);
|
|
||||||
await $({ stdio: "inherit", cwd: platformDir })`npm ${publishArgs}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish main CLI package
|
|
||||||
const mainPackageName = "@sandbox-agent/cli";
|
|
||||||
if (await npmVersionExists(mainPackageName, opts.version)) {
|
|
||||||
console.log(`==> Skipping ${mainPackageName}@${opts.version} (already published)`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`==> Publishing ${mainPackageName}@${opts.version}`);
|
|
||||||
const pkgPath = path.join(cliDir, "package.json");
|
|
||||||
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
|
|
||||||
pkg.version = opts.version;
|
|
||||||
for (const dep of Object.keys(pkg.optionalDependencies || {})) {
|
|
||||||
pkg.optionalDependencies[dep] = opts.version;
|
|
||||||
}
|
|
||||||
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
||||||
|
|
||||||
const publishArgs = ["publish", "--access", "public"];
|
|
||||||
if (npmTag) publishArgs.push("--tag", npmTag);
|
|
||||||
await $({ stdio: "inherit", cwd: cliDir })`npm ${publishArgs}`;
|
|
||||||
}
|
|
||||||
203
scripts/release/sdk.ts
Normal file
203
scripts/release/sdk.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
import { $ } from "execa";
|
||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import type { ReleaseOpts } from "./main";
|
||||||
|
|
||||||
|
// Crates to publish in dependency order
|
||||||
|
const CRATES = [
|
||||||
|
"error",
|
||||||
|
"agent-credentials",
|
||||||
|
"extracted-agent-schemas",
|
||||||
|
"universal-agent-schema",
|
||||||
|
"agent-management",
|
||||||
|
"sandbox-agent",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// NPM CLI packages
|
||||||
|
const CLI_PACKAGES = [
|
||||||
|
"@sandbox-agent/cli",
|
||||||
|
"@sandbox-agent/cli-linux-x64",
|
||||||
|
"@sandbox-agent/cli-win32-x64",
|
||||||
|
"@sandbox-agent/cli-darwin-x64",
|
||||||
|
"@sandbox-agent/cli-darwin-arm64",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
async function npmVersionExists(
|
||||||
|
packageName: string,
|
||||||
|
version: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
console.log(
|
||||||
|
`==> Checking if NPM version exists: ${packageName}@${version}`,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await $({
|
||||||
|
stdout: "ignore",
|
||||||
|
stderr: "pipe",
|
||||||
|
})`npm view ${packageName}@${version} version`;
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.stderr) {
|
||||||
|
if (
|
||||||
|
!error.stderr.includes(
|
||||||
|
`No match found for version ${version}`,
|
||||||
|
) &&
|
||||||
|
!error.stderr.includes(
|
||||||
|
`'${packageName}@${version}' is not in this registry.`,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`unexpected npm view version output: ${error.stderr}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function crateVersionExists(
|
||||||
|
crateName: string,
|
||||||
|
version: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
console.log(`==> Checking if crate version exists: ${crateName}@${version}`);
|
||||||
|
try {
|
||||||
|
const result = await $({
|
||||||
|
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) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error: any) {
|
||||||
|
// If cargo search fails, assume crate doesn't exist
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function publishCrates(opts: ReleaseOpts) {
|
||||||
|
console.log("==> Publishing crates to crates.io");
|
||||||
|
|
||||||
|
for (const crate of CRATES) {
|
||||||
|
const cratePath = join(opts.root, "server/packages", crate);
|
||||||
|
|
||||||
|
// Read Cargo.toml to get the actual crate name
|
||||||
|
const cargoTomlPath = join(cratePath, "Cargo.toml");
|
||||||
|
const cargoToml = await 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);
|
||||||
|
if (versionExists) {
|
||||||
|
console.log(
|
||||||
|
`Version ${opts.version} of ${crateName} already exists on crates.io. Skipping...`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish
|
||||||
|
console.log(`==> Publishing to crates.io: ${crateName}@${opts.version}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $({
|
||||||
|
stdio: "inherit",
|
||||||
|
cwd: cratePath,
|
||||||
|
})`cargo publish --allow-dirty`;
|
||||||
|
console.log(`✅ Published ${crateName}@${opts.version}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ Failed to publish ${crateName}`);
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ All crates published");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function publishNpmSdk(opts: ReleaseOpts) {
|
||||||
|
const sdkPath = join(opts.root, "sdks/typescript");
|
||||||
|
const packageJsonPath = join(sdkPath, "package.json");
|
||||||
|
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
|
||||||
|
const name = packageJson.name;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the SDK
|
||||||
|
console.log(`==> Building TypeScript SDK`);
|
||||||
|
await $({
|
||||||
|
stdio: "inherit",
|
||||||
|
cwd: opts.root,
|
||||||
|
})`pnpm --filter sandbox-agent build`;
|
||||||
|
|
||||||
|
// Publish
|
||||||
|
console.log(`==> Publishing to NPM: ${name}@${opts.version}`);
|
||||||
|
|
||||||
|
// Add --tag flag for release candidates
|
||||||
|
const isReleaseCandidate = opts.version.includes("-rc.");
|
||||||
|
const tag = isReleaseCandidate ? "rc" : "latest";
|
||||||
|
|
||||||
|
await $({
|
||||||
|
stdio: "inherit",
|
||||||
|
cwd: sdkPath,
|
||||||
|
})`pnpm publish --access public --tag ${tag} --no-git-checks`;
|
||||||
|
|
||||||
|
console.log(`✅ Published ${name}@${opts.version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function publishNpmCli(opts: ReleaseOpts) {
|
||||||
|
console.log("==> Publishing CLI packages to NPM");
|
||||||
|
|
||||||
|
for (const packageName of CLI_PACKAGES) {
|
||||||
|
// Check if version already exists
|
||||||
|
const versionExists = await npmVersionExists(packageName, opts.version);
|
||||||
|
if (versionExists) {
|
||||||
|
console.log(
|
||||||
|
`Version ${opts.version} of ${packageName} already exists. Skipping...`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine package path
|
||||||
|
let packagePath: string;
|
||||||
|
if (packageName === "@sandbox-agent/cli") {
|
||||||
|
packagePath = join(opts.root, "sdks/cli");
|
||||||
|
} else {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish
|
||||||
|
console.log(`==> Publishing to NPM: ${packageName}@${opts.version}`);
|
||||||
|
|
||||||
|
// Add --tag flag for release candidates
|
||||||
|
const isReleaseCandidate = opts.version.includes("-rc.");
|
||||||
|
const tag = isReleaseCandidate ? "rc" : "latest";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $({
|
||||||
|
stdio: "inherit",
|
||||||
|
cwd: packagePath,
|
||||||
|
})`pnpm publish --access public --tag ${tag} --no-git-checks`;
|
||||||
|
console.log(`✅ Published ${packageName}@${opts.version}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ Failed to publish ${packageName}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ All CLI packages published");
|
||||||
|
}
|
||||||
51
scripts/release/static/install.ps1
Normal file
51
scripts/release/static/install.ps1
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
#!/usr/bin/env pwsh
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
|
||||||
|
# Create bin directory for sandbox-agent
|
||||||
|
$BinDir = $env:BIN_DIR
|
||||||
|
$SandboxAgentInstall = if ($BinDir) {
|
||||||
|
$BinDir
|
||||||
|
} else {
|
||||||
|
"${Home}\.sandbox-agent\bin"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(Test-Path $SandboxAgentInstall)) {
|
||||||
|
New-Item $SandboxAgentInstall -ItemType Directory | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$SandboxAgentExe = "$SandboxAgentInstall\sandbox-agent.exe"
|
||||||
|
$Version = '__VERSION__'
|
||||||
|
$FileName = 'sandbox-agent-x86_64-pc-windows-gnu.exe'
|
||||||
|
|
||||||
|
Write-Host
|
||||||
|
Write-Host "> Installing sandbox-agent ${Version}"
|
||||||
|
|
||||||
|
# Download binary
|
||||||
|
$DownloadUrl = "https://releases.rivet.dev/sandbox-agent/${Version}/binaries/${FileName}"
|
||||||
|
Write-Host
|
||||||
|
Write-Host "> Downloading ${DownloadUrl}"
|
||||||
|
Invoke-WebRequest $DownloadUrl -OutFile $SandboxAgentExe -UseBasicParsing
|
||||||
|
|
||||||
|
# Install to PATH
|
||||||
|
Write-Host
|
||||||
|
Write-Host "> Installing sandbox-agent"
|
||||||
|
$User = [System.EnvironmentVariableTarget]::User
|
||||||
|
$Path = [System.Environment]::GetEnvironmentVariable('Path', $User)
|
||||||
|
if (!(";${Path};".ToLower() -like "*;${SandboxAgentInstall};*".ToLower())) {
|
||||||
|
[System.Environment]::SetEnvironmentVariable('Path', "${Path};${SandboxAgentInstall}", $User)
|
||||||
|
$Env:Path += ";${SandboxAgentInstall}"
|
||||||
|
Write-Host "Please restart your PowerShell session or run the following command to refresh the environment variables:"
|
||||||
|
Write-Host "[System.Environment]::SetEnvironmentVariable('Path', '${Path};${SandboxAgentInstall}', [System.EnvironmentVariableTarget]::Process)"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host
|
||||||
|
Write-Host "> Checking installation"
|
||||||
|
sandbox-agent.exe --version
|
||||||
|
|
||||||
|
Write-Host
|
||||||
|
Write-Host "sandbox-agent was installed successfully to ${SandboxAgentExe}."
|
||||||
|
Write-Host "Run 'sandbox-agent --help' to get started."
|
||||||
|
Write-Host
|
||||||
|
|
@ -7,17 +7,19 @@
|
||||||
# shellcheck enable=require-variable-braces
|
# shellcheck enable=require-variable-braces
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
WORK_DIR="/tmp/sandbox_agent_install"
|
rm -rf /tmp/sandbox_agent_install
|
||||||
rm -rf "$WORK_DIR"
|
mkdir /tmp/sandbox_agent_install
|
||||||
mkdir -p "$WORK_DIR"
|
cd /tmp/sandbox_agent_install
|
||||||
cd "$WORK_DIR"
|
|
||||||
|
|
||||||
SANDBOX_AGENT_VERSION="${SANDBOX_AGENT_VERSION:-__VERSION__}"
|
SANDBOX_AGENT_VERSION="${SANDBOX_AGENT_VERSION:-__VERSION__}"
|
||||||
SANDBOX_AGENT_BASE_URL="${SANDBOX_AGENT_BASE_URL:-https://releases.rivet.dev}"
|
|
||||||
UNAME="$(uname -s)"
|
UNAME="$(uname -s)"
|
||||||
ARCH="$(uname -m)"
|
ARCH="$(uname -m)"
|
||||||
|
|
||||||
|
# Find asset suffix
|
||||||
if [ "$(printf '%s' "$UNAME" | cut -c 1-6)" = "Darwin" ]; then
|
if [ "$(printf '%s' "$UNAME" | cut -c 1-6)" = "Darwin" ]; then
|
||||||
|
echo
|
||||||
|
echo "> Detected macOS"
|
||||||
|
|
||||||
if [ "$ARCH" = "x86_64" ]; then
|
if [ "$ARCH" = "x86_64" ]; then
|
||||||
FILE_NAME="sandbox-agent-x86_64-apple-darwin"
|
FILE_NAME="sandbox-agent-x86_64-apple-darwin"
|
||||||
elif [ "$ARCH" = "arm64" ]; then
|
elif [ "$ARCH" = "arm64" ]; then
|
||||||
|
|
@ -27,54 +29,62 @@ if [ "$(printf '%s' "$UNAME" | cut -c 1-6)" = "Darwin" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
elif [ "$(printf '%s' "$UNAME" | cut -c 1-5)" = "Linux" ]; then
|
elif [ "$(printf '%s' "$UNAME" | cut -c 1-5)" = "Linux" ]; then
|
||||||
if [ "$ARCH" = "x86_64" ]; then
|
echo
|
||||||
|
echo "> Detected Linux ($(getconf LONG_BIT) bit)"
|
||||||
|
|
||||||
FILE_NAME="sandbox-agent-x86_64-unknown-linux-musl"
|
FILE_NAME="sandbox-agent-x86_64-unknown-linux-musl"
|
||||||
else
|
|
||||||
echo "Unsupported Linux arch $ARCH" 1>&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo "Unable to determine platform" 1>&2
|
echo "Unable to determine platform" 1>&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Determine install location
|
||||||
set +u
|
set +u
|
||||||
if [ -z "$BIN_DIR" ]; then
|
if [ -z "$BIN_DIR" ]; then
|
||||||
BIN_DIR="/usr/local/bin"
|
BIN_DIR="/usr/local/bin"
|
||||||
fi
|
fi
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
INSTALL_PATH="$BIN_DIR/sandbox-agent"
|
INSTALL_PATH="$BIN_DIR/sandbox-agent"
|
||||||
|
|
||||||
if [ ! -d "$BIN_DIR" ]; then
|
if [ ! -d "$BIN_DIR" ]; then
|
||||||
|
# Find the base parent directory. We're using mkdir -p, which recursively creates directories, so we can't rely on `dirname`.
|
||||||
CHECK_DIR="$BIN_DIR"
|
CHECK_DIR="$BIN_DIR"
|
||||||
while [ ! -d "$CHECK_DIR" ] && [ "$CHECK_DIR" != "/" ]; do
|
while [ ! -d "$CHECK_DIR" ] && [ "$CHECK_DIR" != "/" ]; do
|
||||||
CHECK_DIR=$(dirname "$CHECK_DIR")
|
CHECK_DIR=$(dirname "$CHECK_DIR")
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Check if the directory is writable
|
||||||
if [ ! -w "$CHECK_DIR" ]; then
|
if [ ! -w "$CHECK_DIR" ]; then
|
||||||
|
echo
|
||||||
echo "> Creating directory $BIN_DIR (requires sudo)"
|
echo "> Creating directory $BIN_DIR (requires sudo)"
|
||||||
sudo mkdir -p "$BIN_DIR"
|
sudo mkdir -p "$BIN_DIR"
|
||||||
else
|
else
|
||||||
|
echo
|
||||||
echo "> Creating directory $BIN_DIR"
|
echo "> Creating directory $BIN_DIR"
|
||||||
mkdir -p "$BIN_DIR"
|
mkdir -p "$BIN_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
URL="$SANDBOX_AGENT_BASE_URL/sandbox-agent/${SANDBOX_AGENT_VERSION}/${FILE_NAME}"
|
# Download binary
|
||||||
|
URL="https://releases.rivet.dev/sandbox-agent/${SANDBOX_AGENT_VERSION}/binaries/${FILE_NAME}"
|
||||||
|
echo
|
||||||
echo "> Downloading $URL"
|
echo "> Downloading $URL"
|
||||||
|
|
||||||
curl -fsSL "$URL" -o sandbox-agent
|
curl -fsSL "$URL" -o sandbox-agent
|
||||||
chmod +x sandbox-agent
|
chmod +x sandbox-agent
|
||||||
|
|
||||||
|
# Move binary
|
||||||
if [ ! -w "$BIN_DIR" ]; then
|
if [ ! -w "$BIN_DIR" ]; then
|
||||||
|
echo
|
||||||
echo "> Installing sandbox-agent to $INSTALL_PATH (requires sudo)"
|
echo "> Installing sandbox-agent to $INSTALL_PATH (requires sudo)"
|
||||||
sudo mv ./sandbox-agent "$INSTALL_PATH"
|
sudo mv ./sandbox-agent "$INSTALL_PATH"
|
||||||
else
|
else
|
||||||
|
echo
|
||||||
echo "> Installing sandbox-agent to $INSTALL_PATH"
|
echo "> Installing sandbox-agent to $INSTALL_PATH"
|
||||||
mv ./sandbox-agent "$INSTALL_PATH"
|
mv ./sandbox-agent "$INSTALL_PATH"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Check if path may be incorrect
|
||||||
case ":$PATH:" in
|
case ":$PATH:" in
|
||||||
*:$BIN_DIR:*) ;;
|
*:$BIN_DIR:*) ;;
|
||||||
*)
|
*)
|
||||||
|
|
@ -84,4 +94,10 @@ case ":$PATH:" in
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
echo "sandbox-agent installed successfully."
|
echo
|
||||||
|
echo "> Checking installation"
|
||||||
|
"$BIN_DIR/sandbox-agent" --version
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "sandbox-agent was installed successfully."
|
||||||
|
echo "Run 'sandbox-agent --help' to get started."
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
{
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"types": ["node"],
|
||||||
"module": "NodeNext",
|
"paths": {
|
||||||
"moduleResolution": "NodeNext",
|
"@/*": ["./src/*"]
|
||||||
"esModuleInterop": true,
|
}
|
||||||
"strict": true,
|
},
|
||||||
"skipLibCheck": true,
|
"include": ["**/*.ts"],
|
||||||
"noEmit": true
|
"exclude": ["node_modules"]
|
||||||
},
|
|
||||||
"include": ["*.ts"]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,21 @@
|
||||||
import * as fs from "node:fs/promises";
|
import * as fs from "node:fs/promises";
|
||||||
import { glob } from "glob";
|
|
||||||
import { $ } from "execa";
|
import { $ } from "execa";
|
||||||
import type { ReleaseOpts } from "./main.js";
|
import { glob } from "glob";
|
||||||
import { assert } from "./utils.js";
|
import type { ReleaseOpts } from "./main";
|
||||||
|
|
||||||
|
function assert(condition: any, message?: string): asserts condition {
|
||||||
|
if (!condition) {
|
||||||
|
throw new Error(message || "Assertion failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateVersion(opts: ReleaseOpts) {
|
export async function updateVersion(opts: ReleaseOpts) {
|
||||||
|
// Define substitutions
|
||||||
const findReplace = [
|
const findReplace = [
|
||||||
{
|
{
|
||||||
path: "Cargo.toml",
|
path: "Cargo.toml",
|
||||||
find: /^version = ".*"/m,
|
find: /\[workspace\.package\]\nversion = ".*"/,
|
||||||
replace: `version = "${opts.version}"`,
|
replace: `[workspace.package]\nversion = "${opts.version}"`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "sdks/typescript/package.json",
|
path: "sdks/typescript/package.json",
|
||||||
|
|
@ -28,41 +34,17 @@ export async function updateVersion(opts: ReleaseOpts) {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Substitute all files
|
||||||
for (const { path: globPath, find, replace } of findReplace) {
|
for (const { path: globPath, find, replace } of findReplace) {
|
||||||
const paths = await glob(globPath, { cwd: opts.root });
|
const paths = await glob(globPath, { cwd: opts.root });
|
||||||
assert(paths.length > 0, `no paths matched: ${globPath}`);
|
assert(paths.length > 0, `no paths matched: ${globPath}`);
|
||||||
|
for (const path of paths) {
|
||||||
for (const filePath of paths) {
|
const file = await fs.readFile(path, "utf-8");
|
||||||
const fullPath = `${opts.root}/${filePath}`;
|
assert(find.test(file), `file does not match ${find}: ${path}`);
|
||||||
const file = await fs.readFile(fullPath, "utf-8");
|
|
||||||
assert(find.test(file), `file does not match ${find}: ${filePath}`);
|
|
||||||
|
|
||||||
const newFile = file.replace(find, replace);
|
const newFile = file.replace(find, replace);
|
||||||
await fs.writeFile(fullPath, newFile);
|
await fs.writeFile(path, newFile);
|
||||||
|
|
||||||
await $({ cwd: opts.root })`git add ${filePath}`;
|
await $({ cwd: opts.root })`git add ${path}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update optionalDependencies in CLI package.json
|
|
||||||
const cliPkgPath = `${opts.root}/sdks/cli/package.json`;
|
|
||||||
const cliPkg = JSON.parse(await fs.readFile(cliPkgPath, "utf-8"));
|
|
||||||
if (cliPkg.optionalDependencies) {
|
|
||||||
for (const dep of Object.keys(cliPkg.optionalDependencies)) {
|
|
||||||
cliPkg.optionalDependencies[dep] = opts.version;
|
|
||||||
}
|
|
||||||
await fs.writeFile(cliPkgPath, JSON.stringify(cliPkg, null, 2) + "\n");
|
|
||||||
await $({ cwd: opts.root })`git add sdks/cli/package.json`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update optionalDependencies in TypeScript SDK package.json
|
|
||||||
const sdkPkgPath = `${opts.root}/sdks/typescript/package.json`;
|
|
||||||
const sdkPkg = JSON.parse(await fs.readFile(sdkPkgPath, "utf-8"));
|
|
||||||
if (sdkPkg.optionalDependencies) {
|
|
||||||
for (const dep of Object.keys(sdkPkg.optionalDependencies)) {
|
|
||||||
sdkPkg.optionalDependencies[dep] = opts.version;
|
|
||||||
}
|
|
||||||
await fs.writeFile(sdkPkgPath, JSON.stringify(sdkPkg, null, 2) + "\n");
|
|
||||||
await $({ cwd: opts.root })`git add sdks/typescript/package.json`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,177 @@
|
||||||
import * as fs from "node:fs/promises";
|
import * as fs from "node:fs/promises";
|
||||||
import { $ } from "execa";
|
import { $ } from "execa";
|
||||||
|
|
||||||
export function assert(condition: unknown, message?: string): asserts condition {
|
export const PREFIX = "sandbox-agent";
|
||||||
|
|
||||||
|
export function assert(condition: any, message?: string): asserts condition {
|
||||||
if (!condition) {
|
if (!condition) {
|
||||||
throw new Error(message || "Assertion failed");
|
throw new Error(message || "Assertion failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a version string or commit hash to a git ref.
|
||||||
|
* If the input contains a dot, it's treated as a version (e.g., "0.1.0" -> "v0.1.0").
|
||||||
|
* Otherwise, it's treated as a git revision and returned as-is (e.g., "bb7f292").
|
||||||
|
*/
|
||||||
|
export function versionOrCommitToRef(versionOrCommit: string): string {
|
||||||
|
if (versionOrCommit.includes(".")) {
|
||||||
|
assert(
|
||||||
|
!versionOrCommit.startsWith("v"),
|
||||||
|
`Version should not start with "v" (got "${versionOrCommit}", use "${versionOrCommit.slice(1)}" instead)`,
|
||||||
|
);
|
||||||
|
return `v${versionOrCommit}`;
|
||||||
|
}
|
||||||
|
return versionOrCommit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a git ref from the remote. For tags, fetches all tags. For commits, unshallows the repo.
|
||||||
|
*/
|
||||||
|
export async function fetchGitRef(ref: string): Promise<void> {
|
||||||
|
if (ref.startsWith("v")) {
|
||||||
|
console.log(`Fetching tags...`);
|
||||||
|
await $({ stdio: "inherit" })`git fetch --tags --force`;
|
||||||
|
} else {
|
||||||
|
// Git doesn't allow fetching commits directly by SHA, and CI often uses
|
||||||
|
// shallow clones. Unshallow the repo to ensure the commit is available.
|
||||||
|
console.log(`Unshallowing repo to find commit ${ref}...`);
|
||||||
|
try {
|
||||||
|
await $({ stdio: "inherit" })`git fetch --unshallow origin`;
|
||||||
|
} catch {
|
||||||
|
// Already unshallowed, just fetch
|
||||||
|
await $({ stdio: "inherit" })`git fetch origin`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReleasesS3Config {
|
||||||
|
awsEnv: Record<string, string>;
|
||||||
|
endpointUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedConfig: ReleasesS3Config | null = null;
|
||||||
|
|
||||||
|
async function getReleasesS3Config(): Promise<ReleasesS3Config> {
|
||||||
|
if (cachedConfig) {
|
||||||
|
return cachedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
let awsAccessKeyId = process.env.R2_RELEASES_ACCESS_KEY_ID;
|
||||||
|
if (!awsAccessKeyId) {
|
||||||
|
const result =
|
||||||
|
await $`op read ${"op://Engineering/rivet-releases R2 Upload/username"}`;
|
||||||
|
awsAccessKeyId = result.stdout.trim();
|
||||||
|
}
|
||||||
|
let awsSecretAccessKey = process.env.R2_RELEASES_SECRET_ACCESS_KEY;
|
||||||
|
if (!awsSecretAccessKey) {
|
||||||
|
const result =
|
||||||
|
await $`op read ${"op://Engineering/rivet-releases R2 Upload/password"}`;
|
||||||
|
awsSecretAccessKey = result.stdout.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(awsAccessKeyId, "AWS_ACCESS_KEY_ID is required");
|
||||||
|
assert(awsSecretAccessKey, "AWS_SECRET_ACCESS_KEY is required");
|
||||||
|
|
||||||
|
cachedConfig = {
|
||||||
|
awsEnv: {
|
||||||
|
AWS_ACCESS_KEY_ID: awsAccessKeyId,
|
||||||
|
AWS_SECRET_ACCESS_KEY: awsSecretAccessKey,
|
||||||
|
AWS_DEFAULT_REGION: "auto",
|
||||||
|
},
|
||||||
|
endpointUrl:
|
||||||
|
"https://2a94c6a0ced8d35ea63cddc86c2681e7.r2.cloudflarestorage.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
return cachedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadDirToReleases(
|
||||||
|
localPath: string,
|
||||||
|
remotePath: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
||||||
|
// Use --checksum-algorithm CRC32 for R2 compatibility (matches CI upload in release.yaml)
|
||||||
|
await $({
|
||||||
|
env: awsEnv,
|
||||||
|
shell: true,
|
||||||
|
stdio: "inherit",
|
||||||
|
})`aws s3 cp ${localPath} s3://rivet-releases/${remotePath} --recursive --checksum-algorithm CRC32 --endpoint-url ${endpointUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadContentToReleases(
|
||||||
|
content: string,
|
||||||
|
remotePath: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
||||||
|
await $({
|
||||||
|
env: awsEnv,
|
||||||
|
input: content,
|
||||||
|
shell: true,
|
||||||
|
stdio: ["pipe", "inherit", "inherit"],
|
||||||
|
})`aws s3 cp - s3://rivet-releases/${remotePath} --endpoint-url ${endpointUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListReleasesResult {
|
||||||
|
Contents?: { Key: string; Size: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listReleasesObjects(
|
||||||
|
prefix: string,
|
||||||
|
): Promise<ListReleasesResult> {
|
||||||
|
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
||||||
|
const result = await $({
|
||||||
|
env: awsEnv,
|
||||||
|
shell: true,
|
||||||
|
stdio: ["pipe", "pipe", "inherit"],
|
||||||
|
})`aws s3api list-objects --bucket rivet-releases --prefix ${prefix} --endpoint-url ${endpointUrl}`;
|
||||||
|
return JSON.parse(result.stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteReleasesPath(remotePath: string): Promise<void> {
|
||||||
|
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
||||||
|
await $({
|
||||||
|
env: awsEnv,
|
||||||
|
shell: true,
|
||||||
|
stdio: "inherit",
|
||||||
|
})`aws s3 rm s3://rivet-releases/${remotePath} --recursive --endpoint-url ${endpointUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies objects from one S3 path to another within the releases bucket.
|
||||||
|
*
|
||||||
|
* NOTE: We implement our own recursive copy instead of using `aws s3 cp --recursive`
|
||||||
|
* because of a Cloudflare R2 bug. R2 doesn't support the `x-amz-tagging-directive`
|
||||||
|
* header, which the AWS CLI sends even with `--copy-props none` for small files.
|
||||||
|
* Using `s3api copy-object` directly avoids this header.
|
||||||
|
*
|
||||||
|
* See: https://community.cloudflare.com/t/r2-s3-compat-doesnt-support-net-sdk-for-copy-operations-due-to-tagging-header/616867
|
||||||
|
*/
|
||||||
|
export async function copyReleasesPath(
|
||||||
|
sourcePath: string,
|
||||||
|
targetPath: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
||||||
|
|
||||||
|
const listResult = await $({
|
||||||
|
env: awsEnv,
|
||||||
|
})`aws s3api list-objects --bucket rivet-releases --prefix ${sourcePath} --endpoint-url ${endpointUrl}`;
|
||||||
|
|
||||||
|
const objects = JSON.parse(listResult.stdout);
|
||||||
|
if (!objects.Contents?.length) {
|
||||||
|
throw new Error(`No objects found under ${sourcePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const obj of objects.Contents) {
|
||||||
|
const sourceKey = obj.Key;
|
||||||
|
const targetKey = sourceKey.replace(sourcePath, targetPath);
|
||||||
|
console.log(` ${sourceKey} -> ${targetKey}`);
|
||||||
|
await $({
|
||||||
|
env: awsEnv,
|
||||||
|
})`aws s3api copy-object --bucket rivet-releases --key ${targetKey} --copy-source rivet-releases/${sourceKey} --endpoint-url ${endpointUrl}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function assertEquals<T>(actual: T, expected: T, message?: string): void {
|
export function assertEquals<T>(actual: T, expected: T, message?: string): void {
|
||||||
if (actual !== expected) {
|
if (actual !== expected) {
|
||||||
throw new Error(message || `Expected ${expected}, got ${actual}`);
|
throw new Error(message || `Expected ${expected}, got ${actual}`);
|
||||||
|
|
@ -28,148 +193,10 @@ export async function assertDirExists(dirPath: string): Promise<void> {
|
||||||
if (!stat.isDirectory()) {
|
if (!stat.isDirectory()) {
|
||||||
throw new Error(`Path exists but is not a directory: ${dirPath}`);
|
throw new Error(`Path exists but is not a directory: ${dirPath}`);
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: any) {
|
||||||
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
|
if (err.code === "ENOENT") {
|
||||||
throw new Error(`Directory not found: ${dirPath}`);
|
throw new Error(`Directory not found: ${dirPath}`);
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// R2 configuration
|
|
||||||
const ENDPOINT_URL = "https://2a94c6a0ced8d35ea63cddc86c2681e7.r2.cloudflarestorage.com";
|
|
||||||
const BUCKET = "rivet-releases";
|
|
||||||
|
|
||||||
interface ReleasesS3Config {
|
|
||||||
awsEnv: Record<string, string>;
|
|
||||||
endpointUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cachedConfig: ReleasesS3Config | null = null;
|
|
||||||
|
|
||||||
export async function getReleasesS3Config(): Promise<ReleasesS3Config> {
|
|
||||||
if (cachedConfig) {
|
|
||||||
return cachedConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
let awsAccessKeyId = process.env.R2_RELEASES_ACCESS_KEY_ID || process.env.AWS_ACCESS_KEY_ID;
|
|
||||||
let awsSecretAccessKey = process.env.R2_RELEASES_SECRET_ACCESS_KEY || process.env.AWS_SECRET_ACCESS_KEY;
|
|
||||||
|
|
||||||
// Try 1Password fallback for local development
|
|
||||||
if (!awsAccessKeyId) {
|
|
||||||
try {
|
|
||||||
const result = await $`op read ${"op://Engineering/rivet-releases R2 Upload/username"}`;
|
|
||||||
awsAccessKeyId = result.stdout.trim();
|
|
||||||
} catch {
|
|
||||||
// 1Password not available
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!awsSecretAccessKey) {
|
|
||||||
try {
|
|
||||||
const result = await $`op read ${"op://Engineering/rivet-releases R2 Upload/password"}`;
|
|
||||||
awsSecretAccessKey = result.stdout.trim();
|
|
||||||
} catch {
|
|
||||||
// 1Password not available
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(awsAccessKeyId, "R2_RELEASES_ACCESS_KEY_ID is required");
|
|
||||||
assert(awsSecretAccessKey, "R2_RELEASES_SECRET_ACCESS_KEY is required");
|
|
||||||
|
|
||||||
cachedConfig = {
|
|
||||||
awsEnv: {
|
|
||||||
AWS_ACCESS_KEY_ID: awsAccessKeyId,
|
|
||||||
AWS_SECRET_ACCESS_KEY: awsSecretAccessKey,
|
|
||||||
AWS_DEFAULT_REGION: "auto",
|
|
||||||
},
|
|
||||||
endpointUrl: ENDPOINT_URL,
|
|
||||||
};
|
|
||||||
|
|
||||||
return cachedConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function uploadFileToReleases(
|
|
||||||
localPath: string,
|
|
||||||
remotePath: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
|
||||||
await $({
|
|
||||||
env: awsEnv,
|
|
||||||
stdio: "inherit",
|
|
||||||
})`aws s3 cp ${localPath} s3://${BUCKET}/${remotePath} --checksum-algorithm CRC32 --endpoint-url ${endpointUrl}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function uploadDirToReleases(
|
|
||||||
localPath: string,
|
|
||||||
remotePath: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
|
||||||
await $({
|
|
||||||
env: awsEnv,
|
|
||||||
stdio: "inherit",
|
|
||||||
})`aws s3 cp ${localPath} s3://${BUCKET}/${remotePath} --recursive --checksum-algorithm CRC32 --endpoint-url ${endpointUrl}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function uploadContentToReleases(
|
|
||||||
content: string,
|
|
||||||
remotePath: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
|
||||||
await $({
|
|
||||||
env: awsEnv,
|
|
||||||
input: content,
|
|
||||||
stdio: ["pipe", "inherit", "inherit"],
|
|
||||||
})`aws s3 cp - s3://${BUCKET}/${remotePath} --endpoint-url ${endpointUrl}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ListReleasesResult {
|
|
||||||
Contents?: { Key: string; Size: number }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listReleasesObjects(
|
|
||||||
prefix: string,
|
|
||||||
): Promise<ListReleasesResult> {
|
|
||||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
|
||||||
const result = await $({
|
|
||||||
env: awsEnv,
|
|
||||||
stdio: ["pipe", "pipe", "inherit"],
|
|
||||||
})`aws s3api list-objects --bucket ${BUCKET} --prefix ${prefix} --endpoint-url ${endpointUrl}`;
|
|
||||||
return JSON.parse(result.stdout);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteReleasesPath(remotePath: string): Promise<void> {
|
|
||||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
|
||||||
await $({
|
|
||||||
env: awsEnv,
|
|
||||||
stdio: "inherit",
|
|
||||||
})`aws s3 rm s3://${BUCKET}/${remotePath} --recursive --endpoint-url ${endpointUrl}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies objects from one S3 path to another within the releases bucket.
|
|
||||||
* Uses s3api copy-object to avoid R2 tagging header issues.
|
|
||||||
*/
|
|
||||||
export async function copyReleasesPath(
|
|
||||||
sourcePath: string,
|
|
||||||
targetPath: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
|
||||||
|
|
||||||
const listResult = await $({
|
|
||||||
env: awsEnv,
|
|
||||||
})`aws s3api list-objects --bucket ${BUCKET} --prefix ${sourcePath} --endpoint-url ${endpointUrl}`;
|
|
||||||
|
|
||||||
const objects = JSON.parse(listResult.stdout);
|
|
||||||
if (!objects.Contents?.length) {
|
|
||||||
throw new Error(`No objects found under ${sourcePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const obj of objects.Contents) {
|
|
||||||
const sourceKey = obj.Key;
|
|
||||||
const targetKey = sourceKey.replace(sourcePath, targetPath);
|
|
||||||
console.log(` ${sourceKey} -> ${targetKey}`);
|
|
||||||
await $({
|
|
||||||
env: awsEnv,
|
|
||||||
})`aws s3api copy-object --bucket ${BUCKET} --key ${targetKey} --copy-source ${BUCKET}/${sourceKey} --endpoint-url ${endpointUrl}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue