feat: publish changed packages independently
This commit is contained in:
@@ -112,8 +112,8 @@ for (const directory of packageDirectories) {
|
||||
if (hasHelperKit) helperPackageCount++;
|
||||
componentCount += components.length;
|
||||
|
||||
if (manifest.version !== releaseVersion)
|
||||
failures.push(`${manifest.name}: version is ${manifest.version}`);
|
||||
if (!/^0\.8\.\d+$/.test(manifest.version))
|
||||
failures.push(`${manifest.name}: invalid independent version ${manifest.version}`);
|
||||
if (!existsSync(entry)) failures.push(`${manifest.name}: missing public entry ${main}`);
|
||||
if (!existsSync(join(packageRoot, "README.md")))
|
||||
failures.push(`${manifest.name}: missing README.md`);
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/** Independent private releases for changed @wrnexus packages. */
|
||||
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const root = resolve(import.meta.dir, "..");
|
||||
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
const mode = process.argv[2] ?? "plan";
|
||||
const requested = process.argv.slice(3).filter((arg) => !arg.startsWith("--"));
|
||||
|
||||
interface PackageInfo {
|
||||
dir: string;
|
||||
name: string;
|
||||
file: string;
|
||||
manifest: Record<string, any>;
|
||||
}
|
||||
|
||||
function run(command: string, args: string[], capture = false, allowFailure = false) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: root,
|
||||
encoding: capture ? "utf8" : undefined,
|
||||
stdio: capture ? "pipe" : "inherit",
|
||||
});
|
||||
if (result.error) throw result.error;
|
||||
if (result.status !== 0 && !allowFailure) {
|
||||
throw new Error(`${command} ${args.join(" ")} failed with ${result.status}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function allPackages(): PackageInfo[] {
|
||||
return readdirSync(join(root, "packages"), { withFileTypes: true }).flatMap((entry) => {
|
||||
const file = join(root, "packages", entry.name, "package.json");
|
||||
if (!entry.isDirectory() || !existsSync(file)) return [];
|
||||
const manifest = JSON.parse(readFileSync(file, "utf8"));
|
||||
return manifest.name?.startsWith("@wrnexus/")
|
||||
? [{ dir: entry.name, name: manifest.name, file, manifest }]
|
||||
: [];
|
||||
});
|
||||
}
|
||||
|
||||
function changedPaths(): string[] {
|
||||
const tracked = String(run("git", ["diff", "HEAD", "--name-only"], true).stdout ?? "");
|
||||
const untracked = String(
|
||||
run("git", ["ls-files", "--others", "--exclude-standard"], true).stdout ?? "",
|
||||
);
|
||||
return `${tracked}\n${untracked}`
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.map((path) => path.replace(/\\/g, "/"));
|
||||
}
|
||||
|
||||
function selectChanged(all: PackageInfo[]): PackageInfo[] {
|
||||
if (requested.length) {
|
||||
return all.filter((pkg) => requested.includes(pkg.dir) || requested.includes(pkg.name));
|
||||
}
|
||||
const paths = changedPaths();
|
||||
return all.filter((pkg) => paths.some((path) => path.startsWith(`packages/${pkg.dir}/`)));
|
||||
}
|
||||
|
||||
function patch(version: string): string {
|
||||
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
||||
if (!match) throw new Error(`Cannot patch-bump invalid package version ${version}`);
|
||||
return `${match[1]}.${match[2]}.${Number(match[3]) + 1}`;
|
||||
}
|
||||
|
||||
function syncWorkspaceLock(pkgs: PackageInfo[]): void {
|
||||
const file = join(root, "bun.lock");
|
||||
let source = readFileSync(file, "utf8");
|
||||
for (const pkg of pkgs) {
|
||||
const escaped = pkg.dir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const pattern = new RegExp(
|
||||
`("packages/${escaped}": \\{\\r?\\n\\s+"name": "[^"]+",\\r?\\n\\s+"version": ")[^"]+(")`,
|
||||
);
|
||||
if (!pattern.test(source)) throw new Error(`bun.lock is missing workspace package ${pkg.name}`);
|
||||
source = source.replace(pattern, `$1${pkg.manifest.version}$2`);
|
||||
}
|
||||
writeFileSync(file, source);
|
||||
}
|
||||
|
||||
function registryHas(pkg: PackageInfo): boolean {
|
||||
return (
|
||||
run(npm, ["view", `${pkg.name}@${pkg.manifest.version}`, "version", "--json"], true, true)
|
||||
.status === 0
|
||||
);
|
||||
}
|
||||
|
||||
function unpublished(all: PackageInfo[]): PackageInfo[] {
|
||||
return all.filter((pkg) => !registryHas(pkg));
|
||||
}
|
||||
|
||||
function print(pkgs: PackageInfo[], action: string): void {
|
||||
if (!pkgs.length) {
|
||||
console.log(`No packages need ${action}.`);
|
||||
return;
|
||||
}
|
||||
console.log(`Packages selected for ${action}:`);
|
||||
for (const pkg of pkgs) console.log(` ${pkg.name}@${pkg.manifest.version}`);
|
||||
}
|
||||
|
||||
const all = allPackages();
|
||||
if (mode === "plan") {
|
||||
const selected = selectChanged(all);
|
||||
print(selected, "a patch release");
|
||||
for (const pkg of selected) console.log(` -> ${patch(pkg.manifest.version)}`);
|
||||
} else if (mode === "prepare") {
|
||||
const selected = selectChanged(all);
|
||||
if (!selected.length)
|
||||
throw new Error("No changed packages found. Pass package names explicitly if needed.");
|
||||
for (const pkg of selected) {
|
||||
pkg.manifest.version = patch(String(pkg.manifest.version));
|
||||
writeFileSync(pkg.file, `${JSON.stringify(pkg.manifest, null, 2)}\n`);
|
||||
}
|
||||
syncWorkspaceLock(selected);
|
||||
// Bun currently leaves workspace manifest versions stale when only
|
||||
// --lockfile-only is used. The workspace entries are synchronized above;
|
||||
// this install refreshes dependency resolution without changing versions.
|
||||
run(process.execPath, ["install"]);
|
||||
run(process.execPath, ["run", "scripts/publish-packages.ts", ...selected.map((pkg) => pkg.name)]);
|
||||
print(selected, "release preparation");
|
||||
console.log(
|
||||
"Review and commit the version and lockfile changes, then run bun run release:changed:publish.",
|
||||
);
|
||||
} else if (mode === "publish") {
|
||||
const selected = requested.length ? selectChanged(all) : unpublished(all);
|
||||
if (!selected.length) throw new Error("Every local package version is already published.");
|
||||
run(process.execPath, ["run", "check:component-imports"]);
|
||||
run(process.execPath, ["test", ...selected.map((pkg) => `packages/${pkg.dir}`)]);
|
||||
run(process.execPath, ["run", "scripts/publish-packages.ts", ...selected.map((pkg) => pkg.name)]);
|
||||
run(npm, ["whoami"]);
|
||||
for (const pkg of selected) {
|
||||
const stage = join(root, ".publish", pkg.dir);
|
||||
if (!registryHas(pkg)) run(npm, ["publish", stage, "--access", "restricted"]);
|
||||
console.log(` ✓ ${pkg.name}@${pkg.manifest.version}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error("Usage: bun scripts/release-changed.ts plan | prepare | publish [package ...]");
|
||||
}
|
||||
@@ -60,8 +60,8 @@ const manifests = trackedFiles
|
||||
.filter(({ value }) => value.name?.startsWith("@wrnexus/"));
|
||||
addCheck(
|
||||
"SEC-PACKAGE-VERSION",
|
||||
manifests.every(({ value }) => value.version === frameworkVersion),
|
||||
`Every @wrnexus package must use version ${frameworkVersion}.`,
|
||||
manifests.every(({ value }) => /^0\.8\.\d+$/.test(value.version)),
|
||||
"Every @wrnexus package must use a valid independent 0.8 version.",
|
||||
);
|
||||
addCheck(
|
||||
"SEC-PRIVATE-PUBLISH",
|
||||
|
||||
@@ -82,8 +82,8 @@ check(
|
||||
"Temporary focused helpers must not create lint or formatting cascades before workspace repair.",
|
||||
);
|
||||
check(
|
||||
"all framework package versions align",
|
||||
manifests.every(({ manifest }) => manifest.version === rootVersion),
|
||||
"all framework packages use compatible independent versions",
|
||||
manifests.every(({ manifest }) => /^0\.(?:7|8)\.\d+$/.test(manifest.version)),
|
||||
[...new Set(manifests.map(({ manifest }) => manifest.version))].join(", "),
|
||||
);
|
||||
for (const name of ["security", "cache", "image", "observability", "benchmark"]) {
|
||||
|
||||
@@ -59,19 +59,28 @@ check(
|
||||
);
|
||||
check("root version is a 0.8 release", /^0\.8\.\d+$/.test(releaseVersion));
|
||||
check(
|
||||
`all framework packages use ${releaseVersion}`,
|
||||
packageManifests.every((manifest) => manifest.version === releaseVersion),
|
||||
"all framework packages use valid independent 0.8 versions",
|
||||
packageManifests.every((manifest) => /^0\.8\.\d+$/.test(manifest.version)),
|
||||
);
|
||||
const bunLockPackageVersions = [
|
||||
const bunLockPackages = [
|
||||
...text("bun.lock").matchAll(/^ {4}"packages\/[^"\r\n]+": \{\r?\n([\s\S]*?)^ {4}\},?$/gm),
|
||||
].map((match) => /^ {6}"version": "([^"]+)"/m.exec(match[1])?.[1]);
|
||||
].map((match) => ({
|
||||
name: /^ {6}"name": "([^"]+)"/m.exec(match[1])?.[1],
|
||||
version: /^ {6}"version": "([^"]+)"/m.exec(match[1])?.[1],
|
||||
}));
|
||||
const packageVersions = new Map(
|
||||
packageManifests.map((manifest) => [manifest.name, manifest.version]),
|
||||
);
|
||||
check(
|
||||
`bun.lock framework workspaces use ${releaseVersion}`,
|
||||
bunLockPackageVersions.length === packageManifests.length &&
|
||||
bunLockPackageVersions.every((version) => version === releaseVersion),
|
||||
"bun.lock matches each independent framework package version",
|
||||
bunLockPackages.length === packageManifests.length &&
|
||||
bunLockPackages.every(({ name, version }) => packageVersions.get(name) === version),
|
||||
);
|
||||
const editorManifest = JSON.parse(text("editors/vscode/package.json"));
|
||||
check(`VS Code extension uses ${releaseVersion}`, editorManifest.version === releaseVersion);
|
||||
check(
|
||||
"VS Code extension uses a valid independent 0.8 version",
|
||||
/^0\.8\.\d+$/.test(editorManifest.version),
|
||||
);
|
||||
check(
|
||||
"standalone realtime package exists",
|
||||
existsSync(join(root, "packages/realtime/src/index.ts")) &&
|
||||
|
||||
Reference in New Issue
Block a user