chore: harden npm shrinkwrap release path

This commit is contained in:
Peter Steinberger
2026-05-21 13:38:54 +01:00
parent 56308a7144
commit c56067e34f
18 changed files with 5832 additions and 9 deletions
+4
View File
@@ -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");
+2 -2
View File
@@ -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)) {
+1
View File
@@ -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`.
+1
View File
@@ -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
+42
View File
@@ -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:
+5471
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -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)) {
+7 -3
View File
@@ -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",
+125
View File
@@ -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;
}
+16 -1
View File
@@ -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) {
+1
View File
@@ -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"],
+1
View File
@@ -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;
+24
View File
@@ -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");
});
});
+11 -1
View File
@@ -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);
});
});