mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-08 11:02:26 +00:00
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:
committed by
GitHub
parent
ccec5495ae
commit
7866ecae4b
@@ -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")
|
||||
|
||||
27
.github/workflows/plugin-npm-release.yml
vendored
27
.github/workflows/plugin-npm-release.yml
vendored
@@ -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
|
||||
|
||||
@@ -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" &&
|
||||
|
||||
1
scripts/lib/npm-json-output.d.mts
Normal file
1
scripts/lib/npm-json-output.d.mts
Normal file
@@ -0,0 +1 @@
|
||||
export declare function resolveNpmJsonEntries(value: unknown): unknown[];
|
||||
26
scripts/lib/npm-json-output.mjs
Normal file
26
scripts/lib/npm-json-output.mjs
Normal 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];
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 '
|
||||
|
||||
@@ -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.`);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
]);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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".',
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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`)",
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user