mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
Add a Nix flake that builds Pi from the checkout on aarch64/x86_64 Linux and macOS. The build is offline: provider model data is hydrated from a pi.dev model catalog revision pinned in nix/model-catalog.json, and the runtime dependency tree comes from packages/coding-agent/install-lock with internal packages built from source. The Nix workflow keeps the catalog pin current on main, builds release tags on all systems, and fast-forwards the stable branch to each release that builds. The release script refreshes the pin before tagging.
292 lines
8.9 KiB
JavaScript
Executable file
292 lines
8.9 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
/**
|
|
* Release script for pi-mono
|
|
*
|
|
* Usage:
|
|
* node scripts/release.mjs <major|minor|patch>
|
|
* node scripts/release.mjs <x.y.z>
|
|
*
|
|
* Steps:
|
|
* 1. Check for uncommitted changes
|
|
* 2. Verify every public workspace package is registered on npm
|
|
* and refresh the Nix model catalog pin if it is stale
|
|
* 3. Bump version via npm run version:xxx or set an explicit version
|
|
* 4. Update CHANGELOG.md files: [Unreleased] -> [version] - date
|
|
* 5. Regenerate release artifacts
|
|
* 6. Run checks and tests
|
|
* 7. Commit and tag the release
|
|
* 8. Add new [Unreleased] section to changelogs
|
|
* 9. Commit next-cycle changelog updates
|
|
* 10. Push main and the tag to trigger CI publication and verified pi.dev announcement
|
|
*/
|
|
|
|
import { execSync, spawnSync } from "node:child_process";
|
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { findPackageDirectories } from "./package-workspaces.mjs";
|
|
import { getPublicWorkspacePackages } from "./release-packages.mjs";
|
|
|
|
const RELEASE_TARGET = process.argv[2];
|
|
const BUMP_TYPES = new Set(["major", "minor", "patch"]);
|
|
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
|
|
if (!RELEASE_TARGET || (!BUMP_TYPES.has(RELEASE_TARGET) && !SEMVER_RE.test(RELEASE_TARGET))) {
|
|
console.error("Usage: node scripts/release.mjs <major|minor|patch|x.y.z>");
|
|
process.exit(1);
|
|
}
|
|
|
|
function run(cmd, options = {}) {
|
|
console.log(`$ ${cmd}`);
|
|
try {
|
|
return execSync(cmd, { encoding: "utf-8", stdio: options.silent ? "pipe" : "inherit", ...options });
|
|
} catch (e) {
|
|
if (!options.ignoreError) {
|
|
console.error(`Command failed: ${cmd}`);
|
|
process.exit(1);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getVersion() {
|
|
const pkg = JSON.parse(readFileSync("packages/ai/package.json", "utf-8"));
|
|
return pkg.version;
|
|
}
|
|
|
|
function assertPackagesAreRegisteredWithNpm() {
|
|
const packageNames = getPublicWorkspacePackages().map((pkg) => pkg.name);
|
|
const unregisteredPackages = [];
|
|
|
|
console.log("Checking npm package registration...");
|
|
for (const packageName of packageNames) {
|
|
const result = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", ["view", packageName, "version", "--json"], {
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
|
|
if (result.status === 0 && result.stdout.trim()) {
|
|
console.log(` ${packageName}`);
|
|
continue;
|
|
}
|
|
|
|
const output = [result.stdout, result.stderr, result.error?.message].filter(Boolean).join("\n");
|
|
if (output.includes("E404") || output.includes("404 Not Found")) {
|
|
unregisteredPackages.push(packageName);
|
|
continue;
|
|
}
|
|
|
|
throw new Error(output ? `Failed to query npm registration for ${packageName}\n${output}` : `Failed to query npm registration for ${packageName}`);
|
|
}
|
|
|
|
if (unregisteredPackages.length > 0) {
|
|
throw new Error(`The following public workspace packages are not registered on npm:\n${unregisteredPackages.map((packageName) => ` ${packageName}`).join("\n")}\nRegister them before running a release.`);
|
|
}
|
|
|
|
console.log(" All public workspace packages are registered on npm\n");
|
|
}
|
|
|
|
function compareVersions(a, b) {
|
|
const aParts = a.split(".").map(Number);
|
|
const bParts = b.split(".").map(Number);
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
const diff = (aParts[i] || 0) - (bParts[i] || 0);
|
|
if (diff !== 0) {
|
|
return diff;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
function shellQuote(value) {
|
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
}
|
|
|
|
function removeStaleWorkspaceLockEntries() {
|
|
const workspaceVersions = new Map(
|
|
getPublicWorkspacePackages().map((pkg) => [pkg.name, pkg.version]),
|
|
);
|
|
const lockPath = "package-lock.json";
|
|
const lock = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
let removed = 0;
|
|
|
|
for (const [path, pkg] of Object.entries(lock.packages)) {
|
|
if (!path.startsWith("packages/") || pkg.link === true) {
|
|
continue;
|
|
}
|
|
for (const [name, version] of workspaceVersions) {
|
|
if (path.endsWith(`/node_modules/${name}`) && pkg.version !== version) {
|
|
delete lock.packages[path];
|
|
removed++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (removed > 0) {
|
|
writeFileSync(lockPath, `${JSON.stringify(lock, null, "\t")}\n`);
|
|
console.log(`Removed ${removed} stale workspace package lock ${removed === 1 ? "entry" : "entries"}.`);
|
|
}
|
|
}
|
|
|
|
function stageChangedFiles() {
|
|
const output = run("git ls-files -m -o -d --exclude-standard", { silent: true });
|
|
const paths = [...new Set((output || "").split("\n").map((line) => line.trim()).filter(Boolean))];
|
|
if (paths.length === 0) {
|
|
return;
|
|
}
|
|
|
|
run(`git add -- ${paths.map(shellQuote).join(" ")}`);
|
|
}
|
|
|
|
function bumpOrSetVersion(target) {
|
|
const currentVersion = getVersion();
|
|
|
|
if (BUMP_TYPES.has(target)) {
|
|
console.log(`Bumping version (${target})...`);
|
|
run(`npm run version:${target}`);
|
|
} else {
|
|
if (compareVersions(target, currentVersion) <= 0) {
|
|
console.error(`Error: explicit version ${target} must be greater than current version ${currentVersion}.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`Setting explicit version (${target})...`);
|
|
run(`npm version ${target} --workspaces --no-git-tag-version --no-workspaces-update && node scripts/sync-versions.js && npm install --package-lock-only --ignore-scripts`);
|
|
}
|
|
|
|
// npm version can temporarily install the previous workspace versions before
|
|
// sync-versions updates inter-package ranges. Remove those stale lock entries,
|
|
// refresh the lockfile, then hydrate from the final dependency graph.
|
|
removeStaleWorkspaceLockEntries();
|
|
run("npm install --package-lock-only --ignore-scripts");
|
|
run("npm ci --ignore-scripts");
|
|
return getVersion();
|
|
}
|
|
|
|
function getChangelogs() {
|
|
return findPackageDirectories()
|
|
.map((directory) => join(directory, "CHANGELOG.md"))
|
|
.filter((path) => existsSync(path));
|
|
}
|
|
|
|
function updateChangelogsForRelease(version) {
|
|
const date = new Date().toISOString().split("T")[0];
|
|
const changelogs = getChangelogs();
|
|
|
|
for (const changelog of changelogs) {
|
|
const content = readFileSync(changelog, "utf-8");
|
|
|
|
if (!content.includes("## [Unreleased]")) {
|
|
console.log(` Skipping ${changelog}: no [Unreleased] section`);
|
|
continue;
|
|
}
|
|
|
|
const updated = content.replace(
|
|
"## [Unreleased]",
|
|
`## [${version}] - ${date}`
|
|
);
|
|
writeFileSync(changelog, updated);
|
|
console.log(` Updated ${changelog}`);
|
|
}
|
|
}
|
|
|
|
function addUnreleasedSection() {
|
|
const changelogs = getChangelogs();
|
|
const unreleasedSection = "## [Unreleased]\n\n";
|
|
|
|
for (const changelog of changelogs) {
|
|
const content = readFileSync(changelog, "utf-8");
|
|
|
|
// Insert after "# Changelog\n\n"
|
|
const updated = content.replace(
|
|
/^(# Changelog\n\n)/,
|
|
`$1${unreleasedSection}`
|
|
);
|
|
writeFileSync(changelog, updated);
|
|
console.log(` Added [Unreleased] to ${changelog}`);
|
|
}
|
|
}
|
|
|
|
// Main flow
|
|
console.log("\n=== Release Script ===\n");
|
|
|
|
// 1. Check for uncommitted changes
|
|
console.log("Checking for uncommitted changes...");
|
|
const status = run("git status --porcelain", { silent: true });
|
|
if (status && status.trim()) {
|
|
console.error("Error: Uncommitted changes detected. Commit or stash first.");
|
|
console.error(status);
|
|
process.exit(1);
|
|
}
|
|
console.log(" Working directory clean\n");
|
|
|
|
// 2. Verify npm package registration before modifying the worktree.
|
|
assertPackagesAreRegisteredWithNpm();
|
|
|
|
// Release tags are immutable, so the tagged Nix pin must already build the
|
|
// release. Refresh it before the version bump, while pi.dev still knows the
|
|
// current version, so it lands in the release commit.
|
|
console.log("Refreshing the Nix model catalog pin if stale...");
|
|
run("node scripts/update-model-catalog-pin.mjs --if-stale");
|
|
console.log();
|
|
|
|
// 3. Bump or set version
|
|
const version = bumpOrSetVersion(RELEASE_TARGET);
|
|
console.log(` New version: ${version}\n`);
|
|
|
|
// 4. Update changelogs
|
|
console.log("Updating CHANGELOG.md files...");
|
|
updateChangelogsForRelease(version);
|
|
console.log();
|
|
|
|
// 5. Regenerate release artifacts
|
|
console.log("Regenerating release artifacts...");
|
|
run("npm run generate:models");
|
|
run("npm run check:model-data");
|
|
run("npm run install-lock:coding-agent");
|
|
console.log();
|
|
|
|
// 6. Run checks and tests
|
|
console.log("Running checks...");
|
|
run("npm run check");
|
|
console.log();
|
|
|
|
console.log("Building packages for tests...");
|
|
run("npm run build:offline");
|
|
console.log();
|
|
|
|
console.log("Running tests...");
|
|
run("./test.sh");
|
|
console.log();
|
|
|
|
console.log("Checking the packed coding-agent consumer install...");
|
|
run("npm run check:package-install");
|
|
console.log();
|
|
|
|
// 7. Commit and tag
|
|
console.log("Committing and tagging...");
|
|
stageChangedFiles();
|
|
run(`git commit -m "Release v${version}"`);
|
|
run(`git tag v${version}`);
|
|
console.log();
|
|
|
|
// 8. Add new [Unreleased] sections
|
|
console.log("Adding [Unreleased] sections for next cycle...");
|
|
addUnreleasedSection();
|
|
console.log();
|
|
|
|
// 9. Commit
|
|
console.log("Committing changelog updates...");
|
|
stageChangedFiles();
|
|
run(`git commit -m "Add [Unreleased] section for next cycle"`);
|
|
console.log();
|
|
|
|
// 10. Push
|
|
console.log("Pushing to remote...");
|
|
run("git push origin main");
|
|
run(`git push origin v${version}`);
|
|
console.log();
|
|
|
|
console.log(`=== Prepared release v${version}; CI publication and pi.dev announcement start after the tag push ===`);
|