mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
docs: document release audit scripts
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// Creates the debug proxy CA and, on macOS, trusts it in the system keychain.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import process from "node:process";
|
||||
import { resolveSystemBin } from "../src/infra/resolve-system-bin.js";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Prunes omitted bundled plugin files and their unshared runtime dependencies
|
||||
// from Docker-oriented production package output.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
@@ -18,6 +20,9 @@ function parsePluginList(value) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses OPENCLAW_EXTENSIONS into the bundled plugin ids that Docker should keep.
|
||||
*/
|
||||
export function parseDockerPluginKeepList(value) {
|
||||
return parsePluginList(value);
|
||||
}
|
||||
@@ -67,7 +72,9 @@ function collectPackageRuntimeClosure(repoRoot, seedPackageNames) {
|
||||
}
|
||||
seen.add(packageName);
|
||||
|
||||
const packageJson = readPackageJson(path.join(nodeModulePath(repoRoot, packageName), "package.json"));
|
||||
const packageJson = readPackageJson(
|
||||
path.join(nodeModulePath(repoRoot, packageName), "package.json"),
|
||||
);
|
||||
for (const dependencyName of collectRuntimeDependencyNames(packageJson)) {
|
||||
if (!seen.has(dependencyName)) {
|
||||
stack.push(dependencyName);
|
||||
@@ -106,7 +113,9 @@ function pruneNodeModulesForOmittedPlugins(repoRoot, bundledPluginDir, omittedPl
|
||||
const omittedSeeds = new Set();
|
||||
|
||||
for (const pluginId of omittedPluginIds) {
|
||||
const packageJson = readPackageJson(path.join(repoRoot, bundledPluginDir, pluginId, "package.json"));
|
||||
const packageJson = readPackageJson(
|
||||
path.join(repoRoot, bundledPluginDir, pluginId, "package.json"),
|
||||
);
|
||||
if (typeof packageJson?.name === "string") {
|
||||
omittedPackageNames.add(packageJson.name);
|
||||
}
|
||||
@@ -116,7 +125,11 @@ function pruneNodeModulesForOmittedPlugins(repoRoot, bundledPluginDir, omittedPl
|
||||
}
|
||||
|
||||
const keptSeeds = new Set(collectRuntimeDependencyNames(rootPackageJson));
|
||||
for (const dependencyName of collectWorkspacePackageRuntimeSeeds(repoRoot, "packages", new Set())) {
|
||||
for (const dependencyName of collectWorkspacePackageRuntimeSeeds(
|
||||
repoRoot,
|
||||
"packages",
|
||||
new Set(),
|
||||
)) {
|
||||
keptSeeds.add(dependencyName);
|
||||
}
|
||||
for (const dependencyName of collectWorkspacePackageRuntimeSeeds(
|
||||
@@ -150,18 +163,25 @@ function pruneNodeModulesForOmittedPlugins(repoRoot, bundledPluginDir, omittedPl
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes omitted plugin dist trees plus node_modules packages not needed by kept runtime code.
|
||||
*/
|
||||
export function pruneDockerPluginDist(params = {}) {
|
||||
const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd();
|
||||
const env = params.env ?? process.env;
|
||||
const bundledPluginDir = env.OPENCLAW_BUNDLED_PLUGIN_DIR ?? "extensions";
|
||||
const keepPluginIds = parseDockerPluginKeepList(env.OPENCLAW_EXTENSIONS);
|
||||
const excludedPluginIds = collectRootPackageExcludedExtensionDirs({ cwd: repoRoot });
|
||||
const omittedPluginIds = new Set([...excludedPluginIds].filter((pluginId) => !keepPluginIds.has(pluginId)));
|
||||
const omittedPluginIds = new Set(
|
||||
[...excludedPluginIds].filter((pluginId) => !keepPluginIds.has(pluginId)),
|
||||
);
|
||||
const removed = [];
|
||||
|
||||
removed.push(...pruneNodeModulesForOmittedPlugins(repoRoot, bundledPluginDir, omittedPluginIds));
|
||||
|
||||
for (const pluginId of [...omittedPluginIds].toSorted((left, right) => left.localeCompare(right))) {
|
||||
for (const pluginId of [...omittedPluginIds].toSorted((left, right) =>
|
||||
left.localeCompare(right),
|
||||
)) {
|
||||
for (const pluginPath of [
|
||||
path.join(bundledPluginDir, pluginId),
|
||||
path.join("dist", "extensions", pluginId),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
// Coordinates release-candidate validation runs and emits the publish command
|
||||
// only after required local, CI, npm, plugin, and E2E evidence is green.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { basename, join } from "node:path";
|
||||
@@ -50,6 +52,9 @@ function requireValue(argv, index, flag) {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses release-candidate validation options and enforces publish-scope policy.
|
||||
*/
|
||||
export function parseArgs(argv) {
|
||||
const args = stripLeadingPackageManagerSeparator(argv);
|
||||
const options = {
|
||||
@@ -201,6 +206,9 @@ function githubApiTimedOut(error) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the GitHub REST API with the gh-auth token and a bounded timeout.
|
||||
*/
|
||||
export async function githubApi(path, options = {}) {
|
||||
const token = options.token ?? run("gh", ["auth", "token"], { capture: true }).trim();
|
||||
const timeoutMs = options.timeoutMs ?? githubApiTimeoutMs();
|
||||
@@ -254,6 +262,9 @@ async function runArtifacts(repo, runId) {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses the expected artifact name, allowing one same-prefix fallback per run.
|
||||
*/
|
||||
export function resolveArtifactName(artifacts, preferredName, prefix) {
|
||||
const available = artifacts
|
||||
.filter((artifact) => artifact.expired !== true)
|
||||
@@ -312,6 +323,9 @@ function runLocalGeneratedCheckIfNeeded(options) {
|
||||
return { status: "passed", command: "pnpm release:generated:check" };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a GitHub Actions run id from gh workflow dispatch output.
|
||||
*/
|
||||
export function parseRunIdFromDispatchOutput(output) {
|
||||
return output.match(/actions\/runs\/([0-9]+)/u)?.[1] ?? "";
|
||||
}
|
||||
@@ -498,6 +512,9 @@ function shellQuote(value) {
|
||||
return `'${String(value).replace(/'/gu, "'\\''")}'`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the final release publish workflow command once validation evidence is ready.
|
||||
*/
|
||||
export function buildPublishCommand(options) {
|
||||
const fields = [
|
||||
["tag", options.tag],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// Checks or refreshes generated release artifacts before a release publish.
|
||||
import { runManagedCommand } from "./lib/managed-child-process.mjs";
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Resolves Docker upgrade-survivor baseline specs from requested tokens and
|
||||
// live release history JSON captured by release workflows.
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { normalizeUpgradeSurvivorBaselineSpec } from "./lib/docker-e2e-plan.mjs";
|
||||
@@ -101,6 +103,9 @@ function readStableReleases(file, publishedVersions) {
|
||||
.toSorted((a, b) => String(b.publishedAt).localeCompare(String(a.publishedAt)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the release-history token into recent stable plus pinned historical baselines.
|
||||
*/
|
||||
export function resolveReleaseHistory(args) {
|
||||
const releasesJson = args.get("releases-json");
|
||||
if (!releasesJson) {
|
||||
@@ -128,6 +133,9 @@ export function resolveReleaseHistory(args) {
|
||||
return dedupeSpecs(versions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the last N stable release versions from release metadata.
|
||||
*/
|
||||
export function resolveLastStable(args, count) {
|
||||
const releasesJson = args.get("releases-json");
|
||||
if (!releasesJson) {
|
||||
@@ -141,6 +149,9 @@ export function resolveLastStable(args, count) {
|
||||
return dedupeSpecs(releases.slice(0, count).map((release) => release.version));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves all stable release versions at or after the requested minimum.
|
||||
*/
|
||||
export function resolveAllSince(args, minimumVersion) {
|
||||
const releasesJson = args.get("releases-json");
|
||||
if (!releasesJson) {
|
||||
@@ -155,6 +166,9 @@ export function resolveAllSince(args, minimumVersion) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands requested baseline tokens into normalized package/version specs.
|
||||
*/
|
||||
export function resolveBaselines(args) {
|
||||
const requested = args.get("requested") ?? "";
|
||||
const fallback = args.get("fallback") ?? "openclaw@latest";
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Audits root package runtime dependencies against source imports and bundled
|
||||
// plugin ownership so extension-owned deps can move out of root.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
@@ -79,6 +81,9 @@ function sectionFor(relativePath) {
|
||||
return section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects static and simple constant-backed package specifiers from source text.
|
||||
*/
|
||||
export function collectModuleSpecifiers(source) {
|
||||
const specifiers = new Set();
|
||||
for (const pattern of IMPORT_PATTERNS) {
|
||||
@@ -210,6 +215,9 @@ function sectionSetIsSubsetOf(sectionSet, allowed) {
|
||||
return sectionSet.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Classifies whether a root dependency is core-owned, shared, or extension-local.
|
||||
*/
|
||||
export function classifyRootDependencyOwnership(record) {
|
||||
const sections = new Set(record.sections);
|
||||
|
||||
@@ -276,6 +284,9 @@ export function classifyRootDependencyOwnership(record) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds dependency ownership records from root package.json and scanned imports.
|
||||
*/
|
||||
export function collectRootDependencyOwnershipAudit(params = {}) {
|
||||
const repoRoot = path.resolve(params.repoRoot ?? process.cwd());
|
||||
const rootPackageJson = readJson(path.join(repoRoot, "package.json"));
|
||||
@@ -352,6 +363,9 @@ export function collectRootDependencyOwnershipAudit(params = {}) {
|
||||
.toSorted((left, right) => left.depName.localeCompare(right.depName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns actionable errors for dependencies that should not remain root-owned.
|
||||
*/
|
||||
export function collectRootDependencyOwnershipCheckErrors(records) {
|
||||
return records
|
||||
.filter((record) => record.category === "extension_only_localizable")
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
// Runs additional architecture and boundary checks with sharding, concurrency,
|
||||
// timeout handling, and grouped CI output.
|
||||
import { spawn } from "node:child_process";
|
||||
import { performance } from "node:perf_hooks";
|
||||
|
||||
@@ -6,6 +8,7 @@ const DEFAULT_CHECK_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
const DEFAULT_OUTPUT_MAX_BYTES = 512 * 1024;
|
||||
const TIMEOUT_KILL_GRACE_MS = 5_000;
|
||||
|
||||
/** Ordered list of supplemental boundary checks used by CI sharding. */
|
||||
export const BOUNDARY_CHECKS = [
|
||||
["prompt:snapshots:check", "pnpm", ["prompt:snapshots:check"]],
|
||||
["plugin-extension-boundary", "pnpm", ["run", "lint:plugins:no-extension-imports"]],
|
||||
@@ -66,10 +69,16 @@ export const BOUNDARY_CHECKS = [
|
||||
["lint:ui:no-raw-window-open", "pnpm", ["lint:ui:no-raw-window-open"]],
|
||||
].map(([label, command, args]) => ({ label, command, args }));
|
||||
|
||||
/**
|
||||
* Resolves the configured boundary-check concurrency.
|
||||
*/
|
||||
export function resolveConcurrency(value, fallback = 4, label = "concurrency") {
|
||||
return resolvePositiveInteger(value, fallback, label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses positive integer CLI/env options with a fallback.
|
||||
*/
|
||||
export function resolvePositiveInteger(value, fallback, label = "value") {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
return fallback;
|
||||
@@ -85,6 +94,9 @@ export function resolvePositiveInteger(value, fallback, label = "value") {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses one N/TOTAL shard selector into zero-based index form.
|
||||
*/
|
||||
export function parseShardSpec(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
@@ -107,6 +119,9 @@ export function parseShardSpec(value) {
|
||||
return { count, index: index - 1, label: `${index}/${count}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a comma-separated list of N/TOTAL shard selectors.
|
||||
*/
|
||||
export function parseShardSelection(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
@@ -124,6 +139,9 @@ export function parseShardSelection(value) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects checks whose ordinal belongs to the requested shard set.
|
||||
*/
|
||||
export function selectChecksForShard(checks, shardSpec) {
|
||||
const shards =
|
||||
typeof shardSpec === "string"
|
||||
@@ -141,10 +159,16 @@ export function selectChecksForShard(checks, shardSpec) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a check command for CI group output.
|
||||
*/
|
||||
export function formatCommand({ command, args }) {
|
||||
return [command, ...args].join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps only the tail of noisy check output so failure logs stay bounded.
|
||||
*/
|
||||
export function createBoundedOutputBuffer(maxBytes = DEFAULT_OUTPUT_MAX_BYTES) {
|
||||
const limit = Math.max(1, maxBytes);
|
||||
const chunks = [];
|
||||
@@ -247,6 +271,9 @@ function installActiveChildCleanup(activeChildren) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs one boundary check with timeout and process-group termination.
|
||||
*/
|
||||
export function runSingleCheck(
|
||||
check,
|
||||
{
|
||||
@@ -359,6 +386,9 @@ function writeTimingSummary(results, output) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs boundary checks with bounded concurrency and returns the failure count.
|
||||
*/
|
||||
export async function runChecks(
|
||||
checks = BOUNDARY_CHECKS,
|
||||
{
|
||||
@@ -442,11 +472,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
process.env.OPENCLAW_ADDITIONAL_BOUNDARY_CONCURRENCY === undefined
|
||||
? "OPENCLAW_EXTENSION_BOUNDARY_CONCURRENCY"
|
||||
: "OPENCLAW_ADDITIONAL_BOUNDARY_CONCURRENCY";
|
||||
const concurrency = resolveConcurrency(
|
||||
concurrencyRaw,
|
||||
4,
|
||||
concurrencyLabel,
|
||||
);
|
||||
const concurrency = resolveConcurrency(concurrencyRaw, 4, concurrencyLabel);
|
||||
const checkTimeoutMs = resolvePositiveInteger(
|
||||
process.env.OPENCLAW_ADDITIONAL_BOUNDARY_TIMEOUT_MS,
|
||||
DEFAULT_CHECK_TIMEOUT_MS,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Runs oxlint over bundled plugin source files using the shared extension lint runner.
|
||||
import { runExtensionOxlint } from "./lib/run-extension-oxlint.mjs";
|
||||
|
||||
runExtensionOxlint({
|
||||
|
||||
Reference in New Issue
Block a user