From 748daa4857125bcfdeda5a65ad0c5e92876c10db Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 05:45:27 +0100 Subject: [PATCH] ci: make package acceptance legacy-safe --- .github/workflows/package-acceptance.yml | 2 +- docs/ci.md | 7 ++++-- docs/reference/RELEASING.md | 15 ++++++------ scripts/e2e/doctor-install-switch-docker.sh | 6 ++++- scripts/e2e/plugins-docker.sh | 9 ++++--- scripts/e2e/update-channel-switch-docker.sh | 24 ++++++++++++++++++- scripts/lib/docker-e2e-scenarios.mjs | 8 +++++++ test/scripts/docker-build-helper.test.ts | 7 ++++++ .../package-acceptance-workflow.test.ts | 1 + 9 files changed, 64 insertions(+), 15 deletions(-) diff --git a/.github/workflows/package-acceptance.yml b/.github/workflows/package-acceptance.yml index 1db8a986d38..839bf58339a 100644 --- a/.github/workflows/package-acceptance.yml +++ b/.github/workflows/package-acceptance.yml @@ -338,7 +338,7 @@ jobs: docker_lanes="npm-onboard-channel-agent gateway-network config-reload" ;; package) - docker_lanes="install-e2e npm-onboard-channel-agent doctor-switch update-channel-switch bundled-channel-deps plugins plugin-update" + docker_lanes="install-e2e npm-onboard-channel-agent doctor-switch update-channel-switch bundled-channel-deps plugins-offline plugin-update" ;; product) docker_lanes="install-e2e npm-onboard-channel-agent doctor-switch update-channel-switch bundled-channel-deps plugins plugin-update mcp-channels cron-mcp-cleanup openai-web-search-minimal openwebui" diff --git a/docs/ci.md b/docs/ci.md index bdd4afbb2c5..a9d1bf1ceca 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -23,7 +23,9 @@ published npm spec, a trusted `package_ref` built with the selected from another GitHub Actions run, uploads it as `package-under-test`, then reuses the Docker release/E2E scheduler with that tarball instead of repacking the workflow checkout. Profiles cover smoke, package, product, full, and custom -Docker lane selections. The optional Telegram lane reuses the +Docker lane selections. The `package` profile uses offline plugin coverage so +published-package validation is not gated on live ClawHub availability. The +optional Telegram lane reuses the `package-under-test` artifact in the `NPM Telegram Beta E2E` workflow, with the published npm spec path kept for standalone dispatches. @@ -77,7 +79,8 @@ Profiles map to Docker coverage: - `smoke`: `npm-onboard-channel-agent`, `gateway-network`, `config-reload` - `package`: `install-e2e`, `npm-onboard-channel-agent`, `doctor-switch`, - `update-channel-switch`, `bundled-channel-deps`, `plugins`, `plugin-update` + `update-channel-switch`, `bundled-channel-deps`, `plugins-offline`, + `plugin-update` - `product`: `package` plus `mcp-channels`, `cron-mcp-cleanup`, `openai-web-search-minimal`, `openwebui` - `full`: full Docker release-path chunks with OpenWebUI diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index 9d4354bfc16..b65c6a4fa58 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -118,7 +118,7 @@ the maintainer-only release runbook. Example: `gh workflow run package-acceptance.yml --ref main -f workflow_ref=main -f source=npm -f package_spec=openclaw@beta -f suite_profile=product -f telegram_mode=mock-openai` Common profiles: - `smoke`: install/channel/agent, gateway network, and config reload lanes - - `package`: package/update/plugin lanes without OpenWebUI + - `package`: package/update/plugin lanes without OpenWebUI or live ClawHub - `product`: package profile plus MCP channels, cron/subagent cleanup, OpenAI web search, and OpenWebUI - `full`: Docker release-path chunks with OpenWebUI @@ -371,11 +371,12 @@ Supported candidate sources: `OpenClaw Release Checks` runs Package Acceptance with `source=ref`, `package_ref=`, `suite_profile=package`, and `telegram_mode=mock-openai`. That profile covers install, update, plugin -package contracts, and Telegram package QA against the same resolved tarball, -and is the GitHub-native replacement for most of the package/update coverage -that previously required Parallels. Cross-OS release checks still matter for -OS-specific onboarding, installer, and platform behavior, but package/update -product validation should prefer Package Acceptance. +package contracts through offline plugin fixtures, and Telegram package QA +against the same resolved tarball. It is the GitHub-native replacement for most +of the package/update coverage that previously required Parallels. Cross-OS +release checks still matter for OS-specific onboarding, installer, and platform +behavior, but package/update product validation should prefer Package +Acceptance. Use broader Package Acceptance profiles when the release question is about an actual installable package: @@ -393,7 +394,7 @@ Common package profiles: - `smoke`: quick package install/channel/agent, gateway network, and config reload lanes -- `package`: install/update/plugin package contracts; this is the release-check +- `package`: install/update/plugin package contracts without live ClawHub; this is the release-check default - `product`: `package` plus MCP channels, cron/subagent cleanup, OpenAI web search, and OpenWebUI diff --git a/scripts/e2e/doctor-install-switch-docker.sh b/scripts/e2e/doctor-install-switch-docker.sh index c0a3aa8e19f..0d5f8d48236 100755 --- a/scripts/e2e/doctor-install-switch-docker.sh +++ b/scripts/e2e/doctor-install-switch-docker.sh @@ -312,5 +312,9 @@ WRAPPER assert_entrypoint "$unit_path" "$npm_entry" } - run_wrapper_flow + if "$npm_bin" gateway install --help 2>&1 | grep -q -- "--wrapper"; then + run_wrapper_flow + else + echo "Skipping wrapper persistence; package gateway install does not support --wrapper." + fi ' diff --git a/scripts/e2e/plugins-docker.sh b/scripts/e2e/plugins-docker.sh index 238185fa3a3..95d01e72ed8 100755 --- a/scripts/e2e/plugins-docker.sh +++ b/scripts/e2e/plugins-docker.sh @@ -560,8 +560,9 @@ const path = require("node:path"); const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json"); const index = JSON.parse(fs.readFileSync(indexPath, "utf8")); +const installRecords = index.installRecords ?? index.records ?? {}; for (const id of ["marketplace-shortcut", "marketplace-direct"]) { - const record = index.installRecords?.[id]; + const record = installRecords[id]; if (!record) throw new Error(`missing install record for ${id}`); if (record.source !== "marketplace") { throw new Error(`unexpected source for ${id}: ${record.source}`); @@ -845,7 +846,8 @@ if (inspect.plugin?.id !== pluginId) { const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json"); const index = JSON.parse(fs.readFileSync(indexPath, "utf8")); -const record = index.installRecords?.[pluginId]; +const installRecords = index.installRecords ?? index.records ?? {}; +const record = installRecords[pluginId]; if (!record) throw new Error(`missing ClawHub install record for ${pluginId}`); if (record.source !== "clawhub") { throw new Error(`unexpected ClawHub install source for ${pluginId}: ${record.source}`); @@ -890,7 +892,8 @@ if ((list.plugins || []).some((entry) => entry.id === pluginId)) { const indexPath = path.join(process.env.HOME, ".openclaw", "plugins", "installs.json"); const index = fs.existsSync(indexPath) ? JSON.parse(fs.readFileSync(indexPath, "utf8")) : {}; -if (index.installRecords?.[pluginId]) { +const installRecords = index.installRecords ?? index.records ?? {}; +if (installRecords[pluginId]) { throw new Error(`ClawHub install record still present after uninstall: ${pluginId}`); } diff --git a/scripts/e2e/update-channel-switch-docker.sh b/scripts/e2e/update-channel-switch-docker.sh index 97b1e56e910..0dadcf6a1ad 100755 --- a/scripts/e2e/update-channel-switch-docker.sh +++ b/scripts/e2e/update-channel-switch-docker.sh @@ -45,11 +45,33 @@ tar -xzf "$package_tgz" -C "$git_root" --strip-components=1 # absent from the trimmed tarball install; that should not block update preflight. node - <<'"'"'NODE'"'"' 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 fixtureUiBuildSource = `const fs=require("node:fs");fs.mkdirSync("dist/control-ui",{recursive:true});fs.writeFileSync("dist/control-ui/index.html","fixture\\n")`; const fixtureUiBuildCommand = `node -e ${JSON.stringify(fixtureUiBuildSource)}`; -packageJson.pnpm = { ...packageJson.pnpm, allowUnusedPatches: true }; +const nextPnpm = { ...packageJson.pnpm, allowUnusedPatches: true }; +const patchedDependencies = nextPnpm.patchedDependencies; +if ( + patchedDependencies && + typeof patchedDependencies === "object" && + !Array.isArray(patchedDependencies) +) { + const keptPatches = Object.fromEntries( + Object.entries(patchedDependencies).filter(([, patchFile]) => { + return ( + typeof patchFile === "string" && + fs.existsSync(path.resolve(path.dirname(packageJsonPath), patchFile)) + ); + }), + ); + if (Object.keys(keptPatches).length > 0) { + nextPnpm.patchedDependencies = keptPatches; + } else { + delete nextPnpm.patchedDependencies; + } +} +packageJson.pnpm = nextPnpm; packageJson.scripts = { ...packageJson.scripts, build: "node -e \"console.log(\\\"fixture build skipped\\\")\"", diff --git a/scripts/lib/docker-e2e-scenarios.mjs b/scripts/lib/docker-e2e-scenarios.mjs index d08982f2628..4c9345de4c9 100644 --- a/scripts/lib/docker-e2e-scenarios.mjs +++ b/scripts/lib/docker-e2e-scenarios.mjs @@ -215,6 +215,14 @@ export const mainLanes = [ resources: ["npm", "service"], weight: 6, }), + lane( + "plugins-offline", + "OPENCLAW_PLUGINS_E2E_CLAWHUB=0 OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugins", + { + resources: ["npm", "service"], + weight: 6, + }, + ), npmLane("plugin-update", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugin-update"), serviceLane("config-reload", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:config-reload"), ...bundledScenarioLanes, diff --git a/test/scripts/docker-build-helper.test.ts b/test/scripts/docker-build-helper.test.ts index 92f463ef61b..50d9ec3f732 100644 --- a/test/scripts/docker-build-helper.test.ts +++ b/test/scripts/docker-build-helper.test.ts @@ -60,6 +60,13 @@ describe("docker build helper", () => { ); }); + it("keeps package acceptance plugin coverage offline-capable", () => { + const scenarios = readFileSync(DOCKER_E2E_SCENARIOS_PATH, "utf8"); + + expect(scenarios).toContain('"plugins-offline"'); + expect(scenarios).toContain("OPENCLAW_PLUGINS_E2E_CLAWHUB=0"); + }); + it("passes installer tag env to bash, not curl", () => { const runner = readFileSync(INSTALL_E2E_RUNNER_PATH, "utf8"); diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 8afe59fbee7..488d409b139 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -40,6 +40,7 @@ describe("package acceptance workflow", () => { expect(workflow).toContain("suite_profile:"); expect(workflow).toContain("npm-onboard-channel-agent gateway-network config-reload"); expect(workflow).toContain("install-e2e npm-onboard-channel-agent doctor-switch"); + expect(workflow).toContain("plugins-offline plugin-update"); expect(workflow).toContain("include_release_path_suites=true"); expect(workflow).not.toContain("telegram_mode requires source=npm"); expect(workflow).toContain("uses: ./.github/workflows/npm-telegram-beta-e2e.yml");