mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
1ece62a4d8
* fix(release): preflight package source metadata * fix(ci): gate source package producers * fix(ci): preflight canonical package producers * fix(ci): skip no-package source setup * fix(ci): normalize source package validation * fix(ci): normalize Docker package reporting * fix(ci): avoid generic dependency guard * test(ci): cover gated package identity
164 lines
5.3 KiB
JavaScript
164 lines
5.3 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { execFileSync } from "node:child_process";
|
|
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { extractCurrentPackageChangelog } from "./package-changelog.mjs";
|
|
import { validateBundledPackageDependencyAlignment } from "./package-source-dependencies.mjs";
|
|
|
|
const FULL_GIT_COMMIT_RE = /^[0-9a-f]{40}$/u;
|
|
const ROOT_MANIFEST_PATH = "package.json";
|
|
const AI_MANIFEST_PATH = "packages/ai/package.json";
|
|
const CHANGELOG_PATH = "CHANGELOG.md";
|
|
|
|
function parseArgs(argv) {
|
|
const options = {
|
|
allowUnreleasedChangelog: false,
|
|
ref: "",
|
|
sourceDir: "",
|
|
};
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (arg === "--allow-unreleased-changelog") {
|
|
options.allowUnreleasedChangelog = true;
|
|
continue;
|
|
}
|
|
if (arg === "--ref") {
|
|
options.ref = argv[index + 1] ?? "";
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (arg === "--source-dir") {
|
|
options.sourceDir = argv[index + 1] ?? "";
|
|
index += 1;
|
|
continue;
|
|
}
|
|
throw new Error(`Unknown package source preflight option: ${arg}`);
|
|
}
|
|
if (options.ref && options.sourceDir) {
|
|
throw new Error("Use exactly one of --ref or --source-dir.");
|
|
}
|
|
if (options.ref && !FULL_GIT_COMMIT_RE.test(options.ref)) {
|
|
throw new Error(`--ref must be a full lowercase commit SHA; got ${options.ref}`);
|
|
}
|
|
if (!options.ref && !options.sourceDir) {
|
|
throw new Error("Use exactly one of --ref or --source-dir.");
|
|
}
|
|
return options;
|
|
}
|
|
|
|
function parseManifest(content, manifestPath) {
|
|
try {
|
|
return JSON.parse(content);
|
|
} catch (error) {
|
|
throw new Error(`Failed to parse ${manifestPath}.`, { cause: error });
|
|
}
|
|
}
|
|
|
|
export function validatePackageSource({
|
|
aiManifestContent,
|
|
allowUnreleasedChangelog = false,
|
|
changelogContent,
|
|
rootManifestContent,
|
|
}) {
|
|
const rootManifest = parseManifest(rootManifestContent, ROOT_MANIFEST_PATH);
|
|
if (typeof rootManifest.version !== "string") {
|
|
throw new Error(`${ROOT_MANIFEST_PATH} version must be a string.`);
|
|
}
|
|
extractCurrentPackageChangelog(changelogContent, rootManifest.version, {
|
|
allowUnreleased: allowUnreleasedChangelog,
|
|
});
|
|
|
|
const rootDependencies = rootManifest.dependencies ?? {};
|
|
const aiDependency = rootDependencies["@openclaw/ai"];
|
|
if (aiManifestContent === null) {
|
|
if (aiDependency === undefined) {
|
|
return rootManifest.version;
|
|
}
|
|
throw new Error(`${ROOT_MANIFEST_PATH} declares @openclaw/ai without ${AI_MANIFEST_PATH}.`);
|
|
}
|
|
|
|
const aiManifest = parseManifest(aiManifestContent, AI_MANIFEST_PATH);
|
|
if (aiDependency !== "workspace:*") {
|
|
throw new Error(
|
|
`${ROOT_MANIFEST_PATH} must depend on @openclaw/ai via workspace:*; found ${JSON.stringify(aiDependency)}.`,
|
|
);
|
|
}
|
|
if (aiManifest.version !== rootManifest.version) {
|
|
throw new Error(
|
|
`${AI_MANIFEST_PATH} version must match ${ROOT_MANIFEST_PATH}: expected ${rootManifest.version}, found ${String(aiManifest.version ?? "<missing>")}.`,
|
|
);
|
|
}
|
|
|
|
validateBundledPackageDependencyAlignment({
|
|
bundledDependencies: aiManifest.dependencies,
|
|
bundledPackageLabel: AI_MANIFEST_PATH,
|
|
rootDependencies,
|
|
rootPackageLabel: ROOT_MANIFEST_PATH,
|
|
});
|
|
return rootManifest.version;
|
|
}
|
|
|
|
function readGitFile(ref, file, { optional = false } = {}) {
|
|
try {
|
|
return execFileSync("git", ["show", `${ref}:${file}`], {
|
|
encoding: "utf8",
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
timeout: 30_000,
|
|
});
|
|
} catch (error) {
|
|
if (optional && error?.status === 128) {
|
|
return null;
|
|
}
|
|
throw new Error(`Unable to read ${file} from ${ref}.`, { cause: error });
|
|
}
|
|
}
|
|
|
|
export function validatePackageSourceRef(ref, options = {}) {
|
|
return validatePackageSource({
|
|
aiManifestContent: readGitFile(ref, AI_MANIFEST_PATH, { optional: true }),
|
|
allowUnreleasedChangelog: options.allowUnreleasedChangelog,
|
|
changelogContent: readGitFile(ref, CHANGELOG_PATH),
|
|
rootManifestContent: readGitFile(ref, ROOT_MANIFEST_PATH),
|
|
});
|
|
}
|
|
|
|
function readSourceFile(sourceDir, file, { optional = false } = {}) {
|
|
try {
|
|
return readFileSync(path.join(sourceDir, file), "utf8");
|
|
} catch (error) {
|
|
if (optional && error?.code === "ENOENT") {
|
|
return null;
|
|
}
|
|
throw new Error(`Unable to read ${file} from ${sourceDir}.`, { cause: error });
|
|
}
|
|
}
|
|
|
|
export function validatePackageSourceDir(sourceDir, options = {}) {
|
|
const resolvedSourceDir = path.resolve(sourceDir);
|
|
return validatePackageSource({
|
|
aiManifestContent: readSourceFile(resolvedSourceDir, AI_MANIFEST_PATH, { optional: true }),
|
|
allowUnreleasedChangelog: options.allowUnreleasedChangelog,
|
|
changelogContent: readSourceFile(resolvedSourceDir, CHANGELOG_PATH),
|
|
rootManifestContent: readSourceFile(resolvedSourceDir, ROOT_MANIFEST_PATH),
|
|
});
|
|
}
|
|
|
|
function main(argv = process.argv.slice(2)) {
|
|
const options = parseArgs(argv);
|
|
const version = options.ref
|
|
? validatePackageSourceRef(options.ref, options)
|
|
: validatePackageSourceDir(options.sourceDir, options);
|
|
console.log(`package-source-preflight: source manifests and changelog are valid (${version}).`);
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
try {
|
|
main();
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exitCode = 1;
|
|
}
|
|
}
|