mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 06:31:33 +00:00
fix(release): omit unavailable candidate Docker scripts (#116432)
* fix(release): omit unavailable candidate Docker scripts * fix(docker): fail on unreadable candidate manifest * fix(docker): expose candidate planner option
This commit is contained in:
@@ -19,6 +19,7 @@ export type DockerE2ePlanLane = {
|
||||
|
||||
export type DockerE2ePlanOptions = {
|
||||
allowFrozenTargetScenarioOmissions?: boolean;
|
||||
candidatePackageRoot?: string;
|
||||
includeOpenWebUI: boolean;
|
||||
liveMode: "all" | "only" | "skip";
|
||||
liveRetries: number;
|
||||
|
||||
@@ -497,6 +497,63 @@ function applyLiveRetries(poolLanes, retries) {
|
||||
return poolLanes.map((poolLane) => (poolLane.live ? { ...poolLane, retries } : poolLane));
|
||||
}
|
||||
|
||||
const PNPM_NON_SCRIPT_COMMANDS = new Set([
|
||||
"add",
|
||||
"audit",
|
||||
"config",
|
||||
"dlx",
|
||||
"exec",
|
||||
"fetch",
|
||||
"install",
|
||||
"pack",
|
||||
"publish",
|
||||
"rebuild",
|
||||
"remove",
|
||||
]);
|
||||
|
||||
function candidatePackageScripts(candidatePackageRoot) {
|
||||
if (!candidatePackageRoot) {
|
||||
return undefined;
|
||||
}
|
||||
const packageJson = JSON.parse(
|
||||
readFileSync(resolve(candidatePackageRoot, "package.json"), "utf8"),
|
||||
);
|
||||
if (!packageJson || typeof packageJson !== "object" || Array.isArray(packageJson)) {
|
||||
throw new Error("Candidate package manifest must be an object");
|
||||
}
|
||||
if (
|
||||
packageJson.scripts !== undefined &&
|
||||
(!packageJson.scripts ||
|
||||
typeof packageJson.scripts !== "object" ||
|
||||
Array.isArray(packageJson.scripts))
|
||||
) {
|
||||
throw new Error("Candidate package manifest has an invalid scripts field");
|
||||
}
|
||||
return new Set(
|
||||
Object.entries(packageJson.scripts ?? {})
|
||||
.filter(([, command]) => typeof command === "string")
|
||||
.map(([name]) => name),
|
||||
);
|
||||
}
|
||||
|
||||
function requiredPackageScripts(poolLane) {
|
||||
return [...poolLane.command.matchAll(/\bpnpm\s+(?:run\s+)?([a-z][a-z0-9:-]*)/giu)]
|
||||
.map(([, script]) => script)
|
||||
.filter((script) => !PNPM_NON_SCRIPT_COMMANDS.has(script));
|
||||
}
|
||||
|
||||
function filterUnavailableCandidateScriptLanes(poolLanes, candidatePackageRoot) {
|
||||
const scripts = candidatePackageScripts(candidatePackageRoot);
|
||||
if (!scripts) {
|
||||
return poolLanes;
|
||||
}
|
||||
// The trusted catalog can add lanes before a frozen candidate has their scripts.
|
||||
// Only schedule package-script commands the selected candidate can execute.
|
||||
return poolLanes.filter((poolLane) =>
|
||||
requiredPackageScripts(poolLane).every((script) => scripts.has(script)),
|
||||
);
|
||||
}
|
||||
|
||||
export function laneWeight(poolLane) {
|
||||
return Math.max(1, poolLane.weight ?? 1);
|
||||
}
|
||||
@@ -726,8 +783,16 @@ export function resolveDockerE2ePlan(options) {
|
||||
: options.liveMode === "only"
|
||||
? []
|
||||
: applyLiveMode(retriedTailLanes, options.liveMode);
|
||||
const orderedLanes = options.orderLanes(configuredLanes, options.timingStore);
|
||||
const orderedTailLanes = options.orderLanes(configuredTailLanes, options.timingStore);
|
||||
const availableLanes = filterUnavailableCandidateScriptLanes(
|
||||
configuredLanes,
|
||||
options.candidatePackageRoot,
|
||||
);
|
||||
const availableTailLanes = filterUnavailableCandidateScriptLanes(
|
||||
configuredTailLanes,
|
||||
options.candidatePackageRoot,
|
||||
);
|
||||
const orderedLanes = options.orderLanes(availableLanes, options.timingStore);
|
||||
const orderedTailLanes = options.orderLanes(availableTailLanes, options.timingStore);
|
||||
return {
|
||||
omittedUnsupportedLaneNames: [...omittedUnsupportedLaneNames],
|
||||
orderedLanes,
|
||||
|
||||
@@ -1538,6 +1538,7 @@ async function main() {
|
||||
upgradeSurvivorScenarios: process.env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS,
|
||||
upgradeSurvivorTargetRoot: process.env.OPENCLAW_UPGRADE_SURVIVOR_TARGET_ROOT,
|
||||
allowFrozenTargetScenarioOmissions,
|
||||
candidatePackageRoot: ROOT_DIR,
|
||||
});
|
||||
if (omittedUnsupportedLaneNames.length > 0 && !allowFrozenTargetScenarioOmissions) {
|
||||
throw new Error(
|
||||
|
||||
@@ -168,6 +168,7 @@ describe("scripts/test-docker-all scheduler", () => {
|
||||
const scriptsDir = path.join(root, "scripts");
|
||||
const libDir = path.join(scriptsDir, "lib");
|
||||
mkdirSync(libDir, { recursive: true });
|
||||
copyFileSync("package.json", path.join(root, "package.json"));
|
||||
copyFileSync("scripts/test-docker-all.mjs", path.join(scriptsDir, "test-docker-all.mjs"));
|
||||
for (const fileName of ["docker-e2e-plan.mjs", "docker-e2e-scenarios.mjs", "sleep.mjs"]) {
|
||||
copyFileSync(path.join("scripts/lib", fileName), path.join(libDir, fileName));
|
||||
|
||||
@@ -54,6 +54,12 @@ function planFor(
|
||||
}).plan;
|
||||
}
|
||||
|
||||
function writeCandidatePackage(scripts: Record<string, string>): string {
|
||||
const root = tempDirs.make("openclaw-docker-plan-candidate-");
|
||||
writeFileSync(join(root, "package.json"), JSON.stringify({ scripts }));
|
||||
return root;
|
||||
}
|
||||
|
||||
function requireFirstLane(plan: ReturnType<typeof planFor>) {
|
||||
const [lane] = plan.lanes;
|
||||
if (!lane) {
|
||||
@@ -120,6 +126,47 @@ function bundledPluginSweepLane(index: number): ReturnType<typeof summarizeLane>
|
||||
}
|
||||
|
||||
describe("scripts/lib/docker-e2e-plan", () => {
|
||||
it("omits a package-script lane unavailable from the candidate", () => {
|
||||
const plan = planFor({
|
||||
candidatePackageRoot: writeCandidatePackage({}),
|
||||
selectedLaneNames: ["update-run-package-self-upgrade"],
|
||||
});
|
||||
|
||||
expect(plan.lanes).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps a package-script lane available from the candidate", () => {
|
||||
const plan = planFor({
|
||||
candidatePackageRoot: writeCandidatePackage({
|
||||
"test:docker:update-run-package-self-upgrade": "node test.mjs",
|
||||
}),
|
||||
selectedLaneNames: ["update-run-package-self-upgrade"],
|
||||
});
|
||||
|
||||
expect(plan.lanes.map((lane) => lane.name)).toEqual(["update-run-package-self-upgrade"]);
|
||||
});
|
||||
|
||||
it("fails when the selected candidate package manifest is missing", () => {
|
||||
expect(() =>
|
||||
planFor({
|
||||
candidatePackageRoot: tempDirs.make("openclaw-docker-plan-missing-package-"),
|
||||
selectedLaneNames: ["update-run-package-self-upgrade"],
|
||||
}),
|
||||
).toThrow(/package\.json/);
|
||||
});
|
||||
|
||||
it("fails when the selected candidate package manifest is malformed", () => {
|
||||
const root = tempDirs.make("openclaw-docker-plan-malformed-package-");
|
||||
writeFileSync(join(root, "package.json"), "{");
|
||||
|
||||
expect(() =>
|
||||
planFor({
|
||||
candidatePackageRoot: root,
|
||||
selectedLaneNames: ["update-run-package-self-upgrade"],
|
||||
}),
|
||||
).toThrow(SyntaxError);
|
||||
});
|
||||
|
||||
it("finds a named lane through the expanded catalog", () => {
|
||||
expect(findLaneByName("plugin-binding-command-escape")?.name).toBe(
|
||||
"plugin-binding-command-escape",
|
||||
|
||||
Reference in New Issue
Block a user