mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
chore: harden npm shrinkwrap release path
This commit is contained in:
@@ -13,6 +13,10 @@
|
||||
/.github/workflows/codeql-critical-quality.yml @openclaw/openclaw-secops
|
||||
/.github/workflows/dependency-change-awareness.yml @openclaw/openclaw-secops
|
||||
/test/scripts/dependency-change-awareness-workflow.test.ts @openclaw/openclaw-secops
|
||||
/package-lock.json @openclaw/openclaw-secops
|
||||
/npm-shrinkwrap.json @openclaw/openclaw-secops
|
||||
/pnpm-lock.yaml @openclaw/openclaw-secops
|
||||
/scripts/generate-npm-shrinkwrap.mjs @openclaw/openclaw-secops
|
||||
/src/security/ @openclaw/openclaw-secops
|
||||
/src/secrets/ @openclaw/openclaw-secops
|
||||
/src/config/*secret*.ts @openclaw/openclaw-secops
|
||||
|
||||
@@ -34,6 +34,8 @@ jobs:
|
||||
|
||||
const isDependencyFile = (filename) =>
|
||||
filename === "package.json" ||
|
||||
filename === "package-lock.json" ||
|
||||
filename === "npm-shrinkwrap.json" ||
|
||||
filename === "pnpm-lock.yaml" ||
|
||||
filename === "pnpm-workspace.yaml" ||
|
||||
filename === "ui/package.json" ||
|
||||
@@ -143,7 +145,8 @@ jobs:
|
||||
"",
|
||||
"Maintainer follow-up:",
|
||||
"- Review whether the dependency changes are intentional.",
|
||||
"- Inspect resolved package deltas when lockfile or workspace dependency policy changes are present.",
|
||||
"- Inspect resolved package deltas when lockfile, shrinkwrap, or workspace dependency policy changes are present.",
|
||||
"- Treat `package-lock.json` and `npm-shrinkwrap.json` diffs as security-review surfaces.",
|
||||
"- Run `pnpm deps:changes:report -- --base-ref origin/main --markdown /tmp/dependency-changes.md --json /tmp/dependency-changes.json` locally for detailed release-style evidence.",
|
||||
].join("\n");
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const excludedLockfiles = new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"]);
|
||||
const excludedLockfiles = new Set(["pnpm-lock.yaml", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "bun.lockb"]);
|
||||
const totalChangedLines = files.reduce((total, file) => {
|
||||
const path = file.filename ?? "";
|
||||
if (path.startsWith("docs/") || excludedLockfiles.has(path)) {
|
||||
@@ -603,7 +603,7 @@ jobs:
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const excludedLockfiles = new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"]);
|
||||
const excludedLockfiles = new Set(["pnpm-lock.yaml", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "bun.lockb"]);
|
||||
const totalChangedLines = files.reduce((total, file) => {
|
||||
const path = file.filename ?? "";
|
||||
if (path.startsWith("docs/") || excludedLockfiles.has(path)) {
|
||||
|
||||
@@ -161,6 +161,7 @@ Skills own workflows; root owns hard policy and routing.
|
||||
- Never commit real phone numbers, videos, credentials, live config.
|
||||
- Secrets: channel/provider creds in `~/.openclaw/credentials/`; model auth profiles in `~/.openclaw/agents/<agentId>/agent/auth-profiles.json`.
|
||||
- Dependency patches/overrides/vendor changes need explicit approval. `pnpm-workspace.yaml` patched dependencies use exact versions only.
|
||||
- Lockfiles/shrinkwrap are security surface: review `pnpm-lock.yaml`, `npm-shrinkwrap.json`, `package-lock.json`; npm package ships shrinkwrap, not package-lock.
|
||||
- Carbon pins owner-only: do not change `@buape/carbon` unless Shadow (`@thewilloftheshadow`, verified by `gh`) asks.
|
||||
- Releases/publish/version bumps need explicit approval. Use `$openclaw-release-maintainer`.
|
||||
- GHSA/advisories: `$openclaw-ghsa-maintainer` / `$security-triage`. Secret scanning: `$openclaw-secret-scanning-maintainer`.
|
||||
|
||||
@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
|
||||
- QA-Lab: include an opt-in `update.run` package self-upgrade sentinel for destructive latest-package recovery checks.
|
||||
- QA-Lab: add Codex plugin lifecycle and auth-profile fixture coverage for missing installs, pinned-version drift, first-turn install ordering, and doctor migration safety. (#80323, refs #80174) Thanks @100yenadmin.
|
||||
- Models/perf: pre-warm the provider auth-state map at gateway startup so `/models` and every model-listing call short-circuits the per-provider plugin / external-CLI discovery on the hot path. Per-call cost drops from ~20 s to ~5 ms (~4,100×); the one-time startup warm resets and re-warms after hot reloads. (#84816) Thanks @sjf.
|
||||
- Release/security: ship the root npm package and OpenClaw-owned npm plugins with generated shrinkwrap and require review for lockfile/shrinkwrap changes so published installs use locked dependency graphs.
|
||||
- Tests/perf: isolate doctor core health check unit coverage from real skills/workspace discovery so `doctor-core-checks` no longer dominates unit perf while keeping one real skills-readiness smoke. (#84493) Thanks @frankekn.
|
||||
|
||||
### Fixes
|
||||
|
||||
@@ -53,6 +53,48 @@ OpenClaw is both a product and an experiment: you're wiring frontier-model behav
|
||||
|
||||
Start with the smallest access that still works, then widen it as you gain confidence.
|
||||
|
||||
### Published package dependency lock
|
||||
|
||||
OpenClaw source checkouts use `pnpm-lock.yaml`. The published `openclaw` npm
|
||||
package also includes `npm-shrinkwrap.json`, npm's publishable dependency
|
||||
lockfile, so package installs use the reviewed transitive dependency graph
|
||||
from the release instead of resolving a fresh graph at install time.
|
||||
|
||||
This is a supply-chain hardening measure:
|
||||
|
||||
- release installs are more reproducible;
|
||||
- transitive dependency updates become visible review surfaces;
|
||||
- the package tarball contains the dependency graph that release validators
|
||||
checked;
|
||||
- `package-lock.json` stays out of the published package, because npm does not
|
||||
treat it as the publishable lock contract.
|
||||
|
||||
Shrinkwrap is not a sandbox and does not make every dependency trustworthy. It
|
||||
does not replace `openclaw security audit`, host isolation, npm provenance,
|
||||
signature/audit checks, or `--ignore-scripts` install smoke tests when those are
|
||||
appropriate. Treat it as a release reproducibility and review-control boundary.
|
||||
|
||||
Maintainers should update and verify the shrinkwrap whenever the root package's
|
||||
published dependency graph changes:
|
||||
|
||||
```bash
|
||||
pnpm deps:shrinkwrap:generate
|
||||
pnpm deps:shrinkwrap:check
|
||||
```
|
||||
|
||||
Review `pnpm-lock.yaml`, `npm-shrinkwrap.json`, and any `package-lock.json`
|
||||
diff as security-sensitive. The package validators require shrinkwrap in new
|
||||
tarballs and reject `package-lock.json`.
|
||||
|
||||
To inspect a published package:
|
||||
|
||||
```bash
|
||||
npm pack openclaw@<version> --json --pack-destination /tmp/openclaw-pack
|
||||
tar -tf /tmp/openclaw-pack/openclaw-<version>.tgz | grep '^package/npm-shrinkwrap.json$'
|
||||
```
|
||||
|
||||
Background: [npm-shrinkwrap.json](https://docs.npmjs.com/cli/v11/configuring-npm/npm-shrinkwrap-json).
|
||||
|
||||
### Deployment and host trust
|
||||
|
||||
OpenClaw assumes the host and config boundary are trusted:
|
||||
|
||||
Generated
+5471
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@
|
||||
"files": [
|
||||
"CHANGELOG.md",
|
||||
"LICENSE",
|
||||
"npm-shrinkwrap.json",
|
||||
"openclaw.mjs",
|
||||
"pnpm-workspace.yaml",
|
||||
"README.md",
|
||||
@@ -1435,6 +1436,8 @@
|
||||
"deps:changes:report": "node scripts/dependency-changes-report.mjs",
|
||||
"deps:patches:check": "node scripts/check-package-patches.mjs",
|
||||
"deps:pins:check": "node scripts/check-dependency-pins.mjs",
|
||||
"deps:shrinkwrap:check": "node scripts/generate-npm-shrinkwrap.mjs --check",
|
||||
"deps:shrinkwrap:generate": "node scripts/generate-npm-shrinkwrap.mjs",
|
||||
"deps:ownership-surface:check": "node scripts/dependency-ownership-surface-report.mjs --check",
|
||||
"deps:ownership-surface:report": "node scripts/dependency-ownership-surface-report.mjs",
|
||||
"deps:transitive-risk:report": "node scripts/transitive-manifest-risk-report.mjs",
|
||||
|
||||
@@ -82,6 +82,7 @@ const REQUIRED_TARBALL_ENTRIES = ["dist/control-ui/index.html"];
|
||||
const REQUIRED_TARBALL_ENTRY_PREFIXES = ["dist/control-ui/assets/"];
|
||||
const LEGACY_PACKAGE_ACCEPTANCE_COMPAT_MAX = { year: 2026, month: 4, day: 25 };
|
||||
const LEGACY_LOCAL_BUILD_METADATA_COMPAT_MAX = { year: 2026, month: 4, day: 26 };
|
||||
const LEGACY_SHRINKWRAP_COMPAT_MAX = { year: 2026, month: 5, day: 20 };
|
||||
const FORBIDDEN_LOCAL_BUILD_METADATA_FILES = new Set(LOCAL_BUILD_METADATA_DIST_PATHS);
|
||||
|
||||
const LEGACY_OMITTED_PRIVATE_QA_INVENTORY_PREFIXES = [
|
||||
@@ -144,6 +145,11 @@ function isLegacyLocalBuildMetadataCompatVersion(version) {
|
||||
return parsed ? compareCalver(parsed, LEGACY_LOCAL_BUILD_METADATA_COMPAT_MAX) <= 0 : false;
|
||||
}
|
||||
|
||||
function isLegacyShrinkwrapCompatVersion(version) {
|
||||
const parsed = parseCalver(version);
|
||||
return parsed ? compareCalver(parsed, LEGACY_SHRINKWRAP_COMPAT_MAX) <= 0 : false;
|
||||
}
|
||||
|
||||
function readTarEntry(entryPath) {
|
||||
const candidates = [
|
||||
path.join(extractDir, entryPath),
|
||||
@@ -188,6 +194,52 @@ if (entrySet.has("package.json")) {
|
||||
packageVersion = "";
|
||||
}
|
||||
}
|
||||
if (entrySet.has("package-lock.json")) {
|
||||
errors.push("package tarball must ship npm-shrinkwrap.json, not package-lock.json");
|
||||
}
|
||||
if (!entrySet.has("npm-shrinkwrap.json")) {
|
||||
if (isLegacyShrinkwrapCompatVersion(packageVersion)) {
|
||||
warnings.push("legacy package omits npm-shrinkwrap.json");
|
||||
} else {
|
||||
errors.push("missing required tar entry npm-shrinkwrap.json");
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const shrinkwrap = JSON.parse(readTarEntry("npm-shrinkwrap.json"));
|
||||
const rootPackage = shrinkwrap.packages?.[""];
|
||||
if (shrinkwrap.name !== "openclaw") {
|
||||
errors.push("npm-shrinkwrap.json root name must be openclaw");
|
||||
}
|
||||
if (shrinkwrap.version !== packageVersion) {
|
||||
errors.push(
|
||||
`npm-shrinkwrap.json version ${shrinkwrap.version ?? "<missing>"} does not match package.json version ${packageVersion || "<missing>"}`,
|
||||
);
|
||||
}
|
||||
if (!rootPackage || rootPackage.name !== "openclaw") {
|
||||
errors.push("npm-shrinkwrap.json packages root must name openclaw");
|
||||
}
|
||||
if (rootPackage?.version !== packageVersion) {
|
||||
errors.push(
|
||||
`npm-shrinkwrap.json packages root version ${rootPackage?.version ?? "<missing>"} does not match package.json version ${packageVersion || "<missing>"}`,
|
||||
);
|
||||
}
|
||||
if (rootPackage?.devDependencies) {
|
||||
errors.push("npm-shrinkwrap.json must not lock root devDependencies");
|
||||
}
|
||||
const devLockedPackages = Object.entries(shrinkwrap.packages ?? {})
|
||||
.filter(([, packageMetadata]) => packageMetadata?.dev === true)
|
||||
.map(([packagePath]) => packagePath);
|
||||
if (devLockedPackages.length > 0) {
|
||||
errors.push(
|
||||
`npm-shrinkwrap.json must not lock dev packages: ${devLockedPackages.slice(0, 5).join(", ")}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`unreadable npm-shrinkwrap.json: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const forbiddenEntry of FORBIDDEN_LOCAL_BUILD_METADATA_FILES) {
|
||||
if (entrySet.has(forbiddenEntry)) {
|
||||
if (isLegacyLocalBuildMetadataCompatVersion(packageVersion)) {
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
|
||||
const DEPENDENCY_FILE_PATTERNS = [
|
||||
/^package\.json$/u,
|
||||
/^package-lock\.json$/u,
|
||||
/^npm-shrinkwrap\.json$/u,
|
||||
/^pnpm-lock\.yaml$/u,
|
||||
/^pnpm-workspace\.yaml$/u,
|
||||
/^patches\//u,
|
||||
@@ -108,8 +110,8 @@ function renderMarkdownReport(report) {
|
||||
"",
|
||||
"It reports two related but different things:",
|
||||
"",
|
||||
"- Dependency file changes: package manifests, pnpm workspace config, lockfile, and patches.",
|
||||
"- Resolved package changes: package versions added, removed, or changed in pnpm-lock.yaml.",
|
||||
"- Dependency file changes: package manifests, npm shrinkwrap/package-lock, pnpm workspace config, lockfile, and patches.",
|
||||
"- Resolved package changes: package versions added, removed, or changed in pnpm-lock.yaml; inspect shrinkwrap/package-lock diffs directly.",
|
||||
"",
|
||||
"## Summary",
|
||||
"",
|
||||
@@ -168,7 +170,7 @@ function readGitFile(ref, filePath, cwd) {
|
||||
});
|
||||
}
|
||||
|
||||
function isDependencyFile(filePath) {
|
||||
export function isDependencyFile(filePath) {
|
||||
return DEPENDENCY_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
|
||||
}
|
||||
|
||||
@@ -181,6 +183,8 @@ function gitDiffDependencyFiles(baseRef, cwd) {
|
||||
baseRef,
|
||||
"--",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"npm-shrinkwrap.json",
|
||||
"pnpm-lock.yaml",
|
||||
"pnpm-workspace.yaml",
|
||||
"*package.json",
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env node
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parse as parseYaml } from "yaml";
|
||||
|
||||
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const SHRINKWRAP_PATH = path.join(ROOT_DIR, "npm-shrinkwrap.json");
|
||||
|
||||
function usage() {
|
||||
return "Usage: node scripts/generate-npm-shrinkwrap.mjs [--check]";
|
||||
}
|
||||
|
||||
function npmCommand() {
|
||||
return process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
}
|
||||
|
||||
function normalizeOverrideValue(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => normalizeOverrideValue(item));
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, nestedValue]) => [key, normalizeOverrideValue(nestedValue)]),
|
||||
);
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function normalizeOverrides(overrides) {
|
||||
if (!overrides || typeof overrides !== "object" || Array.isArray(overrides)) {
|
||||
return {};
|
||||
}
|
||||
return normalizeOverrideValue(overrides);
|
||||
}
|
||||
|
||||
function readWorkspaceOverrides() {
|
||||
const workspace = parseYaml(readFileSync(path.join(ROOT_DIR, "pnpm-workspace.yaml"), "utf8"));
|
||||
return normalizeOverrides(workspace?.overrides);
|
||||
}
|
||||
|
||||
function mergeOverrides(packageOverrides, workspaceOverrides) {
|
||||
const merged = normalizeOverrides(packageOverrides);
|
||||
for (const [name, spec] of Object.entries(workspaceOverrides)) {
|
||||
const current = merged[name];
|
||||
if (current !== undefined && JSON.stringify(current) !== JSON.stringify(spec)) {
|
||||
throw new Error(
|
||||
`package.json overrides.${name} conflicts with pnpm-workspace.yaml overrides.${name}`,
|
||||
);
|
||||
}
|
||||
merged[name] = spec;
|
||||
}
|
||||
return Object.keys(merged).length > 0 ? merged : undefined;
|
||||
}
|
||||
|
||||
function packageJsonForShrinkwrap(packageJson) {
|
||||
const normalized = { ...packageJson };
|
||||
delete normalized.devDependencies;
|
||||
normalized.overrides = mergeOverrides(packageJson.overrides, readWorkspaceOverrides());
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function runNpm(args, cwd) {
|
||||
execFileSync(npmCommand(), args, {
|
||||
cwd,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
}
|
||||
|
||||
function generateShrinkwrap() {
|
||||
const tempDir = mkdtempSync(path.join(tmpdir(), "openclaw-shrinkwrap-"));
|
||||
try {
|
||||
const packageJson = JSON.parse(readFileSync(path.join(ROOT_DIR, "package.json"), "utf8"));
|
||||
writeFileSync(
|
||||
path.join(tempDir, "package.json"),
|
||||
`${JSON.stringify(packageJsonForShrinkwrap(packageJson), null, 2)}\n`,
|
||||
);
|
||||
runNpm(
|
||||
["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund"],
|
||||
tempDir,
|
||||
);
|
||||
runNpm(["shrinkwrap", "--ignore-scripts", "--no-audit", "--no-fund"], tempDir);
|
||||
return readFileSync(path.join(tempDir, "npm-shrinkwrap.json"), "utf8");
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const check = args.includes("--check");
|
||||
if (args.some((arg) => arg !== "--check")) {
|
||||
throw new Error(usage());
|
||||
}
|
||||
|
||||
const generated = generateShrinkwrap();
|
||||
if (!check) {
|
||||
writeFileSync(SHRINKWRAP_PATH, generated);
|
||||
process.stdout.write("npm-shrinkwrap.json updated.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
let current = "";
|
||||
try {
|
||||
current = readFileSync(SHRINKWRAP_PATH, "utf8");
|
||||
} catch {
|
||||
throw new Error("npm-shrinkwrap.json is missing. Run `pnpm deps:shrinkwrap:generate`.");
|
||||
}
|
||||
if (current !== generated) {
|
||||
throw new Error("npm-shrinkwrap.json is stale. Run `pnpm deps:shrinkwrap:generate`.");
|
||||
}
|
||||
process.stdout.write("npm-shrinkwrap.json is current.\n");
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -66,6 +66,7 @@ const OPTIONAL_LOCAL_EMBEDDING_RUNTIME_PACKAGE = "node-llama-cpp";
|
||||
const FS_SAFE_PACKAGE = "@openclaw/fs-safe";
|
||||
const MAX_CALVER_DISTANCE_DAYS = 2;
|
||||
const REQUIRED_PACKED_PATHS = [
|
||||
"npm-shrinkwrap.json",
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
"dist/control-ui/index.html",
|
||||
...WORKSPACE_TEMPLATE_PACK_PATHS,
|
||||
@@ -593,6 +594,19 @@ function collectPackedTarballErrors(): string[] {
|
||||
];
|
||||
}
|
||||
|
||||
function collectNpmShrinkwrapErrors(): string[] {
|
||||
try {
|
||||
execFileSync(process.execPath, ["scripts/generate-npm-shrinkwrap.mjs", "--check"], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
return [];
|
||||
} catch (error) {
|
||||
return [`npm-shrinkwrap.json must match package dependencies: ${describeExecFailure(error)}`];
|
||||
}
|
||||
}
|
||||
|
||||
export function collectForbiddenPackedPathErrors(paths: Iterable<string>): string[] {
|
||||
const errors: string[] = [];
|
||||
for (const packedPath of paths) {
|
||||
@@ -667,8 +681,9 @@ async function main(): Promise<number> {
|
||||
if (!skipPackValidation) {
|
||||
await writePackageDistInventory(process.cwd());
|
||||
}
|
||||
const shrinkwrapErrors = skipPackValidation ? [] : collectNpmShrinkwrapErrors();
|
||||
const tarballErrors = skipPackValidation ? [] : collectPackedTarballErrors();
|
||||
const errors = [...metadataErrors, ...tagErrors, ...tarballErrors];
|
||||
const errors = [...metadataErrors, ...tagErrors, ...shrinkwrapErrors, ...tarballErrors];
|
||||
|
||||
if (errors.length > 0) {
|
||||
for (const error of errors) {
|
||||
|
||||
@@ -54,6 +54,7 @@ type PackResult = { files?: PackFile[]; filename?: string; unpackedSize?: number
|
||||
|
||||
const rootPackageExcludedExtensionDirs = collectRootPackageExcludedExtensionDirs();
|
||||
const requiredPathGroups = [
|
||||
"npm-shrinkwrap.json",
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
["dist/index.js", "dist/index.mjs"],
|
||||
["dist/entry.js", "dist/entry.mjs"],
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from "../src/infra/package-dist-inventory.ts";
|
||||
|
||||
const REQUIRED_PACKED_PATHS = [
|
||||
"npm-shrinkwrap.json",
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
...WORKSPACE_TEMPLATE_PACK_PATHS,
|
||||
] as const;
|
||||
|
||||
@@ -14,6 +14,12 @@ type RootPackageJson = {
|
||||
};
|
||||
|
||||
type WorkspaceConfig = PnpmBuildConfig;
|
||||
type WorkspaceDependencyPolicy = WorkspaceConfig & {
|
||||
overrides?: Record<string, string | number>;
|
||||
};
|
||||
type NpmShrinkwrap = {
|
||||
packages?: Record<string, { version?: string }>;
|
||||
};
|
||||
|
||||
function readJson(filePath: string): unknown {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown;
|
||||
@@ -29,4 +35,22 @@ describe("package manager build policy", () => {
|
||||
expect(workspace.blockExoticSubdeps).toBe(true);
|
||||
expect(workspace.onlyBuiltDependencies).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps npm shrinkwrap aligned with workspace overrides", () => {
|
||||
const workspace = parse(
|
||||
fs.readFileSync("pnpm-workspace.yaml", "utf8"),
|
||||
) as WorkspaceDependencyPolicy;
|
||||
const shrinkwrap = readJson("npm-shrinkwrap.json") as NpmShrinkwrap;
|
||||
|
||||
for (const packageName of [
|
||||
"@anthropic-ai/sdk",
|
||||
"hono",
|
||||
"@aws-sdk/client-bedrock-runtime",
|
||||
"protobufjs",
|
||||
]) {
|
||||
expect(shrinkwrap.packages?.[`node_modules/${packageName}`]?.version).toBe(
|
||||
String(workspace.overrides?.[packageName]),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,13 +12,29 @@ function withTarball(
|
||||
files: Record<string, string>,
|
||||
testBody: (tarball: string) => void,
|
||||
version = "0.0.0",
|
||||
options: { includeControlUi?: boolean } = {},
|
||||
options: { includeControlUi?: boolean; includeShrinkwrap?: boolean } = {},
|
||||
) {
|
||||
const root = mkdtempSync(join(tmpdir(), "openclaw-package-tarball-test-"));
|
||||
try {
|
||||
const packageRoot = join(root, "package");
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
writeFileSync(join(packageRoot, "package.json"), JSON.stringify({ name: "openclaw", version }));
|
||||
if (options.includeShrinkwrap !== false) {
|
||||
writeFileSync(
|
||||
join(packageRoot, "npm-shrinkwrap.json"),
|
||||
JSON.stringify({
|
||||
name: "openclaw",
|
||||
version,
|
||||
lockfileVersion: 3,
|
||||
packages: {
|
||||
"": {
|
||||
name: "openclaw",
|
||||
version,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "postinstall-inventory.json"),
|
||||
JSON.stringify(inventory),
|
||||
@@ -164,6 +180,52 @@ describe("check-openclaw-package-tarball", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("allows legacy package tarballs without shrinkwrap", () => {
|
||||
withTarball(
|
||||
["dist/index.js"],
|
||||
{ "dist/index.js": "export {};\n" },
|
||||
(tarball) => {
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
expect(result.stderr).toContain("legacy package omits npm-shrinkwrap.json");
|
||||
},
|
||||
"2026.5.20",
|
||||
{ includeShrinkwrap: false },
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects new package tarballs without shrinkwrap", () => {
|
||||
withTarball(
|
||||
["dist/index.js"],
|
||||
{ "dist/index.js": "export {};\n" },
|
||||
(tarball) => {
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
|
||||
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toContain("missing required tar entry npm-shrinkwrap.json");
|
||||
},
|
||||
"2026.5.21",
|
||||
{ includeShrinkwrap: false },
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects package-lock.json in package tarballs", () => {
|
||||
withTarball(
|
||||
["dist/index.js"],
|
||||
{ "dist/index.js": "export {};\n", "package-lock.json": "{}\n" },
|
||||
(tarball) => {
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
|
||||
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toContain(
|
||||
"package tarball must ship npm-shrinkwrap.json, not package-lock.json",
|
||||
);
|
||||
},
|
||||
"2026.4.27",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects local build metadata entries in package tarballs", () => {
|
||||
withTarball(
|
||||
["dist/index.js", ...LOCAL_BUILD_METADATA_DIST_PATHS],
|
||||
|
||||
@@ -87,6 +87,8 @@ describe("dependency change awareness workflow", () => {
|
||||
it("detects the intended dependency-related file surfaces", () => {
|
||||
const script = readWorkflow().jobs?.["dependency-change-awareness"]?.steps?.[0].with?.script;
|
||||
expect(script).toContain('filename === "package.json"');
|
||||
expect(script).toContain('filename === "package-lock.json"');
|
||||
expect(script).toContain('filename === "npm-shrinkwrap.json"');
|
||||
expect(script).toContain('filename === "pnpm-lock.yaml"');
|
||||
expect(script).toContain('filename === "pnpm-workspace.yaml"');
|
||||
expect(script).toContain('filename === "ui/package.json"');
|
||||
@@ -103,5 +105,7 @@ describe("dependency change awareness workflow", () => {
|
||||
expect(codeowners).toContain(
|
||||
"/test/scripts/dependency-change-awareness-workflow.test.ts @openclaw/openclaw-secops",
|
||||
);
|
||||
expect(codeowners).toContain("/package-lock.json @openclaw/openclaw-secops");
|
||||
expect(codeowners).toContain("/npm-shrinkwrap.json @openclaw/openclaw-secops");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createDependencyChangesReport } from "../../scripts/dependency-changes-report.mjs";
|
||||
import {
|
||||
createDependencyChangesReport,
|
||||
isDependencyFile,
|
||||
} from "../../scripts/dependency-changes-report.mjs";
|
||||
|
||||
describe("dependency-changes-report", () => {
|
||||
it("reports added, removed, and changed packages", () => {
|
||||
@@ -39,4 +42,11 @@ describe("dependency-changes-report", () => {
|
||||
{ packageName: "changed", addedVersions: ["2.0.0"], removedVersions: ["1.0.0"] },
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats shrinkwrap and package-lock as dependency files", () => {
|
||||
expect(isDependencyFile("npm-shrinkwrap.json")).toBe(true);
|
||||
expect(isDependencyFile("package-lock.json")).toBe(true);
|
||||
expect(isDependencyFile("pnpm-lock.yaml")).toBe(true);
|
||||
expect(isDependencyFile("docs/gateway/security/index.md")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user