chore(release): publish packages from CI
This commit is contained in:
115
scripts/publish.mjs
Normal file
115
scripts/publish.mjs
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const packages = [
|
||||
{ directory: "packages/ai", name: "@earendil-works/pi-ai" },
|
||||
{ directory: "packages/agent", name: "@earendil-works/pi-agent-core" },
|
||||
{ directory: "packages/tui", name: "@earendil-works/pi-tui" },
|
||||
{ directory: "packages/coding-agent", name: "@earendil-works/pi-coding-agent" },
|
||||
];
|
||||
|
||||
const dryRun = process.argv.includes("--dry-run");
|
||||
const unknownArgs = process.argv.slice(2).filter((arg) => arg !== "--dry-run");
|
||||
|
||||
if (unknownArgs.length > 0) {
|
||||
console.error(`Usage: node scripts/publish.mjs [--dry-run]`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function commandForPlatform(command) {
|
||||
return process.platform === "win32" ? `${command}.cmd` : command;
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
console.log(`$ ${[command, ...args].join(" ")}`);
|
||||
const result = spawnSync(commandForPlatform(command), args, {
|
||||
cwd: options.cwd,
|
||||
encoding: "utf8",
|
||||
stdio: options.capture ? ["inherit", "pipe", "pipe"] : "inherit",
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
||||
throw new Error(output ? `Command failed: ${command} ${args.join(" ")}\n${output}` : `Command failed: ${command} ${args.join(" ")}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function readPackageJson(directory) {
|
||||
return JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
||||
}
|
||||
|
||||
function assertBuildOutputExists(directory) {
|
||||
if (!existsSync(join(directory, "dist"))) {
|
||||
throw new Error(`${directory}/dist does not exist. Run npm run build before publishing.`);
|
||||
}
|
||||
}
|
||||
|
||||
function validatePack(directory) {
|
||||
const result = run("npm", ["pack", "--dry-run", "--ignore-scripts", "--json"], { capture: true, cwd: directory });
|
||||
const packed = JSON.parse(result.stdout)[0];
|
||||
console.log(` ${packed.filename}: ${packed.files.length} files, ${packed.size} bytes packed, ${packed.unpackedSize} bytes unpacked`);
|
||||
}
|
||||
|
||||
function isPublished(name, version) {
|
||||
const result = spawnSync(commandForPlatform("npm"), ["view", `${name}@${version}`, "version", "--json"], {
|
||||
encoding: "utf8",
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
if (result.status === 0 && result.stdout.trim()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
||||
if (result.status !== 0 && (output.includes("E404") || output.includes("404 Not Found"))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new Error(output ? `Failed to query ${name}@${version}\n${output}` : `Failed to query ${name}@${version}`);
|
||||
}
|
||||
|
||||
const packageVersions = new Map();
|
||||
for (const pkg of packages) {
|
||||
const packageJson = readPackageJson(pkg.directory);
|
||||
if (packageJson.name !== pkg.name) {
|
||||
throw new Error(`${pkg.directory}/package.json has name ${packageJson.name}, expected ${pkg.name}`);
|
||||
}
|
||||
packageVersions.set(pkg.name, packageJson.version);
|
||||
}
|
||||
|
||||
const versions = [...new Set(packageVersions.values())];
|
||||
if (versions.length !== 1) {
|
||||
throw new Error(`Publish packages are not lockstep versioned: ${versions.join(", ")}`);
|
||||
}
|
||||
|
||||
console.log(`Publishing pi packages at ${versions[0]}${dryRun ? " (dry run)" : ""}\n`);
|
||||
|
||||
for (const pkg of packages) {
|
||||
const version = packageVersions.get(pkg.name);
|
||||
assertBuildOutputExists(pkg.directory);
|
||||
const published = isPublished(pkg.name, version);
|
||||
|
||||
if (dryRun) {
|
||||
if (published) {
|
||||
console.log(`${pkg.name}@${version} is already published; validating package contents only.`);
|
||||
} else {
|
||||
console.log(`${pkg.name}@${version} is not published; validating package contents before publish.`);
|
||||
}
|
||||
validatePack(pkg.directory);
|
||||
console.log();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (published) {
|
||||
console.log(`Skipping ${pkg.name}@${version}: already published\n`);
|
||||
continue;
|
||||
}
|
||||
|
||||
run("npm", ["publish", "--access", "public", "--provenance", "--ignore-scripts"], { cwd: pkg.directory });
|
||||
console.log();
|
||||
}
|
||||
@@ -10,11 +10,12 @@
|
||||
* 1. Check for uncommitted changes
|
||||
* 2. Bump version via npm run version:xxx or set an explicit version
|
||||
* 3. Update CHANGELOG.md files: [Unreleased] -> [version] - date
|
||||
* 4. Generate the coding-agent npm-shrinkwrap.json
|
||||
* 5. Commit and tag
|
||||
* 6. Publish to npm
|
||||
* 4. Regenerate release artifacts
|
||||
* 5. Run checks
|
||||
* 6. Commit and tag the release
|
||||
* 7. Add new [Unreleased] section to changelogs
|
||||
* 8. Commit
|
||||
* 8. Commit next-cycle changelog updates
|
||||
* 9. Push main and the tag to trigger CI publishing
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process";
|
||||
@@ -91,7 +92,7 @@ function bumpOrSetVersion(target) {
|
||||
}
|
||||
|
||||
console.log(`Setting explicit version (${target})...`);
|
||||
run(`npm version ${target} -ws --no-git-tag-version && node scripts/sync-versions.js && npm install --package-lock-only`);
|
||||
run(`npm version ${target} -ws --no-git-tag-version && node scripts/sync-versions.js && npm install --package-lock-only --ignore-scripts`);
|
||||
return getVersion();
|
||||
}
|
||||
|
||||
@@ -163,23 +164,25 @@ console.log("Updating CHANGELOG.md files...");
|
||||
updateChangelogsForRelease(version);
|
||||
console.log();
|
||||
|
||||
// 4. Generate publish shrinkwrap
|
||||
console.log("Generating coding-agent shrinkwrap...");
|
||||
// 4. Regenerate release artifacts
|
||||
console.log("Regenerating release artifacts...");
|
||||
run("npm --prefix packages/ai run generate-models");
|
||||
run("npm --prefix packages/ai run generate-image-models");
|
||||
run("npm run shrinkwrap:coding-agent");
|
||||
console.log();
|
||||
|
||||
// 5. Commit and tag
|
||||
// 5. Run checks
|
||||
console.log("Running checks...");
|
||||
run("npm run check");
|
||||
console.log();
|
||||
|
||||
// 6. Commit and tag
|
||||
console.log("Committing and tagging...");
|
||||
stageChangedFiles();
|
||||
run(`git commit -m "Release v${version}"`);
|
||||
run(`git tag v${version}`);
|
||||
console.log();
|
||||
|
||||
// 6. Publish
|
||||
console.log("Publishing to npm...");
|
||||
run("npm run publish");
|
||||
console.log();
|
||||
|
||||
// 7. Add new [Unreleased] sections
|
||||
console.log("Adding [Unreleased] sections for next cycle...");
|
||||
addUnreleasedSection();
|
||||
@@ -197,4 +200,4 @@ run("git push origin main");
|
||||
run(`git push origin v${version}`);
|
||||
console.log();
|
||||
|
||||
console.log(`=== Released v${version} ===`);
|
||||
console.log(`=== Prepared release v${version}; CI publishing starts after the tag push ===`);
|
||||
|
||||
Reference in New Issue
Block a user