diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index 37ab6538e71..08eaf802ad2 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -493,6 +493,7 @@ jobs: export OPENCLAW_DOCKER_ALL_INCLUDE_OPENWEBUI="${INCLUDE_OPENWEBUI}" export OPENCLAW_DOCKER_ALL_LOG_DIR=".artifacts/docker-tests/release-${DOCKER_E2E_CHUNK}" export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/release-${DOCKER_E2E_CHUNK}-timings.json" + export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)" pnpm test:docker:all @@ -616,6 +617,7 @@ jobs: export OPENCLAW_DOCKER_ALL_INCLUDE_OPENWEBUI="${INCLUDE_OPENWEBUI}" export OPENCLAW_DOCKER_ALL_LOG_DIR=".artifacts/docker-tests/targeted" export OPENCLAW_DOCKER_ALL_TIMINGS_FILE=".artifacts/docker-tests/targeted-timings.json" + export OPENCLAW_DOCKER_ALL_PNPM_COMMAND="$(command -v pnpm)" if [[ "${{ steps.plan.outputs.needs_live_image }}" == "1" ]]; then pnpm test:docker:live-build fi diff --git a/scripts/test-docker-all.mjs b/scripts/test-docker-all.mjs index 790d3faba91..c4870924bba 100644 --- a/scripts/test-docker-all.mjs +++ b/scripts/test-docker-all.mjs @@ -197,12 +197,38 @@ function buildLaneRerunCommand(name, baseEnv) { ["OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE", baseEnv.OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE], ["OPENCLAW_CURRENT_PACKAGE_TGZ", baseEnv.OPENCLAW_CURRENT_PACKAGE_TGZ], ]; + if (baseEnv.OPENCLAW_DOCKER_ALL_PNPM_COMMAND) { + env.push(["OPENCLAW_DOCKER_ALL_PNPM_COMMAND", baseEnv.OPENCLAW_DOCKER_ALL_PNPM_COMMAND]); + } return `${env .filter(([, value]) => value !== undefined && value !== "") .map(([key, value]) => `${key}=${shellQuote(value)}`) .join(" ")} pnpm test:docker:all`; } +function withResolvedPnpmCommand(command, env) { + const pnpmCommand = env.OPENCLAW_DOCKER_ALL_PNPM_COMMAND?.trim(); + if (!pnpmCommand) { + return command; + } + return command.replace(/(^|\s)pnpm(?=\s)/g, `$1${shellQuote(pnpmCommand)}`); +} + +function timingSeconds(timingStore, poolLane) { + const fromStore = timingStore?.lanes?.[poolLane.name]?.durationSeconds; + if (typeof fromStore === "number" && Number.isFinite(fromStore) && fromStore > 0) { + return fromStore; + } + return poolLane.estimateSeconds ?? 0; +} + +function orderLanes(poolLanes, timingStore) { + return poolLanes + .map((poolLane, index) => ({ index, poolLane, seconds: timingSeconds(timingStore, poolLane) })) + .toSorted((a, b) => b.seconds - a.seconds || a.index - b.index) + .map(({ poolLane }) => poolLane); +} + async function loadTimingStore(file, enabled) { if (!enabled) { return { enabled: false, file, lanes: {}, version: 1 }; @@ -611,10 +637,11 @@ function laneEnv(poolLane, baseEnv, logDir, cacheKey) { } async function runLane(lane, baseEnv, logDir, fallbackTimeoutMs) { - const { command, name } = lane; + const { name } = lane; const timeoutMs = lane.timeoutMs ?? fallbackTimeoutMs; const logFile = path.join(logDir, `${name}.log`); const env = laneEnv(lane, baseEnv, logDir, lane.cacheKey); + const command = withResolvedPnpmCommand(lane.command, env); await mkdir(env.OPENCLAW_DOCKER_CLI_TOOLS_DIR, { recursive: true }); await mkdir(env.OPENCLAW_DOCKER_CACHE_HOME_DIR, { recursive: true }); await fs.promises.writeFile( diff --git a/test/scripts/docker-build-helper.test.ts b/test/scripts/docker-build-helper.test.ts index 28f36e9388b..c7fcb204063 100644 --- a/test/scripts/docker-build-helper.test.ts +++ b/test/scripts/docker-build-helper.test.ts @@ -44,5 +44,7 @@ describe("docker build helper", () => { expect(scheduler).toContain("env.npm_execpath ? path.dirname(env.npm_execpath)"); expect(scheduler).toContain("path.dirname(process.execPath)"); expect(scheduler).toContain("env.PATH = [...new Set(pathEntries)].join(path.delimiter)"); + expect(scheduler).toContain("withResolvedPnpmCommand"); + expect(scheduler).toContain("OPENCLAW_DOCKER_ALL_PNPM_COMMAND"); }); });