Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,64 @@ jobs:
& $btExe --version
$env:PATH = "$binDir;$env:PATH"
& $btExe self update --check --channel stable

publish-npm:
needs:
- plan
- announce
if: ${{ needs.announce.result == 'success' }}
runs-on: ubuntu-22.04
environment: release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false

- name: Download release archives
env:
TAG: ${{ needs.plan.outputs.tag }}
shell: bash
run: |
set -euo pipefail
mkdir -p archives
gh release download "$TAG" \
--repo "${{ github.repository }}" \
--pattern 'bt-*.tar.gz' \
--pattern 'bt-*.zip' \
--dir archives
ls -la archives

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"

- name: Build npm packages
run: |
node npm/scripts/build-platform-packages.mjs \
--version "${{ needs.plan.outputs.release-version }}" \
--archives-dir archives \
--out-dir npm/dist

- name: Publish platform packages
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
shell: bash
run: |
set -euo pipefail
shopt -s nullglob
for dir in npm/dist/bt-*; do
name="$(node -p "require('./$dir/package.json').name")"
version="$(node -p "require('./$dir/package.json').version")"
# Skip already-published versions so a partial publish can be re-run.
# Test output, not exit code: npm view is empty for a missing version.
published="$(npm view "$name@$version" version 2>/dev/null || true)"
if [ -n "$published" ]; then
echo "Skipping $name@$version (already published)"
continue
fi
echo "Publishing $name@$version"
(cd "$dir" && npm publish --access public --provenance)
done
146 changes: 146 additions & 0 deletions npm/scripts/build-platform-packages.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/usr/bin/env node
// Builds the per-platform npm packages from cargo-dist release archives.
//
// --version <semver> version to stamp into every package.json (required)
// --archives-dir <path> directory containing cargo-dist archives
// (bt-<target>.tar.gz / bt-<target>.zip), required
// --out-dir <path> directory to write packages into (default: npm/dist)
//
// Emits <out-dir>/bt-<pkg>/ (one per target), each ready to `npm publish`.
// The `bt` command is exposed via the `braintrust` SDK, which lists these
// packages as optionalDependencies and ships a launcher that resolves the
// matching binary.

import { execFileSync } from "node:child_process";
import {
chmodSync,
cpSync,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const NPM_DIR = resolve(__dirname, "..");

function parseArgs(argv) {
const args = {};
for (let i = 0; i < argv.length; i++) {
const k = argv[i];
if (k.startsWith("--")) args[k.slice(2)] = argv[++i];
}
return args;
}

const args = parseArgs(process.argv.slice(2));
const version = args.version;
const archivesDir = args["archives-dir"] && resolve(args["archives-dir"]);
const outDir = resolve(args["out-dir"] ?? join(NPM_DIR, "dist"));

if (!version) throw new Error("--version is required");
if (!archivesDir) throw new Error("--archives-dir is required");
if (!existsSync(archivesDir))
throw new Error(`archives-dir not found: ${archivesDir}`);

const targets = JSON.parse(readFileSync(join(NPM_DIR, "targets.json"), "utf8"));

if (existsSync(outDir)) rmSync(outDir, { recursive: true, force: true });
mkdirSync(outDir, { recursive: true });

function extract(archive, dest) {
mkdirSync(dest, { recursive: true });
if (archive.endsWith(".tar.gz")) {
execFileSync("tar", ["-xzf", archive, "-C", dest], { stdio: "inherit" });
} else if (archive.endsWith(".zip")) {
execFileSync("unzip", ["-o", "-q", archive, "-d", dest], {
stdio: "inherit",
});
} else {
throw new Error(`Unsupported archive: ${archive}`);
}
}

function findBinary(rootDir, binName) {
const stack = [rootDir];
while (stack.length) {
const cur = stack.pop();
for (const entry of readdirSync(cur)) {
const full = join(cur, entry);
const s = statSync(full);
if (s.isDirectory()) stack.push(full);
else if (entry === binName) return full;
}
}
throw new Error(`Binary ${binName} not found under ${rootDir}`);
}

// --- Per-platform packages ---
let built = 0;
for (const [target, spec] of Object.entries(targets)) {
const archiveName = `bt-${target}.${spec.archiveExt}`;
const archive = join(archivesDir, archiveName);
if (!existsSync(archive)) {
// Fail hard, don't skip: the SDK pins each package at an exact version, so a
// missing platform would break installs for that platform at runtime.
throw new Error(`Archive not found for ${target}: ${archive}`);
}

const stagingDir = join(outDir, ".staging", target);
extract(archive, stagingDir);
const binPath = findBinary(stagingDir, spec.bin);

const pkgName = `@braintrust/bt-${spec.pkg}`;
const pkgOut = join(outDir, `bt-${spec.pkg}`);
const pkgBin = join(pkgOut, "bin");
mkdirSync(pkgBin, { recursive: true });
cpSync(binPath, join(pkgBin, spec.bin));
if (spec.os !== "win32") chmodSync(join(pkgBin, spec.bin), 0o755);

const platformPkg = {
name: pkgName,
version,
description: `Prebuilt bt binary for ${spec.os}-${spec.cpu}${spec.libc ? `-${spec.libc}` : ""}`,
homepage: "https://github.com/braintrustdata/bt",
repository: {
type: "git",
url: "git+https://github.com/braintrustdata/bt.git",
},
license: "Apache-2.0",
author: "Braintrust engineering <eng@braintrust.dev>",
files: ["bin/"],
os: [spec.os],
cpu: [spec.cpu],
publishConfig: {
access: "public",
provenance: true,
},
preferUnplugged: true,
};
if (spec.libc) platformPkg.libc = [spec.libc];

writeFileSync(
join(pkgOut, "package.json"),
JSON.stringify(platformPkg, null, 2) + "\n",
);
writeFileSync(
join(pkgOut, "README.md"),
`# ${pkgName}\n\nPrebuilt \`bt\` binary for ${spec.os}-${spec.cpu}${spec.libc ? ` (${spec.libc})` : ""}.\n\nInstalled automatically as an optional dependency of [\`braintrust\`](https://www.npmjs.com/package/braintrust), which exposes the \`bt\` command. Install that package instead.\n`,
);

console.log(`Built ${pkgName} -> ${pkgOut}`);
built++;
}

rmSync(join(outDir, ".staging"), { recursive: true, force: true });

const expected = Object.keys(targets).length;
if (built !== expected) {
throw new Error(`Built ${built} packages but expected ${expected}`);
}
console.log(`\nAll ${built} packages written to ${outDir}`);
54 changes: 54 additions & 0 deletions npm/targets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"aarch64-apple-darwin": {
"pkg": "darwin-arm64",
"os": "darwin",
"cpu": "arm64",
"bin": "bt",
"archiveExt": "tar.gz"
},
"x86_64-apple-darwin": {
"pkg": "darwin-x64",
"os": "darwin",
"cpu": "x64",
"bin": "bt",
"archiveExt": "tar.gz"
},
"aarch64-unknown-linux-gnu": {
"pkg": "linux-arm64",
"os": "linux",
"cpu": "arm64",
"libc": "glibc",
"bin": "bt",
"archiveExt": "tar.gz"
},
"x86_64-unknown-linux-gnu": {
"pkg": "linux-x64",
"os": "linux",
"cpu": "x64",
"libc": "glibc",
"bin": "bt",
"archiveExt": "tar.gz"
},
"x86_64-unknown-linux-musl": {
"pkg": "linux-x64-musl",
"os": "linux",
"cpu": "x64",
"libc": "musl",
"bin": "bt",
"archiveExt": "tar.gz"
},
"aarch64-pc-windows-msvc": {
"pkg": "win32-arm64",
"os": "win32",
"cpu": "arm64",
"bin": "bt.exe",
"archiveExt": "zip"
},
"x86_64-pc-windows-msvc": {
"pkg": "win32-x64",
"os": "win32",
"cpu": "x64",
"bin": "bt.exe",
"archiveExt": "zip"
}
}
Loading