fix(docker): seed prune store from lockfile

This commit is contained in:
Vincent Koc
2026-06-15 07:35:19 +08:00
parent 43d00c7724
commit 47ec5be9ef
4 changed files with 95 additions and 5 deletions

View File

@@ -138,7 +138,7 @@ ARG OPENCLAW_BUNDLED_PLUGIN_DIR
# BuildKit cache mounts are not part of cached layers; seed tarballs for the
# installed prod graph in the same step that runs offline prune.
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/root/.local/share/pnpm/store,sharing=locked \
pnpm list --prod --depth Infinity --json | node scripts/list-prod-store-packages.mjs | xargs -r pnpm store add && \
node scripts/list-prod-store-packages.mjs | xargs -r pnpm store add && \
CI=true pnpm prune --prod \
--config.offline=true \
--config.supportedArchitectures.os=linux \

View File

@@ -3,8 +3,6 @@ import fs from "node:fs";
import path from "node:path";
import { parse } from "yaml";
const parsed = JSON.parse(fs.readFileSync(0, "utf8"));
const roots = Array.isArray(parsed) ? parsed : [parsed];
const specs = new Set();
const target = {
cpu: process.arch,
@@ -24,6 +22,12 @@ function packageSpec(name, version) {
) {
return undefined;
}
if (normalizedVersion.startsWith("npm:")) {
return normalizedVersion.slice("npm:".length);
}
if (normalizedVersion.startsWith("@")) {
return normalizedVersion;
}
return `${name}@${normalizedVersion}`;
}
@@ -85,6 +89,15 @@ function addSpec(lockfile, spec) {
}
}
function parseListRoots() {
const input = fs.readFileSync(0, "utf8").trim();
if (!input) {
return [];
}
const parsed = JSON.parse(input);
return Array.isArray(parsed) ? parsed : [parsed];
}
function visitListNode(lockfile, node) {
for (const dep of Object.values(node.dependencies ?? {})) {
const name = dep.from || dep.name;
@@ -96,6 +109,16 @@ function visitListNode(lockfile, node) {
}
}
function addImporterRoots(lockfile) {
for (const importer of Object.values(lockfile?.importers ?? {})) {
for (const deps of [importer.dependencies, importer.optionalDependencies]) {
for (const [name, dep] of Object.entries(deps ?? {})) {
addSpec(lockfile, packageSpec(name, dep?.version));
}
}
}
}
function readLockfile() {
const lockfilePath = path.join(process.cwd(), "pnpm-lock.yaml");
if (!fs.existsSync(lockfilePath)) {
@@ -145,9 +168,10 @@ function addSnapshotClosure(lockfile) {
}
const lockfile = readLockfile();
for (const root of roots) {
for (const root of parseListRoots()) {
visitListNode(lockfile, root);
}
addImporterRoots(lockfile);
addSnapshotClosure(lockfile);
process.stdout.write([...specs].toSorted((a, b) => a.localeCompare(b)).join("\n"));

View File

@@ -101,7 +101,7 @@ describe("Dockerfile", () => {
const dockerfile = await readFile(dockerfilePath, "utf8");
const installIndex = dockerfile.indexOf("pnpm install --frozen-lockfile \\");
const storeSeedIndex = dockerfile.indexOf(
"pnpm list --prod --depth Infinity --json | node scripts/list-prod-store-packages.mjs | xargs -r pnpm store add",
"node scripts/list-prod-store-packages.mjs | xargs -r pnpm store add",
);
const pruneIndex = dockerfile.indexOf("CI=true pnpm prune --prod \\");

View File

@@ -16,6 +16,14 @@ function runListProdStorePackages(input: unknown, cwd = process.cwd()) {
});
}
function runListProdStorePackagesRaw(input: string, cwd = process.cwd()) {
return spawnSync(process.execPath, [scriptPath], {
cwd,
encoding: "utf8",
input,
});
}
describe("list-prod-store-packages", () => {
afterEach(() => {
cleanupTempDirs(tempDirs);
@@ -101,6 +109,64 @@ describe("list-prod-store-packages", () => {
expect(result.stdout).toBe("source-map-support@0.5.21\nsource-map@0.6.1");
});
it("adds production importer dependency closures without pnpm list input", () => {
const cwd = makeTempRepoRoot(tempDirs, "openclaw-prod-store-packages-");
writeFileSync(
join(cwd, "pnpm-lock.yaml"),
[
"lockfileVersion: '10.0'",
"",
"importers:",
" .:",
" dependencies:",
" '@homebridge/ciao':",
" specifier: 1.3.9",
" version: 1.3.9",
" fetch-blob:",
" specifier: 3.2.0",
" version: 3.2.0",
"",
"packages:",
" '@homebridge/ciao@1.3.9':",
" resolution: {integrity: sha512-test}",
" source-map-support@0.5.21:",
" resolution: {integrity: sha512-test}",
" source-map@0.6.1:",
" resolution: {integrity: sha512-test}",
" fetch-blob@3.2.0:",
" resolution: {integrity: sha512-test}",
" '@nolyfill/domexception@1.0.28':",
" resolution: {integrity: sha512-test}",
"",
"snapshots:",
" '@homebridge/ciao@1.3.9':",
" dependencies:",
" source-map-support: 0.5.21",
" source-map-support@0.5.21:",
" dependencies:",
" source-map: 0.6.1",
" source-map@0.6.1: {}",
" fetch-blob@3.2.0:",
" dependencies:",
" node-domexception: '@nolyfill/domexception@1.0.28'",
" '@nolyfill/domexception@1.0.28': {}",
"",
].join("\n"),
);
const result = runListProdStorePackagesRaw("", cwd);
expect(result.status).toBe(0);
expect(result.stdout.split("\n")).toEqual(
[
"@homebridge/ciao@1.3.9",
"fetch-blob@3.2.0",
"@nolyfill/domexception@1.0.28",
"source-map-support@0.5.21",
"source-map@0.6.1",
].toSorted((a, b) => a.localeCompare(b)),
);
});
it("adds target optional dependencies from peer-resolved lockfile snapshots", () => {
const cwd = makeTempRepoRoot(tempDirs, "openclaw-prod-store-packages-");
const platformPackages = [