fix(release): keep npm 12 pack checks working (#118209)

* fix(release): accept npm 12 pack json

* fix(release): share npm pack normalizer in plugin workflow
This commit is contained in:
Peter Steinberger
2026-08-02 14:57:49 -07:00
committed by GitHub
parent ccec5495ae
commit 7866ecae4b
20 changed files with 205 additions and 54 deletions

View File

@@ -641,10 +641,11 @@ jobs:
env:
BASELINE_PACK_JSON: ${{ runner.temp }}/openclaw-cross-os-release-checks/prepare/baseline/pack.json
run: |
node <<'NODE' >>"$GITHUB_OUTPUT"
const crypto = require("node:crypto");
const fs = require("node:fs");
const path = require("node:path");
node --input-type=module <<'NODE' >>"$GITHUB_OUTPUT"
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { resolveNpmJsonEntries } from "./scripts/lib/npm-json-output.mjs";
function resolveTarballFileName(value, label) {
const fileName = typeof value === "string" ? value.trim() : "";
if (
@@ -658,7 +659,7 @@ jobs:
return fileName;
}
const payload = JSON.parse(fs.readFileSync(process.env.BASELINE_PACK_JSON, "utf8"));
const entry = Array.isArray(payload) ? payload.at(-1) : null;
const entry = resolveNpmJsonEntries(payload).at(-1);
const fileName = resolveTarballFileName(entry?.filename, "Baseline npm pack filename");
const sha256 = crypto
.createHash("sha256")

View File

@@ -11,6 +11,7 @@ on:
- "package.json"
- "scripts/generate-npm-package-lock.mjs"
- "scripts/lib/npm-publish-plan.mjs"
- "scripts/lib/npm-json-output.mjs"
- "scripts/lib/release-version.mjs"
- "scripts/lib/plugin-npm-package-manifest.mjs"
- "scripts/lib/plugin-npm-release.ts"
@@ -338,6 +339,7 @@ jobs:
fetch-depth: 1
sparse-checkout: |
scripts/generate-npm-package-lock.mjs
scripts/lib/npm-json-output.mjs
scripts/lib/plugin-npm-package-manifest.mjs
sparse-checkout-cone-mode: false
@@ -347,6 +349,9 @@ jobs:
cp \
.release-tooling/scripts/generate-npm-package-lock.mjs \
scripts/generate-npm-package-lock.mjs
cp \
.release-tooling/scripts/lib/npm-json-output.mjs \
scripts/lib/npm-json-output.mjs
cp \
.release-tooling/scripts/lib/plugin-npm-package-manifest.mjs \
scripts/lib/plugin-npm-package-manifest.mjs
@@ -398,26 +403,27 @@ jobs:
OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR="${artifact_dir}" \
bash scripts/plugin-npm-publish.sh --pack "${PACKAGE_DIR}" > "${pack_output}"
node - "${pack_output}" "${pack_json}" <<'NODE'
const fs = require("node:fs");
node --input-type=module - "${pack_output}" "${pack_json}" <<'NODE'
import fs from "node:fs";
import { resolveNpmJsonEntries } from "./scripts/lib/npm-json-output.mjs";
const raw = fs.readFileSync(process.argv[2], "utf8").trim();
let pack;
for (let index = raw.lastIndexOf("["); index >= 0; index = raw.lastIndexOf("[", index - 1)) {
for (let index = raw.length - 1; index >= 0; index -= 1) {
if (raw[index] !== "[" && raw[index] !== "{") continue;
try {
const candidate = JSON.parse(raw.slice(index));
if (Array.isArray(candidate)) {
pack = candidate;
const entries = resolveNpmJsonEntries(candidate);
if (entries.length > 0) {
pack = entries;
break;
}
} catch {
// npm can print bundled-dependency summaries before its JSON result.
}
if (index === 0) {
break;
}
}
if (!pack) {
throw new Error("npm pack did not emit a JSON array.");
throw new Error("npm pack did not emit package JSON.");
}
fs.writeFileSync(process.argv[3], `${JSON.stringify(pack, null, 2)}\n`);
NODE
@@ -1238,6 +1244,9 @@ jobs:
cp \
scripts/generate-npm-package-lock.mjs \
.publication-target/scripts/generate-npm-package-lock.mjs
cp \
scripts/lib/npm-json-output.mjs \
.publication-target/scripts/lib/npm-json-output.mjs
cp \
scripts/lib/plugin-npm-package-manifest.mjs \
.publication-target/scripts/lib/plugin-npm-package-manifest.mjs

View File

@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { resolveNpmJsonEntries } from "../../lib/npm-json-output.mjs";
import { sleep as delay } from "../../lib/sleep.mjs";
import { readPositiveIntEnv } from "./env-limits.ts";
import { exists, readJson } from "./filesystem.ts";
@@ -28,12 +29,7 @@ export async function packageBuildCommitFromTgz(tgzPath: string): Promise<string
}
function resolveNpmPackTarballFilename(value: unknown): string {
// npm 10/11 return arrays; npm 12 keys local-workspace results by package name.
const result = Array.isArray(value)
? value.at(-1)
: value && typeof value === "object" && "openclaw" in value
? value.openclaw
: value;
const result = resolveNpmJsonEntries(value).at(-1);
const filename =
result &&
typeof result === "object" &&

View File

@@ -0,0 +1 @@
export declare function resolveNpmJsonEntries(value: unknown): unknown[];

View File

@@ -0,0 +1,26 @@
/**
* Normalize npm <=11 entry/array JSON and npm 12 name-keyed JSON.
* Keep pack consumers on one dependency-contract boundary so a package-manager
* upgrade cannot silently bypass release, installer, or security checks.
*/
export function resolveNpmJsonEntries(value) {
if (Array.isArray(value)) {
return value;
}
if (value && typeof value === "object") {
const looksLikeEntry =
typeof value.id === "string" ||
typeof value.name === "string" ||
typeof value.version === "string" ||
typeof value.filename === "string";
if (!looksLikeEntry) {
const entries = Object.values(value).filter(
(entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry),
);
if (entries.length > 0) {
return entries;
}
}
}
return [value];
}

View File

@@ -3,8 +3,13 @@ export type NpmPackBudgetResult = {
unpackedSize?: number;
};
export type NpmPackBudgetResults =
| Iterable<NpmPackBudgetResult>
| NpmPackBudgetResult
| Record<string, NpmPackBudgetResult>;
export declare function collectPackUnpackedSizeErrors(
results: Iterable<NpmPackBudgetResult>,
results: NpmPackBudgetResults,
options?: {
budgetBytes?: number;
missingDataMessage?: string;

View File

@@ -1,3 +1,5 @@
import { resolveNpmJsonEntries } from "./npm-json-output.mjs";
// 2026.3.12 ballooned to ~213.6 MiB unpacked and correlated with low-memory
// startup/doctor OOM reports. 2026.4.12 intentionally stages Matrix runtime
// dependencies, including crypto wasm, so packaged installs do not miss Docker
@@ -22,12 +24,15 @@ function formatPackUnpackedSizeBudgetError(params) {
}
export function collectPackUnpackedSizeErrors(results, options = {}) {
const entries = Array.from(results);
const entries = resolveNpmJsonEntries(results);
const errors = [];
const budgetBytes = options.budgetBytes ?? NPM_PACK_UNPACKED_SIZE_BUDGET_BYTES;
let checkedCount = 0;
for (const [index, entry] of entries.entries()) {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
continue;
}
if (typeof entry.unpackedSize !== "number" || !Number.isFinite(entry.unpackedSize)) {
continue;
}

View File

@@ -5,6 +5,7 @@ import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { resolveNpmJsonEntries } from "./lib/npm-json-output.mjs";
import { resolveNpmDistTagMirrorAuth as resolveNpmDistTagMirrorAuthBase } from "./lib/npm-publish-plan.mjs";
import { readPositiveEnvInt } from "./lib/numeric-options.mjs";
import {
@@ -575,16 +576,20 @@ export function parseNpmPackJsonOutput(stdout: string): NpmPackResult[] | null {
}
const candidates = [trimmed];
const trailingArrayStart = trimmed.lastIndexOf("\n[");
if (trailingArrayStart !== -1) {
candidates.push(trimmed.slice(trailingArrayStart + 1).trim());
const trailingJsonStart = Math.max(trimmed.lastIndexOf("\n["), trimmed.lastIndexOf("\n{"));
if (trailingJsonStart !== -1) {
candidates.push(trimmed.slice(trailingJsonStart + 1).trim());
}
for (const candidate of candidates) {
try {
const parsed = JSON.parse(candidate) as unknown;
if (Array.isArray(parsed)) {
return parsed as NpmPackResult[];
const entries = resolveNpmJsonEntries(parsed).filter(
(entry): entry is NpmPackResult =>
Boolean(entry) && typeof entry === "object" && !Array.isArray(entry),
);
if (entries.length > 0) {
return entries;
}
} catch {
// Try the next candidate. npm lifecycle output can prepend non-JSON logs.

View File

@@ -9,6 +9,7 @@ import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV } from "./lib/bundled-plugin-build-entries.mjs";
import { terminateManagedChild } from "./lib/managed-child-process.mjs";
import { resolveNpmJsonEntries } from "./lib/npm-json-output.mjs";
import { resolveNpmRunner } from "./npm-runner.mjs";
import { preparePackageChangelog, restorePackageChangelog } from "./package-changelog.mjs";
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
@@ -416,15 +417,13 @@ async function newestOpenClawTarball(outputDir, packOutput) {
let fromOutput = "";
try {
const parsed = JSON.parse(packOutput);
if (Array.isArray(parsed)) {
for (const entry of parsed) {
if (typeof entry?.filename !== "string") {
continue;
}
const filename = resolvePackedOpenClawFileName(entry.filename);
if (filename) {
fromOutput = filename;
}
for (const entry of resolveNpmJsonEntries(parsed)) {
if (typeof entry?.filename !== "string") {
continue;
}
const filename = resolvePackedOpenClawFileName(entry.filename);
if (filename) {
fromOutput = filename;
}
}
} catch {}
@@ -465,18 +464,22 @@ async function writePackJson(packOutput, tarball, packJsonPath, sourceDir) {
} catch (error) {
throw new Error("npm pack --json output was not valid JSON", { cause: error });
}
if (!Array.isArray(parsed)) {
throw new Error("npm pack --json output must be an array");
const entries = resolveNpmJsonEntries(parsed);
if (
entries.length === 0 ||
entries.some((entry) => !entry || typeof entry !== "object" || Array.isArray(entry))
) {
throw new Error("npm pack --json output did not contain package results");
}
const filename = path.basename(tarball);
for (const entry of parsed) {
for (const entry of entries) {
if (entry && typeof entry === "object" && typeof entry.filename === "string") {
entry.filename = filename;
}
}
const target = path.resolve(sourceDir, packJsonPath);
await fs.mkdir(path.dirname(target), { recursive: true });
await fs.writeFile(target, `${JSON.stringify(parsed, null, 2)}\n`);
await fs.writeFile(target, `${JSON.stringify(entries, null, 2)}\n`);
}
async function cleanPackedOpenClawTarballs(outputDir) {

View File

@@ -31,6 +31,7 @@ import {
collectRootPackageExcludedExtensionDirs,
listBundledPluginPackArtifacts,
} from "./lib/bundled-plugin-build-entries.mjs";
import { resolveNpmJsonEntries } from "./lib/npm-json-output.mjs";
import { collectPackUnpackedSizeErrors as collectNpmPackUnpackedSizeErrors } from "./lib/npm-pack-budget.mjs";
import { readPositiveEnvInt } from "./lib/numeric-options.mjs";
import {
@@ -397,7 +398,7 @@ function runPackDry(): PackResult[] {
stdio: ["ignore", "pipe", "pipe"],
maxBuffer: 1024 * 1024 * 100,
});
return JSON.parse(raw) as PackResult[];
return resolveNpmJsonEntries(JSON.parse(raw)) as PackResult[];
}
function runPack(packDestination: string, cwd?: string): PackResult[] {
@@ -410,8 +411,7 @@ function runPack(packDestination: string, cwd?: string): PackResult[] {
maxBuffer: 1024 * 1024 * 100,
},
);
const parsed = JSON.parse(raw) as PackResult | PackResult[];
return Array.isArray(parsed) ? parsed : [parsed];
return resolveNpmJsonEntries(JSON.parse(raw)) as PackResult[];
}
export function resolvePackedTarballPath(packDestination: string, results: PackResult[]): string {

View File

@@ -13,6 +13,7 @@ import os from "node:os";
import path from "node:path";
import { pipeline } from "node:stream/promises";
import { fileURLToPath } from "node:url";
import { resolveNpmJsonEntries } from "./lib/npm-json-output.mjs";
import { resolveWindowsTaskkillPath } from "./lib/windows-taskkill.mjs";
import { resolveNpmRunner } from "./npm-runner.mjs";
@@ -660,9 +661,10 @@ async function moveNewestPackedTarball(outputDir, packOutput, outputName) {
try {
parsed = JSON.parse(packOutput);
} catch {}
if (Array.isArray(parsed)) {
if (parsed !== undefined) {
const packedFilename =
parsed.find((entry) => typeof entry?.filename === "string")?.filename ?? "";
resolveNpmJsonEntries(parsed).find((entry) => typeof entry?.filename === "string")
?.filename ?? "";
if (packedFilename) {
filename = resolvePackedOpenClawTarballFilename(packedFilename);
}

View File

@@ -11,6 +11,25 @@ source "$HARNESS_ROOT/scripts/lib/docker-e2e-package.sh"
DOCKER_COMMAND_TIMEOUT="${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_INSTALL_SMOKE_DOCKER_COMMAND_TIMEOUT:-600s}}"
INSTALL_SMOKE_DOCKER_RUN_TIMEOUT="${OPENCLAW_INSTALL_SMOKE_DOCKER_RUN_TIMEOUT:-2700s}"
normalize_npm_pack_json_file() {
local pack_json_file="$1"
(
cd "$HARNESS_ROOT"
node --input-type=module - "$pack_json_file" <<'NODE'
import fs from "node:fs";
import { resolveNpmJsonEntries } from "./scripts/lib/npm-json-output.mjs";
const packJsonFile = process.argv[2];
const parsed = JSON.parse(fs.readFileSync(packJsonFile, "utf8"));
const entries = resolveNpmJsonEntries(parsed);
if (entries.length === 0) {
throw new Error("npm pack output did not contain a package result");
}
fs.writeFileSync(packJsonFile, `${JSON.stringify(entries, null, 2)}\n`, "utf8");
NODE
)
}
run_install_smoke_container() {
DOCKER_COMMAND_TIMEOUT="$INSTALL_SMOKE_DOCKER_RUN_TIMEOUT" docker_e2e_docker_run_cmd run "$@"
}
@@ -311,6 +330,7 @@ prepare_update_tarball() {
if [[ -n "$UPDATE_PACKAGE_SPEC" ]]; then
echo "==> Pack update tgz from spec: $UPDATE_PACKAGE_SPEC"
quiet_npm pack "$UPDATE_PACKAGE_SPEC" --json --pack-destination "$UPDATE_DIR" >"$pack_json_file"
normalize_npm_pack_json_file "$pack_json_file"
else
echo "==> Build local release artifacts for update smoke"
if [[ -n "$UPDATE_DIST_IMAGE" ]]; then
@@ -366,6 +386,7 @@ process.stdout.write(last.version);
echo "==> Pack baseline tgz: ${PACKAGE_NAME}@${UPDATE_BASELINE_VERSION}"
quiet_npm pack "${PACKAGE_NAME}@${UPDATE_BASELINE_VERSION}" --json --pack-destination "$UPDATE_DIR" >"$baseline_pack_json_file"
normalize_npm_pack_json_file "$baseline_pack_json_file"
BASELINE_TGZ_FILE="$(read_pack_tarball_filename "$baseline_pack_json_file")"
UPDATE_BASELINE_VERSION="$(
node -e '

View File

@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
import { dirname, join, relative, resolve } from "node:path";
import { promisify } from "node:util";
import { beforeAll, describe, expect, it, test } from "vitest";
import { resolveNpmJsonEntries } from "../infra/npm-registry-spec.js";
import { isScannable, scanDirectoryWithSummary } from "../skills/security/scanner.js";
import { expectNoReaddirSyncDuring } from "../test-utils/fs-scan-assertions.js";
import { listGitTrackedFiles, toRepoPath, toRepoRelativePath } from "../test-utils/repo-files.js";
@@ -55,11 +56,12 @@ const OPTIONAL_REVIEWED_PUBLISHABLE_DIST_CRITICAL_FINDING_COUNTS = new Map<strin
function parseNpmPackFiles(raw: string, packageName: string): string[] {
const parsed = JSON.parse(raw) as unknown;
if (!Array.isArray(parsed) || parsed.length !== 1) {
const entries = resolveNpmJsonEntries(parsed);
if (entries.length !== 1) {
throw new Error(`${packageName}: npm pack --dry-run did not return one package result.`);
}
const result = parsed[0] as NpmPackResult;
const result = entries[0] as NpmPackResult;
if (!Array.isArray(result.files)) {
throw new Error(`${packageName}: npm pack --dry-run did not return a files list.`);
}

View File

@@ -241,6 +241,7 @@ describe("package-openclaw-for-docker", () => {
"scripts/lib/bundled-plugin-build-entries.mjs",
"scripts/lib/bundled-plugin-paths.mjs",
"scripts/lib/managed-child-process.mjs",
"scripts/lib/npm-json-output.mjs",
"scripts/lib/optional-bundled-clusters.mjs",
"scripts/lib/windows-taskkill.mjs",
];
@@ -794,7 +795,7 @@ describe("package-openclaw-for-docker", () => {
}
});
it("writes npm pack metadata for renamed package artifacts", async () => {
it("normalizes npm 12 pack metadata for renamed package artifacts", async () => {
const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-docker-pack-json-"));
const packJsonPath = path.join(outputDir, "pack.json");
@@ -823,15 +824,15 @@ describe("package-openclaw-for-docker", () => {
]);
expect(options.deferForwardedSignalExit).toBe(true);
fs.writeFileSync(path.join(outputDir, "openclaw-2026.5.28.tgz"), "package");
return JSON.stringify([
{
return JSON.stringify({
openclaw: {
entryCount: 1,
filename: "openclaw-2026.5.28.tgz",
size: 7,
unpackedSize: 7,
version: "2026.5.28",
},
]);
});
},
});

View File

@@ -544,6 +544,28 @@ describe("parseNpmPackJsonOutput", () => {
]);
});
it("parses npm 12 name-keyed pack output", () => {
expect(
parseNpmPackJsonOutput(
'{"openclaw":{"filename":"openclaw.tgz","files":[{"path":"dist/control-ui/index.html"}]}}',
),
).toEqual([
{
filename: "openclaw.tgz",
files: [{ path: "dist/control-ui/index.html" }],
},
]);
});
it("parses trailing npm 12 output after lifecycle logs", () => {
const stdout = [
"> openclaw@2026.7.2 prepack",
'{"openclaw":{"filename":"openclaw.tgz","files":[]}}',
].join("\n");
expect(parseNpmPackJsonOutput(stdout)).toEqual([{ filename: "openclaw.tgz", files: [] }]);
});
it("parses the trailing JSON payload after npm lifecycle logs", () => {
const stdout = [
'npm warn Unknown project config "node-linker".',

View File

@@ -5,6 +5,7 @@ import { dirname, join, resolve as resolvePath, win32 } from "node:path";
import { bundledDistPluginFile, bundledPluginFile } from "openclaw/plugin-sdk/test-fixtures";
import { describe, expect, it } from "vitest";
import { listBundledPluginPackArtifacts } from "../scripts/lib/bundled-plugin-build-entries.mjs";
import { resolveNpmJsonEntries } from "../scripts/lib/npm-json-output.mjs";
import {
LOCAL_BUILD_METADATA_DIST_PATHS,
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
@@ -48,6 +49,7 @@ import {
} from "../scripts/release-check.ts";
import { listStaticExtensionAssetOutputs } from "../scripts/runtime-postbuild.mjs";
import { COMPLETION_SKIP_PLUGIN_COMMANDS_ENV } from "../src/cli/completion-runtime.ts";
import { resolveNpmJsonEntries as resolveRuntimeNpmJsonEntries } from "../src/infra/npm-registry-spec.js";
import { withEnv } from "../src/test-utils/env.js";
function makeItem(shortVersion: string, sparkleVersion: string, channel?: string): string {
@@ -911,6 +913,14 @@ describe("collectPackUnpackedSizeErrors", () => {
).toStrictEqual([]);
});
it("accepts npm 12 name-keyed pack results", () => {
expect(
collectPackUnpackedSizeErrors({
openclaw: makePackResult("openclaw-2026.3.14.tgz", 120_354_302),
}),
).toStrictEqual([]);
});
it("flags oversized pack results that risk low-memory startup failures", () => {
expect(
collectPackUnpackedSizeErrors([makePackResult("openclaw-2026.3.12.tgz", 224_002_564)]),
@@ -931,6 +941,20 @@ describe("collectPackUnpackedSizeErrors", () => {
});
});
describe("resolveNpmJsonEntries", () => {
it("normalizes npm <=11 arrays and npm 12 name-keyed objects", () => {
const entry = makePackResult("openclaw-2026.7.2.tgz", 120_354_302);
expect(resolveNpmJsonEntries([entry])).toEqual([entry]);
expect(resolveNpmJsonEntries(entry)).toEqual([entry]);
expect(resolveNpmJsonEntries({ openclaw: entry })).toEqual([entry]);
expect(resolveNpmJsonEntries({ "@openclaw/demo": entry })).toEqual([entry]);
expect(resolveNpmJsonEntries({ openclaw: entry })).toEqual(
resolveRuntimeNpmJsonEntries({ openclaw: entry }),
);
});
});
describe("resolvePackedTarballPath", () => {
it("resolves one local npm pack tarball filename inside the pack destination", () => {
expect(

View File

@@ -65,6 +65,10 @@ describe("cross-OS release checks workflow", () => {
const workflow = readFileSync(WORKFLOW_PATH, "utf8");
expect(workflow).toContain("timeout --preserve-status 300s npm pack --ignore-scripts");
expect(workflow).toContain(
'import { resolveNpmJsonEntries } from "./scripts/lib/npm-json-output.mjs";',
);
expect(workflow).toContain("const entry = resolveNpmJsonEntries(payload).at(-1);");
});
it("keeps release artifact tarball filenames local before upload paths use them", () => {

View File

@@ -105,7 +105,7 @@ describe("plugin npm extended-stable workflow", () => {
}
});
it("overlays the complete trusted packaging helper dependency pair", () => {
it("overlays the complete trusted packaging helper dependency set", () => {
const parsed = workflow();
const preflightCheckout = step(
parsed.jobs?.preview_plugin_pack,
@@ -114,12 +114,16 @@ describe("plugin npm extended-stable workflow", () => {
expect(preflightCheckout.with?.["sparse-checkout"]).toContain(
"scripts/generate-npm-package-lock.mjs",
);
expect(preflightCheckout.with?.["sparse-checkout"]).toContain(
"scripts/lib/npm-json-output.mjs",
);
expect(preflightCheckout.with?.["sparse-checkout"]).toContain(
"scripts/lib/plugin-npm-package-manifest.mjs",
);
const expectedCopies = [
"scripts/generate-npm-package-lock.mjs",
"scripts/lib/npm-json-output.mjs",
"scripts/lib/plugin-npm-package-manifest.mjs",
];
for (const helperPath of expectedCopies) {
@@ -202,9 +206,12 @@ describe("plugin npm extended-stable workflow", () => {
);
expect(prepare.if).toBeUndefined();
expect(prepare.run).toContain('bash scripts/plugin-npm-publish.sh --pack "${PACKAGE_DIR}"');
expect(prepare.run).toContain('raw.lastIndexOf("[")');
expect(prepare.run).toContain(
'import { resolveNpmJsonEntries } from "./scripts/lib/npm-json-output.mjs";',
);
expect(prepare.run).toContain('raw[index] !== "[" && raw[index] !== "{"');
expect(prepare.run).toContain("const entries = resolveNpmJsonEntries(candidate)");
expect(prepare.run).toContain("npm can print bundled-dependency summaries");
expect(prepare.run).toContain("if (index === 0)");
expect(prepare.run).toContain(
"fs.writeFileSync(process.argv[3], `${JSON.stringify(pack, null, 2)}\\n`)",
);

View File

@@ -390,6 +390,20 @@ describe("resolve-openclaw-package-candidate", () => {
await expect(readFile(path.join(dir, "openclaw-current.tgz"), "utf8")).resolves.toBe("package");
});
it("reads npm 12 name-keyed package candidate filenames", async () => {
const dir = await mkdtemp(path.join(tmpdir(), "openclaw-package-npm-pack-"));
tempDirs.push(dir);
await writeFile(path.join(dir, "openclaw-2026.6.17.tgz"), "package");
await expect(
moveNewestPackedTarballForTest(
dir,
JSON.stringify({ openclaw: { filename: "openclaw-2026.6.17.tgz" } }),
"openclaw-current.tgz",
),
).resolves.toBe(path.join(dir, "openclaw-current.tgz"));
});
it("rejects path-like npm pack filenames instead of renaming outside the output directory", async () => {
const dir = await mkdtemp(path.join(tmpdir(), "openclaw-package-npm-pack-"));
tempDirs.push(dir);

View File

@@ -1123,6 +1123,9 @@ printf 'status=%s\\n' "$status"
expect(script).toContain("print_pack_delta_audit");
expect(script).toContain("==> Pack audit");
expect(script).toContain("==> Pack audit delta");
expect(script).toContain("normalize_npm_pack_json_file");
expect(script).toContain('normalize_npm_pack_json_file "$pack_json_file"');
expect(script).toContain('normalize_npm_pack_json_file "$baseline_pack_json_file"');
});
it("fails the update smoke when the candidate npm pack exceeds the release budget", () => {