diff --git a/.agents/skills/openclaw-testing/SKILL.md b/.agents/skills/openclaw-testing/SKILL.md index 87f6e967217d..b43b159b2e63 100644 --- a/.agents/skills/openclaw-testing/SKILL.md +++ b/.agents/skills/openclaw-testing/SKILL.md @@ -340,11 +340,12 @@ lanes are intentionally reserved for the separate `Plugin Prerelease` child so PRs, main pushes, and ad hoc broad CI checks do not spend Docker/package time or all-plugin runtime time on release-only product coverage. -`Plugin Prerelease` may execute candidate code only in secretless packaging -jobs. The trusted scanner treats uploaded immutable tarballs as inert data and -scans the exact post-build bytes; publication must consume those verified bytes -or require an identical digest. Artifact ingestion stays fail-slow so one -malformed package records its own error without hiding other package reports. +`Plugin Prerelease` performs a supplemental scan of checked-in npm package input +as inert data; it never runs candidate lifecycle, asset, build, install, or +replacement scanner code. This scan does not approve post-build or publication +bytes. The publication workflow must independently scan its exact final artifact +and publish the same digest. Ingestion stays fail-slow so one malformed package +cannot hide other package reports. Use one operator, one transition-only watcher, and at most one investigator for the current failed surface. Parent timeout or cancellation leaves adopted exact diff --git a/.github/workflows/plugin-prerelease.yml b/.github/workflows/plugin-prerelease.yml index ae5565411174..419b9f63f71f 100644 --- a/.github/workflows/plugin-prerelease.yml +++ b/.github/workflows/plugin-prerelease.yml @@ -359,13 +359,13 @@ jobs: persist-credentials: false submodules: false - - name: Setup candidate package build environment + - name: Setup trusted inert pack environment uses: ./.github/actions/setup-node-env with: node-version: "24.x" install-bun: "false" - - name: Build publication-equivalent plugin artifact + - name: Pack supplemental inert plugin input env: CANDIDATE_SHA: ${{ needs.resolve-candidate.outputs.checkout_revision }} EXTENSION_ID: ${{ matrix.extension_id }} @@ -385,7 +385,7 @@ jobs: --package-name "$PACKAGE_NAME" \ --tooling-sha "$TOOLING_SHA" - - name: Upload publication-equivalent plugin artifact + - name: Upload supplemental inert plugin input uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: plugin-npm-security-package-${{ needs.resolve-candidate.outputs.checkout_revision }}-${{ matrix.extension_id }} @@ -426,14 +426,14 @@ jobs: - name: Install trusted scanner dependencies run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts - - name: Download publication-equivalent plugin artifacts + - name: Download supplemental inert plugin inputs continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: pattern: plugin-npm-security-package-${{ needs.resolve-candidate.outputs.checkout_revision }}-* path: ${{ runner.temp }}/plugin-npm-security-packages - - name: Scan publication-equivalent plugin artifacts + - name: Scan supplemental inert plugin inputs env: CANDIDATE_SHA: ${{ needs.resolve-candidate.outputs.checkout_revision }} EXPECTED_PACKAGES_JSON: ${{ needs.plugin-npm-security-plan.outputs.packages_json }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e4de06d27db..4990ad87c10d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Docs: https://docs.openclaw.ai ### Changes - **Secret egress host binding:** bind each shared-store secret to exact HTTPS destination hosts across CLI, Gateway RPC, and Control UI so unbound sentinel substitution fails closed before plaintext egress. -- **Plugin release security scan:** scan immutable publication-equivalent npm plugin tarballs in trusted tooling, while isolating candidate builds in secretless jobs and bounding malformed artifacts, archive bytes, findings, memory, and reports. +- **Plugin release security scan:** add a trusted supplemental scan of inert npm plugin package inputs without running candidate code, with bounded fail-slow handling for malformed artifacts, archive bytes, findings, memory, and reports. - **Release validation:** defer beta candidate Parallels smoke to postpublish `release:beta-smoke` by default, keep stable/full prepublish coverage, and bound nested release workflow monitors with explicit job timeouts. - **macOS app profiles:** isolate named app instances across state, preferences, Keychain, Gateway services, and duplicate-instance ownership while keeping host-global login and node services untouched. - **Developer workflow:** remove the obsolete scoped-commit helper and use standard Git commands in isolated worktrees. diff --git a/scripts/lib/plugin-npm-security-scan.mts b/scripts/lib/plugin-npm-security-scan.mts index 763370e087c7..eb3cf863bb84 100644 --- a/scripts/lib/plugin-npm-security-scan.mts +++ b/scripts/lib/plugin-npm-security-scan.mts @@ -48,12 +48,11 @@ export type ScanPackageResult = { }; type PluginNpmSecurityArtifact = PublishablePluginPackage & { - artifactKind: "publication-equivalent-plugin-tarball"; + artifactKind: "supplemental-inert-package-input"; artifactDir: string; candidateSha: string; compressedBytes: number; expandedBytes: number; - packOwner: "scripts/plugin-npm-publish.sh"; tarballPath: string; tarballSha256: string; toolingSha: string; @@ -64,6 +63,7 @@ export type PluginNpmSecurityScanReport = { errors: string[]; layout: string | null; packages: ScanPackageResult[]; + scanScope: "supplemental-inert-package-input"; schemaVersion: 1; status: "pass" | "fail"; summary: { @@ -421,7 +421,6 @@ function readPluginSecurityArtifact( "artifactKind", "candidateSha", "extensionId", - "packOwner", "packageDir", "packageName", "packageVersion", @@ -431,8 +430,7 @@ function readPluginSecurityArtifact( "toolingSha", ]; if ( - metadata.artifactKind !== "publication-equivalent-plugin-tarball" || - metadata.packOwner !== "scripts/plugin-npm-publish.sh" || + metadata.artifactKind !== "supplemental-inert-package-input" || metadata.schemaVersion !== 1 || JSON.stringify(Object.keys(metadata).toSorted()) !== JSON.stringify(expectedKeys) ) { @@ -509,13 +507,12 @@ function readPluginSecurityArtifact( throw new Error("Plugin security artifact tarball identity is invalid."); } return { - artifactKind: "publication-equivalent-plugin-tarball", + artifactKind: "supplemental-inert-package-input", artifactDir, candidateSha: expectedCandidateSha, compressedBytes: tarballStat.size, expandedBytes: inspection.totalFileBytes, extensionId, - packOwner: "scripts/plugin-npm-publish.sh", packageDir, packageName, packageVersion, @@ -912,6 +909,7 @@ export function buildPluginNpmSecurityScanReport(params: { errors: sortStrings(errors), layout: layout?.id ?? null, packages: sortedPackages, + scanScope: "supplemental-inert-package-input", schemaVersion: 1, status: errors.length === 0 ? "pass" : "fail", summary: { @@ -937,6 +935,7 @@ export function constrainPluginNpmSecurityScanReport( errors: ["Plugin npm security scan report exceeded the byte limit."], layout: null, packages: [], + scanScope: "supplemental-inert-package-input", schemaVersion: 1, status: "fail", summary: report.summary, diff --git a/scripts/plugin-npm-security-prepare.mts b/scripts/plugin-npm-security-prepare.mts index 9c250ca465b0..66d50e69fd74 100644 --- a/scripts/plugin-npm-security-prepare.mts +++ b/scripts/plugin-npm-security-prepare.mts @@ -7,6 +7,7 @@ import { realpathSync, writeFileSync, } from "node:fs"; +import { tmpdir } from "node:os"; import { basename, join, relative, resolve, sep } from "node:path"; import { pathToFileURL } from "node:url"; import { resolveNpmJsonEntries } from "./lib/npm-json-output.mts"; @@ -15,6 +16,7 @@ import { resolveCandidatePluginPackageDir, type PublishablePluginPackage, } from "./lib/plugin-npm-security-scan.mts"; +import { resolveNpmRunner } from "./npm-runner.mts"; import { inspectPackageTarballBytes, readBoundedRegularFile, @@ -164,32 +166,45 @@ async function preparePackage(args: ParsedArgs): Promise { mkdirSync(args.outputDir, { recursive: true }); } - // This unprivileged job is the only candidate-code execution boundary. It - // uses the same pack owner as publication so the trusted scanner sees built, - // overlaid, dependency-complete package bytes without executing candidate code. - const publishScript = - process.env.NODE_ENV === "test" && process.env.OPENCLAW_PLUGIN_SECURITY_TEST_PUBLISH_SCRIPT - ? process.env.OPENCLAW_PLUGIN_SECURITY_TEST_PUBLISH_SCRIPT - : join(toolingRoot, "scripts", "plugin-npm-publish.sh"); - const result = spawnSync( - "bash", - [publishScript, "--repo-root", candidateRoot, "--pack", args.packageDir], - { - cwd: toolingRoot, - encoding: "utf8", - env: { - ...process.env, - OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR: args.outputDir, - }, - killSignal: "SIGKILL", - maxBuffer: MAX_PACK_STDOUT_BYTES, - shell: false, - stdio: ["ignore", "pipe", "inherit"], - timeout: PACK_TIMEOUT_MS, + // Supplemental qualification keeps candidate code inert. Publication builds + // and scans its final artifact separately before any registry mutation. + const npm = resolveNpmRunner({ + env: { + CI: "1", + HOME: tmpdir(), + NODE_OPTIONS: "--max-old-space-size=512", + NPM_CONFIG_AUDIT: "false", + NPM_CONFIG_FUND: "false", + NPM_CONFIG_GLOBALCONFIG: "/dev/null", + NPM_CONFIG_IGNORE_SCRIPTS: "true", + NPM_CONFIG_PROVENANCE: "false", + NPM_CONFIG_USERCONFIG: join(tmpdir(), "openclaw-plugin-security-empty-npmrc"), + NPM_CONFIG_WORKSPACES: "false", + PATH: process.env.PATH, + TMPDIR: tmpdir(), }, - ); + npmArgs: [ + "pack", + "--json", + "--ignore-scripts", + "--workspaces=false", + "--pack-destination", + args.outputDir, + ], + }); + const result = spawnSync(npm.command, npm.args, { + cwd: selected.packageDir, + encoding: "utf8", + env: npm.env, + killSignal: "SIGKILL", + maxBuffer: MAX_PACK_STDOUT_BYTES, + shell: npm.shell, + stdio: ["ignore", "pipe", "inherit"], + timeout: PACK_TIMEOUT_MS, + windowsVerbatimArguments: npm.windowsVerbatimArguments, + }); if (result.status !== 0 || result.signal || result.error) { - throw new Error(`${selected.packageName}: publication-equivalent plugin pack failed.`); + throw new Error(`${selected.packageName}: trusted inert plugin pack failed.`); } const packEntries = parsePackOutput(result.stdout); if (packEntries.length !== 1) { @@ -215,17 +230,16 @@ async function preparePackage(args: ParsedArgs): Promise { inspection.packageManifest.name !== selected.packageName || inspection.packageManifest.version !== selected.packageVersion ) { - throw new Error(`${selected.packageName}: publication-equivalent package identity mismatch.`); + throw new Error(`${selected.packageName}: inert package input identity mismatch.`); } const artifactEntries = readdirSync(args.outputDir); if (artifactEntries.length !== 1 || artifactEntries[0] !== tarballName) { throw new Error(`${selected.packageName}: packaging produced unexpected artifact files.`); } const metadata = { - artifactKind: "publication-equivalent-plugin-tarball", + artifactKind: "supplemental-inert-package-input", candidateSha: args.candidateSha, extensionId: selected.extensionId, - packOwner: "scripts/plugin-npm-publish.sh", packageDir: args.packageDir, packageName: selected.packageName, packageVersion: selected.packageVersion, diff --git a/scripts/plugin-npm-security-scan-runner.mjs b/scripts/plugin-npm-security-scan-runner.mjs index a3add04c6b46..3b5ed9b1e6c1 100644 --- a/scripts/plugin-npm-security-scan-runner.mjs +++ b/scripts/plugin-npm-security-scan-runner.mjs @@ -106,6 +106,7 @@ function compactFailureReport(args, category) { errors: [`Plugin npm security scanner ${category}.`], layout: null, packages: [], + scanScope: "supplemental-inert-package-input", schemaVersion: 1, status: "fail", summary: { @@ -140,6 +141,7 @@ function existingReportStatus(args) { report?.candidateSha === args.candidateSha && Array.isArray(report?.errors) && Array.isArray(report?.packages) && + report?.scanScope === "supplemental-inert-package-input" && (report?.status === "pass" || report?.status === "fail") && typeof report?.summary === "object" && report?.toolingSha === args.toolingSha && diff --git a/scripts/plugin-npm-security-scan.mts b/scripts/plugin-npm-security-scan.mts index ff937c775a96..62c5e0649f98 100644 --- a/scripts/plugin-npm-security-scan.mts +++ b/scripts/plugin-npm-security-scan.mts @@ -95,6 +95,7 @@ function failureReport(args: ParsedArgs, message: string): PluginNpmSecurityScan errors: [message], layout: null, packages: [], + scanScope: "supplemental-inert-package-input", schemaVersion: 1, status: "fail", summary: { diff --git a/test/scripts/plugin-npm-security-scan.test.ts b/test/scripts/plugin-npm-security-scan.test.ts index 3744cacb2cd2..0c0853ee40c8 100644 --- a/test/scripts/plugin-npm-security-scan.test.ts +++ b/test/scripts/plugin-npm-security-scan.test.ts @@ -101,10 +101,9 @@ function writePluginArtifact(params: { writeFileSync( join(artifactDir, "plugin-npm-security-artifact.json"), `${JSON.stringify({ - artifactKind: "publication-equivalent-plugin-tarball", + artifactKind: "supplemental-inert-package-input", candidateSha: CANDIDATE_SHA, extensionId: params.extensionId, - packOwner: "scripts/plugin-npm-publish.sh", packageDir: `extensions/${params.extensionId}`, packageName: params.packageName, packageVersion, @@ -117,7 +116,7 @@ function writePluginArtifact(params: { ); return { artifact: { - artifactKind: "publication-equivalent-plugin-tarball" as const, + artifactKind: "supplemental-inert-package-input" as const, artifactDir, candidateSha: CANDIDATE_SHA, compressedBytes: readFileSync(tarballPath).byteLength, @@ -126,7 +125,6 @@ function writePluginArtifact(params: { 0, ), extensionId: params.extensionId, - packOwner: "scripts/plugin-npm-publish.sh" as const, packageDir: `extensions/${params.extensionId}`, packageName: params.packageName, packageVersion, @@ -192,37 +190,37 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { expect(resolveReviewedSourceLayout([...current, current[0]!])).toBeUndefined(); }); - it("scans malicious output generated by the canonical publication pack boundary", async () => { - const candidateRoot = tempDirs.make("openclaw-plugin-security-built-pack-"); - const artifactRoot = tempDirs.make("openclaw-plugin-security-built-artifacts-"); - const packageDir = join(candidateRoot, "extensions", "generated"); - const trustedPackScript = join( - tempDirs.make("openclaw-plugin-security-trusted-pack-"), - "pack.sh", - ); - const generatedPath = join(packageDir, "dist", "generated.js"); - const lifecycleMarkers = ["prepare", "prepack", "postpack"].map((name) => - join(candidateRoot, `${name}-ran`), + it("scans checked-in malicious code without running candidate hooks or helpers", async () => { + const candidateRoot = tempDirs.make("openclaw-plugin-security-inert-pack-"); + const artifactRoot = tempDirs.make("openclaw-plugin-security-inert-artifacts-"); + const packageDir = join(candidateRoot, "extensions", "inert"); + const executionMarkers = ["asset", "prepare", "prepack", "postpack", "replacement"].map( + (name) => join(candidateRoot, `${name}-ran`), ); initGitRepo(candidateRoot); mkdirSync(packageDir, { recursive: true }); const markerCommand = (marker: string) => `node -e "require('node:fs').writeFileSync(${JSON.stringify(marker)}, 'ran')"`; - const generatedCode = 'const { execSync } = require("node:child_process");\nexecSync("id");\n'; - const assetBuild = "node build-assets.mjs"; + const maliciousCode = 'const { execSync } = require("node:child_process");\nexecSync("id");\n'; writeFileSync( join(packageDir, "package.json"), `${JSON.stringify({ - files: ["dist", "openclaw.plugin.json", "package.json"], - name: "@openclaw/test-generated-pack", + files: [ + "build-assets.mjs", + "index.js", + "openclaw.plugin.json", + "package.json", + "plugin-npm-security-scan.mjs", + ], + name: "@openclaw/test-inert-pack", openclaw: { - assetScripts: { build: assetBuild }, + assetScripts: { build: markerCommand(executionMarkers[0]!) }, release: { publishToNpm: true }, }, scripts: { - postpack: markerCommand(lifecycleMarkers[2]!), - prepack: markerCommand(lifecycleMarkers[1]!), - prepare: markerCommand(lifecycleMarkers[0]!), + postpack: markerCommand(executionMarkers[3]!), + prepack: markerCommand(executionMarkers[2]!), + prepare: markerCommand(executionMarkers[1]!), }, version: "2026.8.1-beta.1", })}\n`, @@ -230,32 +228,18 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { ); writeFileSync( join(packageDir, "openclaw.plugin.json"), - `${JSON.stringify({ id: "generated" })}\n`, + `${JSON.stringify({ id: "inert" })}\n`, "utf8", ); + writeFileSync(join(packageDir, "index.js"), maliciousCode, "utf8"); writeFileSync( join(packageDir, "build-assets.mjs"), - [ - 'import { mkdirSync, writeFileSync } from "node:fs";', - 'mkdirSync("dist", { recursive: true });', - `writeFileSync("dist/generated.js", ${JSON.stringify(generatedCode)});`, - "", - ].join("\n"), + `require("node:fs").writeFileSync(${JSON.stringify(executionMarkers[0])}, "ran");\n`, "utf8", ); writeFileSync( - trustedPackScript, - [ - "#!/usr/bin/env bash", - "set -euo pipefail", - 'repo_root="$2"', - 'package_dir="$4"', - 'cd "$repo_root/$package_dir"', - 'asset_build="$(node -p "require(\'./package.json\').openclaw.assetScripts.build")"', - 'bash -c "$asset_build"', - 'npm pack --json --ignore-scripts --pack-destination "$OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR"', - "", - ].join("\n"), + join(packageDir, "plugin-npm-security-scan.mjs"), + `require("node:fs").writeFileSync(${JSON.stringify(executionMarkers[4])}, "ran");\n`, "utf8", ); execFileSync("git", ["-C", candidateRoot, "add", "."]); @@ -263,7 +247,7 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { const candidateSha = execFileSync("git", ["-C", candidateRoot, "rev-parse", "HEAD"], { encoding: "utf8", }).trim(); - const outputDir = join(artifactRoot, `plugin-npm-security-package-${candidateSha}-generated`); + const outputDir = join(artifactRoot, `plugin-npm-security-package-${candidateSha}-inert`); const toolingSha = execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim(); mkdirSync(outputDir, { recursive: true }); @@ -279,41 +263,36 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { "--candidate-sha", candidateSha, "--extension-id", - "generated", + "inert", "--output-dir", outputDir, "--package-dir", - "extensions/generated", + "extensions/inert", "--package-name", - "@openclaw/test-generated-pack", + "@openclaw/test-inert-pack", "--tooling-sha", toolingSha, ], { cwd: process.cwd(), encoding: "utf8", - env: { - ...process.env, - NODE_ENV: "test", - OPENCLAW_PLUGIN_SECURITY_TEST_PUBLISH_SCRIPT: trustedPackScript, - }, + env: { ...process.env, NODE_ENV: "test" }, }, ); expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); - expect(readFileSync(generatedPath, "utf8")).toBe(generatedCode); - for (const marker of lifecycleMarkers) { + for (const marker of executionMarkers) { expect(existsSync(marker)).toBe(false); } const metadata = JSON.parse( readFileSync(join(outputDir, "plugin-npm-security-artifact.json"), "utf8"), ) as { artifactKind?: unknown; tarballName?: unknown }; - expect(metadata.artifactKind).toBe("publication-equivalent-plugin-tarball"); + expect(metadata.artifactKind).toBe("supplemental-inert-package-input"); expect(typeof metadata.tarballName).toBe("string"); const expectedPackage = { - extensionId: "generated", - packageDir: "extensions/generated", - packageName: "@openclaw/test-generated-pack", + extensionId: "inert", + packageDir: "extensions/inert", + packageName: "@openclaw/test-inert-pack", packageVersion: "2026.8.1-beta.1", }; const loaded = loadPluginNpmSecurityArtifacts({ @@ -327,7 +306,7 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { expect(scanned.scanErrors).toEqual([]); expect(scanned.packageResults[0]?.unexpectedCriticalFindings).toContainEqual({ line: 2, - path: "dist/generated.js", + path: "index.js", ruleId: "dangerous-exec", }); }); @@ -600,6 +579,7 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => { expect(report.errors).toContain( "Plugin npm security scan exceeded the total finding-count limit.", ); + expect(report.scanScope).toBe("supplemental-inert-package-input"); expect(JSON.stringify(report)).toBe( JSON.stringify( buildPluginNpmSecurityScanReport({ diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index f659cad62cb2..b436a4ef7531 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -300,7 +300,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => { (step: WorkflowStep) => step.name === "Install trusted scanner dependencies", ); const runSecurityScan = securityScan.steps.find( - (step: WorkflowStep) => step.name === "Scan publication-equivalent plugin artifacts", + (step: WorkflowStep) => step.name === "Scan supplemental inert plugin inputs", ); const uploadReport = securityScan.steps.find( (step: WorkflowStep) => step.name === "Upload plugin npm security scan report", @@ -372,12 +372,21 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => { expect(JSON.stringify(securityPackage)).not.toContain("id-token"); expect(JSON.stringify(securityPackage)).not.toContain("packages: write"); expect(JSON.stringify(securityPackage)).not.toContain("${{ secrets."); - expect(securityPrepareSource).toContain('"scripts", "plugin-npm-publish.sh"'); - expect(securityPrepareSource).toContain('"--pack"'); - expect(securityPrepareSource).toContain("shell: false"); + expect(JSON.stringify(securityPackage)).not.toContain("plugin-npm-publish.sh"); + expect(JSON.stringify(securityPackage)).not.toContain("plugin-npm-runtime-build"); + expect(JSON.stringify(securityPackage)).not.toContain("generate-npm-package-lock"); + expect(securityPrepareSource).not.toContain("plugin-npm-publish.sh"); + expect(securityPrepareSource).not.toContain("plugin-npm-runtime-build"); + expect(securityPrepareSource).not.toContain("generate-npm-package-lock"); + expect(securityPrepareSource).not.toContain("...process.env"); + expect(securityPrepareSource).toContain('"--ignore-scripts"'); + expect(securityPrepareSource).toContain('"--workspaces=false"'); + expect(securityPrepareSource).toContain( + 'NPM_CONFIG_USERCONFIG: join(tmpdir(), "openclaw-plugin-security-empty-npmrc")', + ); + expect(securityPrepareSource).not.toContain("GITHUB_OUTPUT: process.env.GITHUB_OUTPUT"); expect(securityPrepareSource).toContain("resolveCandidatePluginPackageDir"); - expect(pluginNpmReleaseSource).toContain("bash .release-tooling/scripts/plugin-npm-publish.sh"); - expect(pluginNpmReleaseSource).toContain('--pack "${PACKAGE_DIR}"'); + expect(pluginNpmReleaseSource).toContain("plugin-publication-artifact.mjs verify"); expect(nodeShard.needs).toEqual(["resolve-candidate", "preflight"]); expect(runNodeShard?.run).toContain('spawnSync("pnpm", ["test", "--", ...configs]'); expect(pluginSource).not.toContain("npm-install-security-scan.release.test.ts");