mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 17:33:51 +02:00
fix(ci): tighten release bootstrap guard
This commit is contained in:
1 parent
48ce651160
commit
6e42909a22
5 files changed
+133
-28
No files matched your search
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
import { buildReleasePackagePlan } from "./release-package-map.mjs";
|
||||
|
||||
@@ -8,41 +8,81 @@ function normalizePath(filePath) {
|
||||
return filePath.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
function npmPackageExists(packageName) {
|
||||
try {
|
||||
execFileSync("npm", ["view", packageName, "name", "--json"], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf8",
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
function classifyNpmViewFailure(output) {
|
||||
return /\bE404\b|404 Not Found|could not be found/i.test(output) ? "missing" : "registry_error";
|
||||
}
|
||||
|
||||
function main(changedPaths) {
|
||||
function inspectNpmPackage(packageName) {
|
||||
const result = spawnSync("npm", ["view", packageName, "name", "--json"], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
if (result.status === 0) {
|
||||
return { status: "exists" };
|
||||
}
|
||||
|
||||
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
|
||||
const failureType = classifyNpmViewFailure(output);
|
||||
|
||||
if (failureType === "missing") {
|
||||
return { status: "missing" };
|
||||
}
|
||||
|
||||
return {
|
||||
status: "registry_error",
|
||||
detail: output || `npm view exited with status ${result.status ?? "unknown"}`,
|
||||
};
|
||||
}
|
||||
|
||||
function collectReleasePackagesForChangedPaths(changedPaths, releasePackages = buildReleasePackagePlan()) {
|
||||
const normalizedChangedPaths = changedPaths.map(normalizePath);
|
||||
const releasePackages = buildReleasePackagePlan();
|
||||
const manifestFileChanged = normalizedChangedPaths.includes("scripts/release-package-manifest.json");
|
||||
const changedReleasePackages = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const pkg of releasePackages) {
|
||||
if (!pkg.publishFromCi) continue;
|
||||
const packageJsonPath = `${pkg.dir}/package.json`;
|
||||
const isRelevant = manifestFileChanged || normalizedChangedPaths.includes(packageJsonPath);
|
||||
|
||||
if (!normalizedChangedPaths.includes(packageJsonPath)) continue;
|
||||
if (!isRelevant) continue;
|
||||
if (seen.has(pkg.name)) continue;
|
||||
|
||||
changedReleasePackages.push(pkg);
|
||||
seen.add(pkg.name);
|
||||
}
|
||||
|
||||
return changedReleasePackages;
|
||||
}
|
||||
|
||||
function main(changedPaths) {
|
||||
const changedReleasePackages = collectReleasePackagesForChangedPaths(changedPaths);
|
||||
|
||||
if (changedReleasePackages.length === 0) {
|
||||
process.stdout.write("No release-enabled package manifests changed in this PR.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
const missingPackages = changedReleasePackages.filter((pkg) => !npmPackageExists(pkg.name));
|
||||
const missingPackages = [];
|
||||
const registryFailures = [];
|
||||
|
||||
for (const pkg of changedReleasePackages) {
|
||||
const npmStatus = inspectNpmPackage(pkg.name);
|
||||
|
||||
if (npmStatus.status === "missing") {
|
||||
missingPackages.push(pkg);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (npmStatus.status === "registry_error") {
|
||||
registryFailures.push({ pkg, detail: npmStatus.detail });
|
||||
}
|
||||
}
|
||||
|
||||
if (missingPackages.length > 0) {
|
||||
const details = missingPackages
|
||||
@@ -55,9 +95,27 @@ function main(changedPaths) {
|
||||
throw new Error(`release package bootstrap check failed:\n- ${details}`);
|
||||
}
|
||||
|
||||
if (registryFailures.length > 0) {
|
||||
const details = registryFailures
|
||||
.map(
|
||||
({ pkg, detail }) =>
|
||||
`${pkg.name} (${pkg.dir}) could not be checked against npm due to a registry error:\n${detail}`,
|
||||
)
|
||||
.join("\n- ");
|
||||
|
||||
throw new Error(`release package bootstrap check could not verify npm state:\n- ${details}`);
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`Release bootstrap OK for changed manifests: ${changedReleasePackages.map((pkg) => pkg.name).join(", ")}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
main(process.argv.slice(2));
|
||||
if (process.argv[1] && normalizePath(process.argv[1]).endsWith("scripts/check-release-package-bootstrap.mjs")) {
|
||||
main(process.argv.slice(2));
|
||||
}
|
||||
|
||||
export {
|
||||
classifyNpmViewFailure,
|
||||
collectReleasePackagesForChangedPaths,
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
classifyNpmViewFailure,
|
||||
collectReleasePackagesForChangedPaths,
|
||||
} from "./check-release-package-bootstrap.mjs";
|
||||
|
||||
test("manifest changes force validation of all release-enabled packages", () => {
|
||||
const releasePackages = [
|
||||
{ dir: "packages/a", name: "@paperclipai/a", publishFromCi: true },
|
||||
{ dir: "packages/b", name: "@paperclipai/b", publishFromCi: true },
|
||||
{ dir: "packages/c", name: "@paperclipai/c", publishFromCi: false },
|
||||
];
|
||||
|
||||
const changedPackages = collectReleasePackagesForChangedPaths(
|
||||
["scripts/release-package-manifest.json"],
|
||||
releasePackages,
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
changedPackages.map((pkg) => pkg.name),
|
||||
["@paperclipai/a", "@paperclipai/b"],
|
||||
);
|
||||
});
|
||||
|
||||
test("package-specific changes only validate affected release-enabled packages", () => {
|
||||
const releasePackages = [
|
||||
{ dir: "packages/a", name: "@paperclipai/a", publishFromCi: true },
|
||||
{ dir: "packages/b", name: "@paperclipai/b", publishFromCi: true },
|
||||
];
|
||||
|
||||
const changedPackages = collectReleasePackagesForChangedPaths(
|
||||
["packages/b/package.json", "README.md"],
|
||||
releasePackages,
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
changedPackages.map((pkg) => pkg.name),
|
||||
["@paperclipai/b"],
|
||||
);
|
||||
});
|
||||
|
||||
test("npm E404 failures are treated as missing packages", () => {
|
||||
assert.equal(classifyNpmViewFailure("npm error code E404"), "missing");
|
||||
assert.equal(classifyNpmViewFailure("404 Not Found"), "missing");
|
||||
});
|
||||
|
||||
test("non-404 npm failures are treated as registry errors", () => {
|
||||
assert.equal(classifyNpmViewFailure("npm error code EAI_AGAIN"), "registry_error");
|
||||
assert.equal(classifyNpmViewFailure("npm error code E429"), "registry_error");
|
||||
});
|
||||
@@ -13,15 +13,10 @@ test("release package manifest covers all public packages with explicit CI enrol
|
||||
assert.ok(packages.every((pkg) => typeof pkg.publishFromCi === "boolean"));
|
||||
});
|
||||
|
||||
test("ACPX adapter remains enrolled for CI publishing", () => {
|
||||
const packages = buildReleasePackagePlan();
|
||||
const acpxPackage = packages.find((pkg) => pkg.name === "@paperclipai/adapter-acpx-local");
|
||||
assert.ok(acpxPackage);
|
||||
assert.equal(acpxPackage.publishFromCi, true);
|
||||
|
||||
const enabledNames = new Set(getReleasePackages().map((pkg) => pkg.name));
|
||||
assert.equal(enabledNames.has("@paperclipai/adapter-acpx-local"), true);
|
||||
assert.equal(enabledNames.has("@paperclipai/adapter-utils"), true);
|
||||
test("release package list only contains CI-enrolled packages", () => {
|
||||
const enabledPackages = getReleasePackages();
|
||||
assert.ok(enabledPackages.length > 0);
|
||||
assert.ok(enabledPackages.every((pkg) => pkg.publishFromCi === true));
|
||||
});
|
||||
|
||||
test("release package configuration validates successfully", () => {
|
||||
|
||||
Reference in new issue
Block a user