mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:20:43 +00:00
test(e2e): expand published upgrade survivor baselines
This commit is contained in:
@@ -57,6 +57,74 @@ export function parseLaneSelection(raw) {
|
||||
];
|
||||
}
|
||||
|
||||
function shellQuote(value) {
|
||||
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
||||
}
|
||||
|
||||
function sanitizeLaneNameSuffix(value) {
|
||||
return (
|
||||
String(value)
|
||||
.replace(/^openclaw@/u, "")
|
||||
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "") || "baseline"
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeUpgradeSurvivorBaselineSpec(raw) {
|
||||
const value = String(raw ?? "").trim();
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const spec = value.startsWith("openclaw@") ? value : `openclaw@${value}`;
|
||||
if (
|
||||
!/^openclaw@(?:beta|latest|[0-9]{4}\.[0-9]+\.[0-9]+(?:-(?:[0-9]+|beta\.[0-9]+))?)$/u.test(spec)
|
||||
) {
|
||||
throw new Error(
|
||||
`invalid published upgrade survivor baseline: ${JSON.stringify(
|
||||
value,
|
||||
)}. Expected openclaw@latest, openclaw@beta, or openclaw@YYYY.M.D.`,
|
||||
);
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
export function parseUpgradeSurvivorBaselineSpecs(raw) {
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
...new Set(
|
||||
String(raw)
|
||||
.split(/[,\s]+/u)
|
||||
.map(normalizeUpgradeSurvivorBaselineSpec)
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
export function expandUpgradeSurvivorBaselineLanes(poolLanes, rawBaselineSpecs) {
|
||||
const baselineSpecs = parseUpgradeSurvivorBaselineSpecs(rawBaselineSpecs);
|
||||
if (baselineSpecs.length === 0) {
|
||||
return poolLanes;
|
||||
}
|
||||
return poolLanes.flatMap((poolLane) => {
|
||||
if (poolLane.name !== "published-upgrade-survivor") {
|
||||
return [poolLane];
|
||||
}
|
||||
return baselineSpecs.map((baselineSpec) => {
|
||||
const suffix = sanitizeLaneNameSuffix(baselineSpec);
|
||||
const name = `${poolLane.name}-${suffix}`;
|
||||
return Object.assign({}, poolLane, {
|
||||
cacheKey: poolLane.cacheKey ? `${poolLane.cacheKey}-${suffix}` : name,
|
||||
command: `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC=${shellQuote(
|
||||
baselineSpec,
|
||||
)} ${poolLane.command}`,
|
||||
name,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function dedupeLanes(poolLanes) {
|
||||
const byName = new Map();
|
||||
for (const poolLane of poolLanes) {
|
||||
@@ -141,11 +209,12 @@ export function lanesNeedOpenClawPackage(poolLanes) {
|
||||
}
|
||||
|
||||
export function findLaneByName(name) {
|
||||
return dedupeLanes([
|
||||
...allReleasePathLanes({ includeOpenWebUI: true }),
|
||||
...mainLanes,
|
||||
...tailLanes,
|
||||
]).find((poolLane) => poolLane.name === name);
|
||||
return dedupeLanes(
|
||||
expandUpgradeSurvivorBaselineLanes(
|
||||
[...allReleasePathLanes({ includeOpenWebUI: true }), ...mainLanes, ...tailLanes],
|
||||
process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS,
|
||||
),
|
||||
).find((poolLane) => poolLane.name === name);
|
||||
}
|
||||
|
||||
export function laneCredentialRequirements(poolLane) {
|
||||
@@ -207,25 +276,45 @@ export function buildPlanJson(params) {
|
||||
export function resolveDockerE2ePlan(options) {
|
||||
const retriedMainLanes = applyLiveRetries(mainLanes, options.liveRetries);
|
||||
const retriedTailLanes = applyLiveRetries(tailLanes, options.liveRetries);
|
||||
const upgradeSurvivorBaselines = options.upgradeSurvivorBaselines ?? "";
|
||||
const unexpandedSelectableLanes = dedupeLanes([
|
||||
...allReleasePathLanes({ includeOpenWebUI: options.includeOpenWebUI }),
|
||||
...retriedMainLanes,
|
||||
...retriedTailLanes,
|
||||
]);
|
||||
const selectableLanes = dedupeLanes(
|
||||
expandUpgradeSurvivorBaselineLanes(unexpandedSelectableLanes, upgradeSurvivorBaselines),
|
||||
);
|
||||
const releaseLanes =
|
||||
options.selectedLaneNames.length === 0 && options.profile === RELEASE_PATH_PROFILE
|
||||
? options.planReleaseAll
|
||||
? allReleasePathLanes({ includeOpenWebUI: options.includeOpenWebUI })
|
||||
: releasePathChunkLanes(options.releaseChunk, {
|
||||
includeOpenWebUI: options.includeOpenWebUI,
|
||||
})
|
||||
? expandUpgradeSurvivorBaselineLanes(
|
||||
allReleasePathLanes({ includeOpenWebUI: options.includeOpenWebUI }),
|
||||
upgradeSurvivorBaselines,
|
||||
)
|
||||
: expandUpgradeSurvivorBaselineLanes(
|
||||
releasePathChunkLanes(options.releaseChunk, {
|
||||
includeOpenWebUI: options.includeOpenWebUI,
|
||||
}),
|
||||
upgradeSurvivorBaselines,
|
||||
)
|
||||
: undefined;
|
||||
const selectedLanes =
|
||||
options.selectedLaneNames.length > 0
|
||||
? selectNamedLanes(
|
||||
dedupeLanes([
|
||||
...allReleasePathLanes({ includeOpenWebUI: options.includeOpenWebUI }),
|
||||
...retriedMainLanes,
|
||||
...retriedTailLanes,
|
||||
]),
|
||||
options.selectedLaneNames,
|
||||
"OPENCLAW_DOCKER_ALL_LANES",
|
||||
)
|
||||
? options.selectedLaneNames.flatMap((selectedName) => {
|
||||
const expandedLane = selectableLanes.find((poolLane) => poolLane.name === selectedName);
|
||||
if (expandedLane) {
|
||||
return [expandedLane];
|
||||
}
|
||||
const unexpandedLane = unexpandedSelectableLanes.find(
|
||||
(poolLane) => poolLane.name === selectedName,
|
||||
);
|
||||
if (unexpandedLane) {
|
||||
return expandUpgradeSurvivorBaselineLanes([unexpandedLane], upgradeSurvivorBaselines);
|
||||
}
|
||||
selectNamedLanes(selectableLanes, [selectedName], "OPENCLAW_DOCKER_ALL_LANES");
|
||||
return [];
|
||||
})
|
||||
: undefined;
|
||||
const configuredLanes = selectedLanes
|
||||
? selectedLanes
|
||||
|
||||
115
scripts/resolve-upgrade-survivor-baselines.mjs
Normal file
115
scripts/resolve-upgrade-survivor-baselines.mjs
Normal file
@@ -0,0 +1,115 @@
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { normalizeUpgradeSurvivorBaselineSpec } from "./lib/docker-e2e-plan.mjs";
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = new Map();
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (!arg.startsWith("--")) {
|
||||
throw new Error(`unexpected argument: ${arg}`);
|
||||
}
|
||||
const key = arg.slice(2);
|
||||
const value = argv[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) {
|
||||
throw new Error(`missing value for --${key}`);
|
||||
}
|
||||
args.set(key, value);
|
||||
index += 1;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function splitSpecs(raw) {
|
||||
return String(raw ?? "")
|
||||
.split(/[,\s]+/u)
|
||||
.map((token) => token.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function dedupeSpecs(specs) {
|
||||
return [...new Set(specs.map(normalizeUpgradeSurvivorBaselineSpec).filter(Boolean))];
|
||||
}
|
||||
|
||||
function stableVersionFromTag(tagName) {
|
||||
const version = String(tagName ?? "").replace(/^v/u, "");
|
||||
if (!/^[0-9]{4}\.[0-9]+\.[0-9]+(?:-[0-9]+)?$/u.test(version)) {
|
||||
return undefined;
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
function readStableReleases(file) {
|
||||
const ansiEscape = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g");
|
||||
const raw = readFileSync(file, "utf8").replace(ansiEscape, "");
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error(`release list must be a JSON array: ${file}`);
|
||||
}
|
||||
return parsed
|
||||
.filter((release) => !release.isPrerelease)
|
||||
.map((release) => ({
|
||||
publishedAt: release.publishedAt,
|
||||
version: stableVersionFromTag(release.tagName),
|
||||
}))
|
||||
.filter((release) => release.version && release.publishedAt)
|
||||
.toSorted((a, b) => String(b.publishedAt).localeCompare(String(a.publishedAt)));
|
||||
}
|
||||
|
||||
export function resolveReleaseHistory(args) {
|
||||
const releasesJson = args.get("releases-json");
|
||||
if (!releasesJson) {
|
||||
throw new Error("--releases-json is required when requested baselines include release-history");
|
||||
}
|
||||
const historyCount = Number.parseInt(args.get("history-count") ?? "6", 10);
|
||||
if (!Number.isInteger(historyCount) || historyCount < 1) {
|
||||
throw new Error("--history-count must be a positive integer");
|
||||
}
|
||||
const includeVersion = args.get("include-version") ?? "2026.4.23";
|
||||
const preDate = args.get("pre-date") ?? "2026-03-15T00:00:00Z";
|
||||
const releases = readStableReleases(releasesJson);
|
||||
const versions = releases.slice(0, historyCount).map((release) => release.version);
|
||||
const exact = releases.find((release) => release.version === includeVersion);
|
||||
if (exact) {
|
||||
versions.push(exact.version);
|
||||
}
|
||||
const preDateRelease = releases.find(
|
||||
(release) => new Date(release.publishedAt).getTime() < new Date(preDate).getTime(),
|
||||
);
|
||||
if (preDateRelease) {
|
||||
versions.push(preDateRelease.version);
|
||||
}
|
||||
return dedupeSpecs(versions);
|
||||
}
|
||||
|
||||
export function resolveBaselines(args) {
|
||||
const requested = args.get("requested") ?? "";
|
||||
const fallback = args.get("fallback") ?? "openclaw@latest";
|
||||
const requestedTokens = splitSpecs(requested);
|
||||
if (requestedTokens.length === 0) {
|
||||
return dedupeSpecs([fallback]);
|
||||
}
|
||||
const exactTokens = [];
|
||||
const resolved = [];
|
||||
for (const token of requestedTokens) {
|
||||
if (token === "release-history") {
|
||||
resolved.push(...resolveReleaseHistory(args));
|
||||
} else {
|
||||
exactTokens.push(token);
|
||||
}
|
||||
}
|
||||
return dedupeSpecs([...exactTokens, ...resolved]);
|
||||
}
|
||||
|
||||
const isMain = process.argv[1] ? fileURLToPath(import.meta.url) === process.argv[1] : false;
|
||||
|
||||
if (isMain) {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const baselines = resolveBaselines(args).join(" ");
|
||||
process.stdout.write(`${baselines}\n`);
|
||||
|
||||
const githubOutput = args.get("github-output");
|
||||
if (githubOutput) {
|
||||
writeFileSync(githubOutput, `baselines=${baselines}\n`, { flag: "a" });
|
||||
}
|
||||
}
|
||||
@@ -228,6 +228,12 @@ function githubWorkflowRerunCommand(laneNames, ref) {
|
||||
`published_upgrade_survivor_baseline=${shellQuote(process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC)}`,
|
||||
);
|
||||
}
|
||||
if (process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS) {
|
||||
fields.push(
|
||||
"-f",
|
||||
`published_upgrade_survivor_baselines=${shellQuote(process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS)}`,
|
||||
);
|
||||
}
|
||||
if (process.env.OPENCLAW_DOCKER_E2E_BARE_IMAGE) {
|
||||
fields.push(
|
||||
"-f",
|
||||
@@ -257,6 +263,7 @@ function buildLaneRerunCommand(name, baseEnv) {
|
||||
["OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE", baseEnv.OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE],
|
||||
["OPENCLAW_CURRENT_PACKAGE_TGZ", baseEnv.OPENCLAW_CURRENT_PACKAGE_TGZ],
|
||||
["OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC", baseEnv.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC],
|
||||
["OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS", baseEnv.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS],
|
||||
];
|
||||
if (baseEnv.OPENCLAW_DOCKER_ALL_PNPM_COMMAND) {
|
||||
env.push(["OPENCLAW_DOCKER_ALL_PNPM_COMMAND", baseEnv.OPENCLAW_DOCKER_ALL_PNPM_COMMAND]);
|
||||
@@ -1125,6 +1132,7 @@ async function main() {
|
||||
releaseChunk,
|
||||
selectedLaneNames,
|
||||
timingStore,
|
||||
upgradeSurvivorBaselines: process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS,
|
||||
});
|
||||
|
||||
if (planJson) {
|
||||
|
||||
Reference in New Issue
Block a user