ci: time-box package acceptance legacy compat

This commit is contained in:
Peter Steinberger
2026-04-27 07:11:11 +01:00
parent e6d2c9b080
commit 44a504cd39
10 changed files with 256 additions and 16 deletions

View File

@@ -38,6 +38,7 @@ const normalized = entries.map((entry) => entry.replace(/^package\//u, ""));
const entrySet = new Set(normalized);
const errors = [];
const warnings = [];
const LEGACY_PACKAGE_ACCEPTANCE_COMPAT_MAX = { year: 2026, month: 4, day: 25 };
const LEGACY_OMITTED_PRIVATE_QA_INVENTORY_PREFIXES = [
"dist/extensions/qa-channel/",
@@ -68,6 +69,32 @@ function isLegacyOmittedPrivateQaInventoryEntry(relativePath) {
);
}
function parseCalver(version) {
const match = /^(\d{4})\.(\d{1,2})\.(\d{1,2})(?:[-+].*)?$/u.exec(version);
if (!match) {
return null;
}
return {
year: Number(match[1]),
month: Number(match[2]),
day: Number(match[3]),
};
}
function compareCalver(left, right) {
for (const key of ["year", "month", "day"]) {
if (left[key] !== right[key]) {
return left[key] - right[key];
}
}
return 0;
}
function isLegacyPackageAcceptanceCompatVersion(version) {
const parsed = parseCalver(version);
return parsed ? compareCalver(parsed, LEGACY_PACKAGE_ACCEPTANCE_COMPAT_MAX) <= 0 : false;
}
function readTarEntry(entryPath) {
const candidates = [entryPath, `package/${entryPath}`];
for (const candidate of candidates) {
@@ -99,6 +126,10 @@ if (!entrySet.has("dist/postinstall-inventory.json")) {
}
if (entrySet.has("dist/postinstall-inventory.json")) {
try {
const packageJson = JSON.parse(readTarEntry("package.json"));
const packageVersion = typeof packageJson.version === "string" ? packageJson.version : "";
const allowLegacyPrivateQaInventoryOmissions =
isLegacyPackageAcceptanceCompatVersion(packageVersion);
const inventory = JSON.parse(readTarEntry("dist/postinstall-inventory.json"));
if (!Array.isArray(inventory) || inventory.some((entry) => typeof entry !== "string")) {
errors.push("invalid dist/postinstall-inventory.json");
@@ -106,7 +137,10 @@ if (entrySet.has("dist/postinstall-inventory.json")) {
for (const inventoryEntry of inventory) {
const normalizedEntry = inventoryEntry.replace(/\\/gu, "/");
if (!entrySet.has(normalizedEntry)) {
if (isLegacyOmittedPrivateQaInventoryEntry(normalizedEntry)) {
if (
allowLegacyPrivateQaInventoryOmissions &&
isLegacyOmittedPrivateQaInventoryEntry(normalizedEntry)
) {
warnings.push(
`legacy inventory references omitted private QA tar entry ${normalizedEntry}`,
);

View File

@@ -120,6 +120,22 @@ LOGINCTL
fi
git_cli="$git_root/openclaw.mjs"
package_version="$(node -p "require(\"$npm_root/package.json\").version")"
is_legacy_package_acceptance_compat() {
node - "$1" <<"NODE"
const version = process.argv[2] || "";
const match = /^(\d{4})\.(\d{1,2})\.(\d{1,2})(?:[-+].*)?$/.exec(version);
if (!match) process.exit(1);
const value = [Number(match[1]), Number(match[2]), Number(match[3])];
const max = [2026, 4, 25];
for (let i = 0; i < value.length; i += 1) {
if (value[i] < max[i]) process.exit(0);
if (value[i] > max[i]) process.exit(1);
}
process.exit(0);
NODE
}
assert_entrypoint() {
local unit_path="$1"
local expected="$2"
@@ -314,7 +330,11 @@ WRAPPER
if "$npm_bin" gateway install --help 2>&1 | grep -q -- "--wrapper"; then
run_wrapper_flow
else
elif is_legacy_package_acceptance_compat "$package_version"; then
# Legacy compatibility: 2026.4.25 and older did not ship gateway install --wrapper.
echo "Skipping wrapper persistence; package gateway install does not support --wrapper."
else
echo "Package $package_version must support gateway install --wrapper." >&2
exit 1
fi
'

View File

@@ -27,6 +27,9 @@ package_tgz=\"\${OPENCLAW_CURRENT_PACKAGE_TGZ:?missing OPENCLAW_CURRENT_PACKAGE_
npm install -g --prefix /tmp/npm-prefix \"\$package_tgz\" --no-fund --no-audit >/tmp/openclaw-install.log 2>&1
entry=\"/tmp/npm-prefix/lib/node_modules/openclaw/dist/index.mjs\"
[ -f \"\$entry\" ] || entry=/tmp/npm-prefix/lib/node_modules/openclaw/dist/index.js
package_version=\$(node -p \"require('/tmp/npm-prefix/lib/node_modules/openclaw/package.json').version\")
OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT=\$(PACKAGE_VERSION=\"\$package_version\" node -e 'const version = process.env.PACKAGE_VERSION || \"\"; const match = new RegExp(\"^(\\\\d{4})\\\\.(\\\\d{1,2})\\\\.(\\\\d{1,2})(?:[-+].*)?\").exec(version); if (!match) { console.log(\"0\"); process.exit(0); } const value = [Number(match[1]), Number(match[2]), Number(match[3])]; const max = [2026, 4, 25]; for (let i = 0; i < value.length; i += 1) { if (value[i] < max[i]) { console.log(\"1\"); process.exit(0); } if (value[i] > max[i]) { console.log(\"0\"); process.exit(0); } } console.log(\"1\");')
export OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT
export NPM_CONFIG_REGISTRY=http://127.0.0.1:4873
export PATH=\"/tmp/npm-prefix/bin:\$PATH\"
@@ -37,7 +40,8 @@ cat > \"\$HOME/.openclaw/extensions/lossless-claw/package.json\" <<'JSON'
\"version\": \"0.9.0\"
}
JSON
cat > \"\$HOME/.openclaw/openclaw.json\" <<'JSON'
if [ \"\$OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT\" = \"1\" ]; then
cat > \"\$HOME/.openclaw/openclaw.json\" <<'JSON'
{
\"plugins\": {
\"installs\": {
@@ -55,6 +59,13 @@ cat > \"\$HOME/.openclaw/openclaw.json\" <<'JSON'
}
}
JSON
else
cat > \"\$HOME/.openclaw/openclaw.json\" <<'JSON'
{
\"plugins\": {}
}
JSON
fi
mkdir -p \"\$HOME/.openclaw/plugins\"
cat > \"\$HOME/.openclaw/plugins/installs.json\" <<'JSON'
{
@@ -141,6 +152,11 @@ if [ \"\$registry_ready\" -ne 1 ]; then
exit 1
fi
before_config_hash=\"\"
if [ \"\$OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT\" != \"1\" ]; then
before_config_hash=\$(sha256sum \"\$HOME/.openclaw/openclaw.json\" | awk '{print \$1}')
fi
node --input-type=module > /tmp/plugin-update-before.json <<'NODE'
import fs from \"node:fs\";
import os from \"node:os\";
@@ -175,6 +191,15 @@ NODE
node \"\$entry\" plugins update @example/lossless-claw > /tmp/plugin-update-output.log 2>&1
if [ -n \"\$before_config_hash\" ]; then
after_config_hash=\$(sha256sum \"\$HOME/.openclaw/openclaw.json\" | awk '{print \$1}')
if [ \"\$before_config_hash\" != \"\$after_config_hash\" ]; then
echo \"Config changed unexpectedly for modern package \$package_version\"
cat /tmp/plugin-update-output.log
exit 1
fi
fi
node --input-type=module <<'NODE'
import fs from \"node:fs\";
import os from \"node:os\";

View File

@@ -38,6 +38,31 @@ else
exit 1
fi
export OPENCLAW_ENTRY
PACKAGE_VERSION="$(node -p 'require("./package.json").version')"
OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT="$(
node - "$PACKAGE_VERSION" <<'NODE'
const version = process.argv[2] || "";
const match = /^(\d{4})\.(\d{1,2})\.(\d{1,2})(?:[-+].*)?$/.exec(version);
if (!match) {
console.log("0");
process.exit(0);
}
const value = [Number(match[1]), Number(match[2]), Number(match[3])];
const max = [2026, 4, 25];
for (let i = 0; i < value.length; i += 1) {
if (value[i] < max[i]) {
console.log("1");
process.exit(0);
}
if (value[i] > max[i]) {
console.log("0");
process.exit(0);
}
}
console.log("1");
NODE
)"
export OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT
home_dir=$(mktemp -d "/tmp/openclaw-plugins-e2e.XXXXXX")
export HOME="$home_dir"
@@ -562,12 +587,21 @@ const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.
const index = JSON.parse(fs.readFileSync(indexPath, "utf8"));
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
const installRecords = index.installRecords ?? index.records ?? config.plugins?.installs ?? {};
const allowLegacyCompat = process.env.OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT === "1";
if (!allowLegacyCompat && !index.installRecords) {
throw new Error("expected modern installRecords in installed plugin index");
}
const installRecords = allowLegacyCompat
? index.installRecords ?? index.records ?? config.plugins?.installs ?? {}
: index.installRecords ?? {};
for (const id of ["marketplace-shortcut", "marketplace-direct"]) {
const record = installRecords[id];
if (!record) {
console.log(`legacy package did not persist marketplace install record for ${id}`);
continue;
if (allowLegacyCompat) {
console.log(`legacy package did not persist marketplace install record for ${id}`);
continue;
}
throw new Error(`missing marketplace install record for ${id}`);
}
if (record.source !== "marketplace") {
throw new Error(`unexpected source for ${id}: ${record.source}`);
@@ -853,7 +887,13 @@ const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.
const index = JSON.parse(fs.readFileSync(indexPath, "utf8"));
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
const installRecords = index.installRecords ?? index.records ?? config.plugins?.installs ?? {};
const allowLegacyCompat = process.env.OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT === "1";
if (!allowLegacyCompat && !index.installRecords) {
throw new Error("expected modern installRecords in installed plugin index");
}
const installRecords = allowLegacyCompat
? index.installRecords ?? index.records ?? config.plugins?.installs ?? {}
: index.installRecords ?? {};
const record = installRecords[pluginId];
if (!record) throw new Error(`missing ClawHub install record for ${pluginId}`);
if (record.source !== "clawhub") {

View File

@@ -48,6 +48,17 @@ const fs = require("node:fs");
const path = require("node:path");
const packageJsonPath = "/tmp/openclaw-git/package.json";
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
const isLegacyPackageAcceptanceCompat = (version) => {
const match = /^(\d{4})\.(\d{1,2})\.(\d{1,2})(?:[-+].*)?$/.exec(version || "");
if (!match) return false;
const value = [Number(match[1]), Number(match[2]), Number(match[3])];
const max = [2026, 4, 25];
for (let i = 0; i < value.length; i += 1) {
if (value[i] < max[i]) return true;
if (value[i] > max[i]) return false;
}
return true;
};
const fixtureUiBuildSource = `const fs=require("node:fs");fs.mkdirSync("dist/control-ui",{recursive:true});fs.writeFileSync("dist/control-ui/index.html","<!doctype html><title>fixture</title>\\n")`;
const fixtureUiBuildCommand = `node -e ${JSON.stringify(fixtureUiBuildSource)}`;
const nextPnpm = { ...packageJson.pnpm, allowUnusedPatches: true };
@@ -57,14 +68,28 @@ if (
typeof patchedDependencies === "object" &&
!Array.isArray(patchedDependencies)
) {
const patchEntries = Object.entries(patchedDependencies);
const keptPatches = Object.fromEntries(
Object.entries(patchedDependencies).filter(([, patchFile]) => {
patchEntries.filter(([, patchFile]) => {
return (
typeof patchFile === "string" &&
fs.existsSync(path.resolve(path.dirname(packageJsonPath), patchFile))
);
}),
);
const missingPatches = patchEntries.filter(([dependency, patchFile]) => {
return (
typeof patchFile !== "string" ||
!fs.existsSync(path.resolve(path.dirname(packageJsonPath), patchFile))
);
});
if (missingPatches.length > 0 && !isLegacyPackageAcceptanceCompat(packageJson.version)) {
throw new Error(
`package ${packageJson.version} has missing pnpm.patchedDependencies in package fixture: ${missingPatches
.map(([dependency, patchFile]) => `${dependency} -> ${patchFile}`)
.join(", ")}`,
);
}
if (Object.keys(keptPatches).length > 0) {
nextPnpm.patchedDependencies = keptPatches;
} else {
@@ -105,6 +130,31 @@ fixture_sha="$(git -C "$git_root" rev-parse HEAD)"
pkg_tgz_path="$package_tgz"
npm install -g --prefix /tmp/npm-prefix --omit=optional "$pkg_tgz_path"
package_version="$(node -p "require('/tmp/npm-prefix/lib/node_modules/openclaw/package.json').version")"
OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT="$(
node - "$package_version" <<"NODE"
const version = process.argv[2] || "";
const match = /^(\d{4})\.(\d{1,2})\.(\d{1,2})(?:[-+].*)?$/.exec(version);
if (!match) {
console.log("0");
process.exit(0);
}
const value = [Number(match[1]), Number(match[2]), Number(match[3])];
const max = [2026, 4, 25];
for (let i = 0; i < value.length; i += 1) {
if (value[i] < max[i]) {
console.log("1");
process.exit(0);
}
if (value[i] > max[i]) {
console.log("0");
process.exit(0);
}
}
console.log("1");
NODE
)"
export OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT
home_dir="$(mktemp -d /tmp/openclaw-update-channel-switch-home.XXXXXX)"
export HOME="$home_dir"
@@ -149,7 +199,11 @@ const path = require("node:path");
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
if (config.update?.channel !== "dev") {
console.log(`legacy package did not persist update.channel dev; got ${JSON.stringify(config.update?.channel)}`);
if (process.env.OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT === "1") {
console.log(`legacy package did not persist update.channel dev; got ${JSON.stringify(config.update?.channel)}`);
} else {
throw new Error(`expected persisted update.channel dev, got ${JSON.stringify(config.update?.channel)}`);
}
}
NODE
@@ -190,7 +244,11 @@ const path = require("node:path");
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
if (config.update?.channel !== "stable") {
console.log(`legacy package did not persist update.channel stable; got ${JSON.stringify(config.update?.channel)}`);
if (process.env.OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT === "1") {
console.log(`legacy package did not persist update.channel stable; got ${JSON.stringify(config.update?.channel)}`);
} else {
throw new Error(`expected persisted update.channel stable, got ${JSON.stringify(config.update?.channel)}`);
}
}
NODE