Add dependency release safety evidence and PR awareness (#81325)

* test: cover dependency pin guard

* build: add dependency vulnerability gate

* build: add dependency risk report

* build: add dependency drift reports

* build: include dependency ownership surface evidence

* build: rename dependency report commands

* build: respect release age exclusions in risk report

* build: clarify transitive risk accounting

* build: remove transitive risk exception registry

* build: clarify transitive risk signal wording

* ci: attach dependency evidence to release preflight

* ci: extract dependency release evidence generator

* build: rename ownership surface dependency report

* ci: clarify release evidence naming

* build: clarify recently published risk report

* build: reorder transitive risk report sections

* build: fix ownership surface pluralization

* ci: surface dependency changes on PRs

* ci: harden dependency change awareness

* ci: use dependency changed PR label

* build: fix dependency report lint

* docs: add dependency safety changelog
This commit is contained in:
Josh Avant
2026-05-13 03:05:09 -05:00
committed by GitHub
parent b9b7ffc8cd
commit bd4db5ee62
21 changed files with 3096 additions and 60 deletions

View File

@@ -0,0 +1,179 @@
import { execFileSync } from "node:child_process";
import { mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { collectDependencyPinViolations } from "../../scripts/check-dependency-pins.mjs";
import { cleanupTempDirs, makeTempRepoRoot } from "../helpers/temp-repo.js";
const tempDirs: string[] = [];
const nestedGitEnvKeys = [
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
"GIT_DIR",
"GIT_INDEX_FILE",
"GIT_OBJECT_DIRECTORY",
"GIT_QUARANTINE_PATH",
"GIT_WORK_TREE",
] as const;
function createNestedGitEnv(): NodeJS.ProcessEnv {
const env = {
...process.env,
GIT_CONFIG_NOSYSTEM: "1",
GIT_TERMINAL_PROMPT: "0",
};
for (const key of nestedGitEnvKeys) {
delete env[key];
}
return env;
}
function git(cwd: string, args: string[]) {
execFileSync("git", args, {
cwd,
encoding: "utf8",
env: createNestedGitEnv(),
});
}
function writeJson(filePath: string, value: unknown) {
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
function makeRepo() {
const dir = makeTempRepoRoot(tempDirs, "openclaw-dependency-pins-");
git(dir, ["init", "-q", "--initial-branch=main"]);
return dir;
}
afterEach(() => {
cleanupTempDirs(tempDirs);
});
describe("check-dependency-pins", () => {
it("accepts exact dependency specs and intentionally ranged peer contracts", () => {
const dir = makeRepo();
writeJson(path.join(dir, "package.json"), {
dependencies: {
exact: "1.2.3",
prerelease: "1.2.3-beta.1",
alias: "npm:@scope/real-package@2.3.4",
workspace: "workspace:*",
linked: "link:../linked",
local: "file:../local",
gitPinned: "github:owner/repo#0123456789abcdef0123456789abcdef01234567",
},
devDependencies: {
devExact: "4.5.6",
},
optionalDependencies: {
optionalExact: "7.8.9",
},
peerDependencies: {
peerCanRange: "^1.0.0",
},
});
writeFileSync(
path.join(dir, "pnpm-workspace.yaml"),
`overrides:
exact: 1.2.3
alias: "npm:@scope/real-package@2.3.4"
packageExtensions:
parent@1.0.0:
dependencies:
child: 3.2.1
`,
"utf8",
);
git(dir, ["add", "package.json", "pnpm-workspace.yaml"]);
expect(collectDependencyPinViolations(dir)).toEqual([]);
});
it("rejects floating dependency specs in tracked package manifests", () => {
const dir = makeRepo();
mkdirSync(path.join(dir, "extensions", "demo"), { recursive: true });
writeJson(path.join(dir, "package.json"), {
dependencies: {
caret: "^1.2.3",
tilde: "~1.2.3",
wildcard: "*",
tag: "latest",
broad: ">=1 <2",
gitFloating: "github:owner/repo#main",
},
});
writeJson(path.join(dir, "extensions", "demo", "package.json"), {
devDependencies: {
devCaret: "^4.5.6",
},
optionalDependencies: {
optionalTilde: "~7.8.9",
},
peerDependencies: {
peerCanRange: "^10.0.0",
},
});
git(dir, ["add", "package.json", "extensions/demo/package.json"]);
expect(collectDependencyPinViolations(dir)).toEqual([
{
file: "extensions/demo/package.json",
section: "devDependencies",
name: "devCaret",
spec: "^4.5.6",
},
{
file: "extensions/demo/package.json",
section: "optionalDependencies",
name: "optionalTilde",
spec: "~7.8.9",
},
{ file: "package.json", section: "dependencies", name: "caret", spec: "^1.2.3" },
{ file: "package.json", section: "dependencies", name: "tilde", spec: "~1.2.3" },
{ file: "package.json", section: "dependencies", name: "wildcard", spec: "*" },
{ file: "package.json", section: "dependencies", name: "tag", spec: "latest" },
{ file: "package.json", section: "dependencies", name: "broad", spec: ">=1 <2" },
{
file: "package.json",
section: "dependencies",
name: "gitFloating",
spec: "github:owner/repo#main",
},
]);
});
it("rejects floating workspace overrides and package extension dependencies", () => {
const dir = makeRepo();
writeJson(path.join(dir, "package.json"), {});
writeFileSync(
path.join(dir, "pnpm-workspace.yaml"),
`overrides:
exact: 1.2.3
floating: ^2.0.0
packageExtensions:
parent@1.0.0:
dependencies:
exact-child: 3.2.1
floating-child: ~4.0.0
`,
"utf8",
);
git(dir, ["add", "package.json", "pnpm-workspace.yaml"]);
expect(collectDependencyPinViolations(dir)).toEqual([
{
file: "pnpm-workspace.yaml",
section: "overrides",
name: "floating",
spec: "^2.0.0",
},
{
file: "pnpm-workspace.yaml",
section: "packageExtensions.parent@1.0.0.dependencies",
name: "floating-child",
spec: "~4.0.0",
},
]);
});
});

View File

@@ -0,0 +1,105 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
const WORKFLOW = ".github/workflows/dependency-change-awareness.yml";
const CODEOWNERS = ".github/CODEOWNERS";
type WorkflowStep = {
name?: string;
run?: string;
uses?: string;
with?: Record<string, string>;
};
type WorkflowJob = {
steps?: WorkflowStep[];
};
type Workflow = {
jobs?: Record<string, WorkflowJob>;
permissions?: Record<string, string>;
};
function readWorkflow(): Workflow {
return parse(readFileSync(WORKFLOW, "utf8")) as Workflow;
}
describe("dependency change awareness workflow", () => {
it("uses a metadata-only pull_request_target workflow with minimal write permissions", () => {
const workflow = readFileSync(WORKFLOW, "utf8");
const parsed = readWorkflow();
expect(workflow).toContain("pull_request_target:");
expect(workflow).toContain("metadata-only workflow; no checkout or untrusted code execution");
expect(parsed.permissions).toEqual({
"pull-requests": "read",
issues: "write",
});
});
it("does not checkout or execute PR-controlled code", () => {
const workflow = readFileSync(WORKFLOW, "utf8");
const forbiddenSnippets = [
"actions/checkout",
"github.event.pull_request.head",
"pullRequest.head",
"pnpm install",
"npm install",
"pnpm dlx",
"contents: write",
"actions: write",
"id-token: write",
"secrets.",
"github.rest.issues.createLabel",
];
for (const snippet of forbiddenSnippets) {
expect(workflow).not.toContain(snippet);
}
const steps = readWorkflow().jobs?.["dependency-change-awareness"]?.steps ?? [];
expect(steps).toHaveLength(1);
expect(steps[0].run).toBeUndefined();
});
it("uses a pinned GitHub Script action and bounded sticky comments", () => {
const workflow = readFileSync(WORKFLOW, "utf8");
const steps = readWorkflow().jobs?.["dependency-change-awareness"]?.steps ?? [];
const step = steps[0];
expect(step.uses).toBe("actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3");
expect(step.with?.script).toContain("<!-- openclaw:dependency-change-awareness -->");
expect(step.with?.script).toContain("const maxListedFiles = 25;");
expect(step.with?.script).toContain("const sanitizeDisplayValue = (value)");
expect(step.with?.script).toContain('.replace(/[\\u0000-\\u001f\\u007f]/gu, "?")');
expect(step.with?.script).toContain(".slice(0, 240)");
expect(step.with?.script).toContain('comment.user?.login === "github-actions[bot]"');
expect(step.with?.script).toContain("github.rest.pulls.listFiles");
expect(step.with?.script).toContain("github.rest.issues.createComment");
expect(step.with?.script).toContain("github.rest.issues.updateComment");
expect(step.with?.script).toContain("github.rest.issues.deleteComment");
expect(workflow).toContain('"dependencies-changed"');
});
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 === "pnpm-lock.yaml"');
expect(script).toContain('filename === "pnpm-workspace.yaml"');
expect(script).toContain('filename === "ui/package.json"');
expect(script).toContain('filename.startsWith("patches/")');
expect(script).toContain("^packages\\/[^/]+\\/package\\.json$");
expect(script).toContain("^extensions\\/[^/]+\\/package\\.json$");
});
it("requires secops review for future workflow or guard changes", () => {
const codeowners = readFileSync(CODEOWNERS, "utf8");
expect(codeowners).toContain(
"/.github/workflows/dependency-change-awareness.yml @openclaw/openclaw-secops",
);
expect(codeowners).toContain(
"/test/scripts/dependency-change-awareness-workflow.test.ts @openclaw/openclaw-secops",
);
});
});

View File

@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import { createDependencyChangesReport } from "../../scripts/dependency-changes-report.mjs";
describe("dependency-changes-report", () => {
it("reports added, removed, and changed packages", () => {
const report = createDependencyChangesReport({
basePayload: {
removed: ["1.0.0"],
stable: ["1.0.0"],
changed: ["1.0.0"],
},
headPayload: {
added: ["1.0.0"],
stable: ["1.0.0"],
changed: ["2.0.0"],
},
dependencyFileChanges: [
{ status: "M", path: "pnpm-lock.yaml", oldPath: null },
{ status: "M", path: "pnpm-workspace.yaml", oldPath: null },
],
generatedAt: "2026-05-12T00:00:00Z",
});
expect(report.summary).toEqual({
basePackages: 3,
headPackages: 3,
addedPackages: 1,
removedPackages: 1,
changedPackages: 1,
dependencyFileChanges: 2,
});
expect(report.dependencyFileChanges).toEqual([
{ status: "M", path: "pnpm-lock.yaml", oldPath: null },
{ status: "M", path: "pnpm-workspace.yaml", oldPath: null },
]);
expect(report.addedPackages).toEqual([{ packageName: "added", versions: ["1.0.0"] }]);
expect(report.removedPackages).toEqual([{ packageName: "removed", versions: ["1.0.0"] }]);
expect(report.changedPackages).toEqual([
{ packageName: "changed", addedVersions: ["2.0.0"], removedVersions: ["1.0.0"] },
]);
});
});

View File

@@ -3,10 +3,11 @@ import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
collectSbomRiskCheckErrors,
collectSbomRiskReport,
collectDependencyOwnershipSurfaceCheckErrors,
collectDependencyOwnershipSurfaceReport,
packageNameFromLockKey,
} from "../../scripts/sbom-risk-report.mjs";
renderDependencyOwnershipSurfaceMarkdownReport,
} from "../../scripts/dependency-ownership-surface-report.mjs";
const tempDirs: string[] = [];
@@ -17,7 +18,7 @@ afterEach(() => {
});
function makeTempRepo() {
const dir = mkdtempSync(path.join(tmpdir(), "openclaw-sbom-risk-"));
const dir = mkdtempSync(path.join(tmpdir(), "openclaw-ownership-surface-"));
tempDirs.push(dir);
return dir;
}
@@ -35,8 +36,8 @@ describe("packageNameFromLockKey", () => {
});
});
describe("collectSbomRiskReport", () => {
it("reports root closure sizes, build-risk packages, and ownership gaps", () => {
describe("collectDependencyOwnershipSurfaceReport", () => {
it("reports root dependency reachability, install-surface packages, and ownership metadata gaps", () => {
const repoRoot = makeTempRepo();
writeRepoFile(
repoRoot,
@@ -98,7 +99,7 @@ snapshots:
);
writeRepoFile(repoRoot, "src/index.ts", 'import "core-lib";\n');
const report = collectSbomRiskReport({ repoRoot });
const report = collectDependencyOwnershipSurfaceReport({ repoRoot });
expect(report.summary).toEqual({
buildRiskPackageCount: 1,
@@ -123,9 +124,28 @@ snapshots:
sourceSections: [],
specifier: "1.0.0",
});
expect(collectSbomRiskCheckErrors(report)).toEqual([
expect(collectDependencyOwnershipSurfaceCheckErrors(report)).toEqual([
"root dependency 'missing-owner' is missing from scripts/lib/dependency-ownership.json",
]);
const markdown = renderDependencyOwnershipSurfaceMarkdownReport(report);
expect(markdown).toContain("# Dependency Ownership and Install Surface Report");
expect(markdown).toContain("## Target");
expect(markdown).toContain("## Scope");
expect(markdown).toContain("It does not query npm advisories");
expect(markdown).toContain("## Root Dependencies Missing Ownership Metadata");
expect(markdown).toContain("`missing-owner`");
expect(markdown).toContain("## Root Dependencies By Resolved Transitive Package Count");
expect(markdown).toContain("`core-lib`: 3 resolved transitive packages");
expect(markdown).toContain("## Workspace Packages With The Most Dependencies");
expect(markdown).toContain("3 direct dependencies");
expect(markdown).not.toContain("dependencys");
expect(markdown).toContain("## Packages With Install-Time Or Platform-Specific Behavior");
expect(markdown).toContain("`transitive-native@1.0.0`: requires build");
expect(markdown).not.toContain("# Dependency Risk Report");
expect(markdown).not.toContain("Ownership gaps");
expect(markdown).not.toContain("Largest root dependency cones");
expect(markdown).not.toContain("## Root Dependencies With The Most Transitive Packages");
});
it("does not mark plugin importer dependencies as stale ownership records", () => {
@@ -180,7 +200,7 @@ snapshots:
}),
);
const report = collectSbomRiskReport({ repoRoot });
const report = collectDependencyOwnershipSurfaceReport({ repoRoot });
expect(report.ownershipGaps).toStrictEqual([]);
expect(report.staleOwnershipRecords).toEqual(["removed-lib"]);

View File

@@ -0,0 +1,171 @@
import { mkdtemp, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
classifyVulnerabilityFindings,
renderDependencyVulnerabilityGateMarkdownReport,
runDependencyVulnerabilityGate,
} from "../../scripts/dependency-vulnerability-gate.mjs";
function advisory({
id,
severity,
title,
vulnerableVersions = "<=1.0.0",
}: {
id: string;
severity: string;
title: string;
vulnerableVersions?: string;
}) {
return {
id,
severity,
title,
vulnerable_versions: vulnerableVersions,
url: `https://github.com/advisories/${id}`,
};
}
async function writeLockfile(rootDir: string) {
await writeFile(
path.join(rootDir, "pnpm-lock.yaml"),
`lockfileVersion: '9.0'
importers:
.:
dependencies:
runtime-high:
version: 1.0.0
devDependencies:
dev-high:
version: 1.0.0
snapshots:
runtime-high@1.0.0: {}
dev-high@1.0.0: {}
transitive-critical@1.0.0: {}
`,
"utf8",
);
}
describe("dependency-vulnerability-gate", () => {
it("blocks critical advisories anywhere and high advisories in the production graph", () => {
const result = classifyVulnerabilityFindings({
allAdvisories: {
"dev-high": [advisory({ id: "GHSA-dev-high", severity: "high", title: "dev high" })],
"transitive-critical": [
advisory({ id: "GHSA-critical", severity: "critical", title: "critical issue" }),
],
},
productionAdvisories: {
"runtime-high": [
advisory({ id: "GHSA-runtime-high", severity: "high", title: "runtime high" }),
],
},
});
expect(result.blockers.map((finding) => finding.id)).toEqual([
"GHSA-critical",
"GHSA-runtime-high",
]);
expect(result.findings.map((finding) => finding.id)).toEqual([
"GHSA-critical",
"GHSA-dev-high",
"GHSA-runtime-high",
]);
});
it("blocks malware advisories regardless of severity or graph", () => {
const result = classifyVulnerabilityFindings({
allAdvisories: {
dev: [advisory({ id: "GHSA-malware", severity: "low", title: "Malware in dev" })],
},
productionAdvisories: {},
});
expect(result.blockers).toMatchObject([
{
id: "GHSA-malware",
malware: true,
severity: "low",
},
]);
});
it("queries full and production lockfile graphs separately", async () => {
const rootDir = await mkdtemp(path.join(tmpdir(), "openclaw-vuln-gate-"));
await writeLockfile(rootDir);
const payloads: Record<string, string[]>[] = [];
const report = await runDependencyVulnerabilityGate({
rootDir,
fetchImpl: async (_url, init) => {
const payload = JSON.parse(String(init?.body));
payloads.push(payload);
const packages = Object.keys(payload);
const body: Record<string, unknown[]> = {};
if (packages.includes("runtime-high")) {
body["runtime-high"] = [
advisory({ id: "GHSA-runtime-high", severity: "high", title: "runtime high" }),
];
}
if (packages.includes("dev-high")) {
body["dev-high"] = [
advisory({ id: "GHSA-dev-high", severity: "high", title: "dev high" }),
];
}
return new Response(JSON.stringify(body), {
status: 200,
headers: { "content-type": "application/json" },
});
},
});
expect(payloads).toHaveLength(2);
expect(payloads[0]).toEqual({
"dev-high": ["1.0.0"],
"runtime-high": ["1.0.0"],
"transitive-critical": ["1.0.0"],
});
expect(payloads[1]).toEqual({
"runtime-high": ["1.0.0"],
});
expect(report.blockers.map((finding) => finding.id)).toEqual(["GHSA-runtime-high"]);
expect(report.findings.map((finding) => finding.id)).toEqual([
"GHSA-dev-high",
"GHSA-runtime-high",
]);
});
it("documents the resolved transitive dependency graph scope in Markdown", () => {
const markdown = renderDependencyVulnerabilityGateMarkdownReport({
generatedAt: "2026-05-12T00:00:00.000Z",
policy: {
blocks: [
"known malware advisories anywhere in the installed graph",
"critical advisories anywhere in the installed graph",
"high advisories in the production/runtime graph",
],
reports: [
"moderate and lower advisories",
"high advisories outside production/runtime graph",
],
vulnerabilityExceptions: false,
},
graphs: {
all: { packages: 2, packageVersions: 2 },
production: { packages: 1, packageVersions: 1 },
},
blockers: [],
findings: [],
});
expect(markdown).toContain("# npm Advisory Vulnerability Gate: Resolved Dependency Graph");
expect(markdown).toContain("## Scope");
expect(markdown).toContain("resolved package versions from pnpm-lock.yaml");
expect(markdown).toContain("It includes transitive dependencies.");
});
});

View File

@@ -0,0 +1,172 @@
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
DEPENDENCY_EVIDENCE_REPORTS,
collectDependencyEvidenceSummaryCounts,
createDependencyEvidenceManifest,
renderDependencyEvidenceStepSummary,
renderDependencyEvidenceSummary,
resolvePreviousReleaseTag,
resolveReleaseTag,
} from "../../scripts/generate-dependency-release-evidence.mjs";
async function writeJson(dir: string, fileName: string, value: unknown) {
await writeFile(path.join(dir, fileName), `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
describe("generate-dependency-release-evidence", () => {
it("defines the release evidence command list and policy classifications", () => {
expect(DEPENDENCY_EVIDENCE_REPORTS.map(({ command, policy }) => ({ command, policy }))).toEqual(
[
{ command: "pnpm deps:vuln:gate", policy: "hard-blocking" },
{ command: "pnpm deps:transitive-risk:report", policy: "report-only" },
{ command: "pnpm deps:ownership-surface:report", policy: "report-only" },
{ command: "pnpm deps:changes:report", policy: "report-only" },
],
);
});
it("creates the dependency evidence manifest shape", () => {
const manifest = createDependencyEvidenceManifest({
generatedAt: "2026-05-13T00:00:00.000Z",
releaseTag: "v2026.5.13-beta.1",
releaseRef: "v2026.5.13-beta.1",
releaseSha: "abc123",
npmDistTag: "beta",
packageVersion: "2026.5.13-beta.1",
workflowRunId: "123",
workflowRunAttempt: "2",
dependencyChangeBaseRef: "v2026.5.1",
});
expect(manifest).toEqual({
schemaVersion: 1,
generatedAt: "2026-05-13T00:00:00.000Z",
releaseTag: "v2026.5.13-beta.1",
releaseRef: "v2026.5.13-beta.1",
releaseSha: "abc123",
npmDistTag: "beta",
packageName: "openclaw",
packageVersion: "2026.5.13-beta.1",
workflowRunId: "123",
workflowRunAttempt: "2",
dependencyChangeBaseRef: "v2026.5.1",
reports: DEPENDENCY_EVIDENCE_REPORTS,
});
});
it("uses a synthetic release tag for validation-only SHA preflight input", () => {
expect(
resolveReleaseTag({
releaseRef: "0123456789abcdef0123456789abcdef01234567",
packageVersion: "2026.5.13",
}),
).toBe("v2026.5.13");
expect(
resolveReleaseTag({
releaseRef: "v2026.5.13-beta.1",
packageVersion: "2026.5.13-beta.1",
}),
).toBe("v2026.5.13-beta.1");
});
it("falls back to fetching tags when local previous-release resolution misses", () => {
const calls: Array<{ command: string; args: string[] }> = [];
let describeCalls = 0;
const execFileSyncImpl = (command: string, args: string[] = []) => {
calls.push({ command, args });
if (command !== "git") {
throw new Error(`unexpected command: ${command}`);
}
if (args[0] === "describe") {
describeCalls += 1;
if (describeCalls === 1) {
throw new Error("tag not found");
}
return "v2026.5.1\n";
}
if (args[0] === "fetch") {
return "";
}
throw new Error(`unexpected git args: ${args.join(" ")}`);
};
expect(
resolvePreviousReleaseTag({
rootDir: "/repo",
execFileSyncImpl,
}),
).toBe("v2026.5.1");
expect(calls.map(({ args }) => args[0])).toEqual(["describe", "fetch", "describe"]);
expect(calls[1].args).toEqual(["fetch", "--tags", "--force", "origin"]);
});
it("collects report counts and renders human summaries", async () => {
const dir = await mkdtemp(path.join(tmpdir(), "openclaw-release-dependency-evidence-test-"));
await writeJson(dir, "dependency-vulnerability-gate.json", {
blockers: [{ id: "GHSA-blocker" }],
findings: [{ id: "GHSA-blocker" }, { id: "GHSA-report" }],
});
await writeJson(dir, "transitive-manifest-risk-report.json", {
findingCount: 17,
workspaceExcludedFindingCount: 3,
metadataFailures: [{ packageName: "missing" }],
});
await writeJson(dir, "dependency-ownership-surface-report.json", {
summary: {
lockfilePackageCount: 101,
buildRiskPackageCount: 8,
},
});
await writeJson(dir, "dependency-changes-report.json", {
summary: {
dependencyFileChanges: 4,
addedPackages: 5,
removedPackages: 6,
changedPackages: 7,
},
});
const counts = await collectDependencyEvidenceSummaryCounts(dir);
expect(counts).toEqual({
vulnerabilityBlockers: 1,
vulnerabilityFindings: 2,
transitiveRiskSignals: 17,
workspaceExcludedTransitiveSignals: 3,
transitiveMetadataFailures: 1,
ownershipLockfilePackages: 101,
ownershipBuildRiskPackages: 8,
dependencyFileChanges: 4,
dependencyAddedPackages: 5,
dependencyRemovedPackages: 6,
dependencyChangedPackages: 7,
});
const summary = renderDependencyEvidenceSummary({
releaseTag: "v2026.5.13",
releaseSha: "abc123",
baseRef: "v2026.5.1",
counts,
});
expect(summary).toContain("- npm advisory vulnerability hard blockers: 1");
expect(summary).toContain("- Transitive manifest reported risk signals: 17");
expect(summary).toContain("- Dependency change baseline: `v2026.5.1`");
expect(summary).toContain("- Resolved package changes: +5 -6 changed 7");
const stepSummary = renderDependencyEvidenceStepSummary({
evidenceArtifactName: "openclaw-release-dependency-evidence-v2026.5.13",
baseRef: "v2026.5.1",
counts,
});
expect(stepSummary).toContain(
"- Evidence artifact: `openclaw-release-dependency-evidence-v2026.5.13`",
);
expect(stepSummary).toContain("- npm advisory vulnerability hard blockers: `1`");
await expect(
readFile(path.join(dir, "dependency-vulnerability-gate.json"), "utf8"),
).resolves.toContain("GHSA-blocker");
});
});

View File

@@ -0,0 +1,177 @@
import { describe, expect, it } from "vitest";
import {
createTransitiveManifestRiskReport,
renderTransitiveManifestRiskMarkdownReport,
} from "../../scripts/transitive-manifest-risk-report.mjs";
describe("transitive-manifest-risk-report", () => {
it("reports floating transitive specs, lifecycle scripts, exotic sources, and recently published versions", async () => {
const report = await createTransitiveManifestRiskReport({
packageVersions: [
{ packageName: "parent", version: "1.0.0" },
{ packageName: "tarball-package", version: "https://example.test/pkg.tgz" },
],
now: new Date("2026-05-12T00:00:00Z"),
minimumReleaseAgeMinutes: 2_880,
manifestLoader: async ({ packageName, version }) => {
if (packageName !== "parent" || version !== "1.0.0") {
throw new Error("unexpected manifest request");
}
return {
publishedAt: "2026-05-11T23:00:00Z",
manifest: {
dependencies: {
floating: "^1.2.3",
exact: "2.0.0",
gitdep: "github:owner/repo#main",
},
optionalDependencies: {
optionalFloating: "~3.0.0",
},
scripts: {
install: "node install.js",
},
},
};
},
});
expect(report.byType).toEqual({
"exotic-source": 2,
"floating-transitive-spec": 3,
"lifecycle-script": 1,
"recently-published-version": 1,
});
expect(report.workspaceExcludedFindings).toEqual([]);
expect(report.metadataFailures).toEqual([]);
});
it("uses pnpm minimum release age exclusions for recently published versions", async () => {
const report = await createTransitiveManifestRiskReport({
packageVersions: [
{ packageName: "regular", version: "1.0.0" },
{ packageName: "exact-package", version: "2.0.0" },
{ packageName: "either-version", version: "5.102.1" },
{ packageName: "@scope/native-linux-x64", version: "3.0.0" },
],
now: new Date("2026-05-12T00:00:00Z"),
minimumReleaseAgeMinutes: 2_880,
minimumReleaseAgeExclude: [
"exact-package@2.0.0",
"either-version@4.47.0 || 5.102.1",
"@scope/native-*",
],
manifestLoader: async () => ({
publishedAt: "2026-05-11T23:00:00Z",
manifest: {},
}),
});
expect(report.byType).toEqual({
"recently-published-version": 1,
});
expect(report.workspaceExcludedByType).toEqual({
"recently-published-version": 3,
});
expect(report.findings).toMatchObject([
{
packageName: "regular",
type: "recently-published-version",
},
]);
expect(report.workspaceExcludedFindings).toMatchObject([
{
packageName: "@scope/native-linux-x64",
type: "recently-published-version",
workspaceExcluded: true,
workspaceExclusion: "@scope/native-*",
},
{
packageName: "either-version",
type: "recently-published-version",
workspaceExcluded: true,
workspaceExclusion: "either-version@4.47.0 || 5.102.1",
},
{
packageName: "exact-package",
type: "recently-published-version",
workspaceExcluded: true,
workspaceExclusion: "exact-package@2.0.0",
},
]);
const markdown = renderTransitiveManifestRiskMarkdownReport(report);
expect(markdown).toContain(
"## Recently Published Versions Not Covered By Workspace Exclusions",
);
expect(markdown).toContain("## Recently Published Versions Covered By Workspace Exclusions");
expect(markdown).toContain("Workspace minimum release age: 2880 minutes.");
expect(markdown).toContain("`regular@1.0.0`: published 2026-05-11T23:00:00Z");
expect(markdown).toContain(
"`exact-package@2.0.0`: published 2026-05-11T23:00:00Z; workspace exclusion `exact-package@2.0.0`",
);
expect(markdown).not.toContain(
"`regular@1.0.0`: published 2026-05-11T23:00:00Z; minimum release age 2880 minutes",
);
});
it("documents JSON completeness and renders grouped Markdown summaries", async () => {
const report = await createTransitiveManifestRiskReport({
packageVersions: [
{ packageName: "@earendil-works/pi-ai", version: "0.74.0" },
{ packageName: "aaa-package", version: "1.0.0" },
{ packageName: "recent-package", version: "1.0.0" },
],
now: new Date("2026-05-12T00:00:00Z"),
minimumReleaseAgeMinutes: 2_880,
minimumReleaseAgeExclude: ["recent-package@1.0.0"],
manifestLoader: async ({ packageName }) => ({
publishedAt:
packageName === "recent-package" ? "2026-05-11T23:00:00Z" : "2026-04-01T00:00:00Z",
manifest:
packageName === "@earendil-works/pi-ai"
? {
dependencies: {
"@mistralai/mistralai": "^2.2.0",
},
}
: packageName === "recent-package"
? {
dependencies: {
"recent-dependency": "^1.0.0",
},
}
: {
dependencies: {
"aaa-dependency": "^1.0.0",
},
},
}),
});
const markdown = renderTransitiveManifestRiskMarkdownReport(report);
expect(markdown).toContain("# Transitive Manifest Risk Report");
expect(markdown).toContain("## Scope");
expect(markdown).toContain("published package manifests for resolved packages");
expect(markdown).toContain("It is report-only.");
expect(markdown).toContain("Resolved package versions inspected");
expect(markdown).toContain("Reported risk signals");
expect(markdown).toContain("Signals covered by workspace policy exclusions");
expect(markdown).toContain("## Reported Risk Signals By Type");
expect(markdown).toContain("## Signals Covered By Workspace Policy Exclusions");
expect(markdown).toContain("not included in the reported risk signal totals");
expect(markdown).toContain("## Complete Evidence");
expect(markdown).toContain("The complete reported signal list is available in the JSON report");
expect(markdown).toContain("## Published Package Manifests With Risk Findings");
expect(markdown).toContain("`@earendil-works/pi-ai@0.74.0`: 1 manifest finding");
expect(markdown).toContain("`aaa-package@1.0.0`: 1 manifest finding");
expect(markdown).toContain("## Floating Dependency Targets");
expect(markdown).toContain("`@mistralai/mistralai`: 1 declarations");
expect(markdown).toContain("`aaa-dependency`: 1 declarations");
expect(markdown).not.toContain("## Packages With Findings");
expect(markdown).not.toContain("## Finding Details");
expect(markdown).not.toContain("## Notable Findings");
expect(markdown).not.toContain("## Additional Sample Findings");
});
});