From ec8f957f2e7ca3125824d610a2ad5a53446d6631 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Jul 2026 00:53:23 -0400 Subject: [PATCH] fix(release): test candidate external channel plugins (#112899) --- .../e2e/npm-onboard-channel-agent-docker.sh | 42 +++++++++++++++++++ scripts/lib/docker-e2e-scenarios.mjs | 18 ++++++++ scripts/lib/plugin-prerelease-test-plan.mjs | 4 +- .../plugin-prerelease-test-plan.test.ts | 17 +++++++- 4 files changed, 77 insertions(+), 4 deletions(-) diff --git a/scripts/e2e/npm-onboard-channel-agent-docker.sh b/scripts/e2e/npm-onboard-channel-agent-docker.sh index 16a92d9f5f78..5ab1600cc3d0 100644 --- a/scripts/e2e/npm-onboard-channel-agent-docker.sh +++ b/scripts/e2e/npm-onboard-channel-agent-docker.sh @@ -4,6 +4,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SOURCE_ROOT="$(cd "${OPENCLAW_NPM_ONBOARD_SOURCE_ROOT:-${OPENCLAW_LIVE_DOCKER_REPO_ROOT:-$ROOT_DIR}}" && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" @@ -12,6 +13,7 @@ DOCKER_TARGET="${OPENCLAW_NPM_ONBOARD_DOCKER_TARGET:-bare}" HOST_BUILD="${OPENCLAW_NPM_ONBOARD_HOST_BUILD:-1}" PACKAGE_TGZ="${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" CHANNEL="${OPENCLAW_NPM_ONBOARD_CHANNEL:-telegram}" +USE_SOURCE_PLUGIN_PACKAGE="${OPENCLAW_NPM_ONBOARD_USE_SOURCE_PLUGIN_PACKAGE:-0}" JSON_ARTIFACT_MAX_BYTES="$( docker_e2e_read_positive_int_env OPENCLAW_NPM_ONBOARD_JSON_ARTIFACT_MAX_BYTES 1048576 )" @@ -19,6 +21,8 @@ STATUS_TEXT_MAX_BYTES="$( docker_e2e_read_positive_int_env OPENCLAW_NPM_ONBOARD_STATUS_TEXT_MAX_BYTES 1048576 )" run_log="" +plugin_pack_dir="" +plugin_package_args=() cleanup() { if [ -n "${PACKAGE_TGZ:-}" ]; then @@ -27,6 +31,9 @@ cleanup() { if [ -n "${run_log:-}" ]; then rm -f "$run_log" fi + if [ -n "${plugin_pack_dir:-}" ]; then + rm -rf "$plugin_pack_dir" + fi } trap cleanup EXIT @@ -54,6 +61,40 @@ prepare_package_tgz() { prepare_package_tgz +prepare_source_plugin_package() { + if [ "$USE_SOURCE_PLUGIN_PACKAGE" != "1" ] || [ "$CHANNEL" = "telegram" ]; then + return 0 + fi + + local package_dir="extensions/$CHANNEL" + if [ ! -f "$SOURCE_ROOT/$package_dir/package.json" ]; then + echo "Missing source plugin package for $CHANNEL: $package_dir" >&2 + exit 1 + fi + + plugin_pack_dir="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-npm-onboard-plugin.XXXXXX")" + ( + cd "$SOURCE_ROOT" + OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR="$plugin_pack_dir" \ + bash scripts/plugin-npm-publish.sh --pack "$package_dir" + ) + + local archives=("$plugin_pack_dir"/*.tgz) + if [ "${#archives[@]}" -ne 1 ] || [ ! -f "${archives[0]}" ]; then + echo "Expected exactly one packed source plugin for $CHANNEL" >&2 + exit 1 + fi + + local container_package="/tmp/openclaw-channel-plugin.tgz" + plugin_package_args=( + -v "${archives[0]}:$container_package:ro" + -e OPENCLAW_ALLOW_PLUGIN_INSTALL_OVERRIDES=1 + -e "OPENCLAW_PLUGIN_INSTALL_OVERRIDES={\"$CHANNEL\":\"npm-pack:$container_package\"}" + ) +} + +prepare_source_plugin_package + docker_e2e_package_mount_args "$PACKAGE_TGZ" run_log="$(docker_e2e_run_log npm-onboard-channel-agent)" OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 npm-onboard-channel-agent empty)" @@ -66,6 +107,7 @@ if ! docker_e2e_run_with_harness \ -e "OPENCLAW_NPM_ONBOARD_STATUS_TEXT_MAX_BYTES=$STATUS_TEXT_MAX_BYTES" \ -e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \ "${DOCKER_E2E_PACKAGE_ARGS[@]}" \ + "${plugin_package_args[@]}" \ -i "$IMAGE_NAME" bash -s >"$run_log" 2>&1 <<'EOF'; then set -Eeuo pipefail diff --git a/scripts/lib/docker-e2e-scenarios.mjs b/scripts/lib/docker-e2e-scenarios.mjs index a824c4a44eec..85f1338412c7 100644 --- a/scripts/lib/docker-e2e-scenarios.mjs +++ b/scripts/lib/docker-e2e-scenarios.mjs @@ -382,6 +382,24 @@ export const mainLanes = [ "OPENCLAW_NPM_ONBOARD_CHANNEL=slack OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:npm-onboard-channel-agent", { resources: ["service"], stateScenario: "empty", weight: 3 }, ), + // Prerelease validation must pair frozen core bytes with matching target plugin bytes. + // Keep the registry-backed lanes above unchanged for published-package proof. + npmLane( + "npm-onboard-discord-candidate-channel-agent", + liveDockerScriptCommand( + "e2e/npm-onboard-channel-agent-docker.sh", + "OPENCLAW_NPM_ONBOARD_CHANNEL=discord OPENCLAW_NPM_ONBOARD_USE_SOURCE_PLUGIN_PACKAGE=1", + ), + { resources: ["service"], stateScenario: "empty", weight: 3 }, + ), + npmLane( + "npm-onboard-slack-candidate-channel-agent", + liveDockerScriptCommand( + "e2e/npm-onboard-channel-agent-docker.sh", + "OPENCLAW_NPM_ONBOARD_CHANNEL=slack OPENCLAW_NPM_ONBOARD_USE_SOURCE_PLUGIN_PACKAGE=1", + ), + { resources: ["service"], stateScenario: "empty", weight: 3 }, + ), npmLane( "release-user-journey", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:release-user-journey", diff --git a/scripts/lib/plugin-prerelease-test-plan.mjs b/scripts/lib/plugin-prerelease-test-plan.mjs index 451a3b1e294c..fb8a49ea3dd8 100644 --- a/scripts/lib/plugin-prerelease-test-plan.mjs +++ b/scripts/lib/plugin-prerelease-test-plan.mjs @@ -27,7 +27,7 @@ const pluginPrereleaseDockerLanes = Object.freeze([ surfaces: ["package-artifact", "gateway-bootstrap", "status-diagnostics"], }, { - lane: "npm-onboard-discord-channel-agent", + lane: "npm-onboard-discord-candidate-channel-agent", surfaces: [ "package-artifact", "external-plugins", @@ -37,7 +37,7 @@ const pluginPrereleaseDockerLanes = Object.freeze([ ], }, { - lane: "npm-onboard-slack-channel-agent", + lane: "npm-onboard-slack-candidate-channel-agent", surfaces: ["package-artifact", "gateway-bootstrap", "status-diagnostics"], }, { diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index 663b775eaecd..4edf2b89fde4 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -57,11 +57,15 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { it("runs the package and Docker product lanes through the existing scheduler", () => { const plan = createPluginPrereleaseTestPlan(); + const channelLaneScript = readFileSync( + "scripts/e2e/npm-onboard-channel-agent-docker.sh", + "utf8", + ); expect(plan.dockerLanes).toEqual([ "npm-onboard-channel-agent", - "npm-onboard-discord-channel-agent", - "npm-onboard-slack-channel-agent", + "npm-onboard-discord-candidate-channel-agent", + "npm-onboard-slack-candidate-channel-agent", "doctor-switch", "update-channel-switch", "plugins-offline", @@ -82,6 +86,15 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => { for (const lane of plan.dockerLanes) { expect(getDockerLane(lane).name).toBe(lane); } + expect(channelLaneScript).toContain("OPENCLAW_NPM_ONBOARD_USE_SOURCE_PLUGIN_PACKAGE"); + expect(channelLaneScript).toContain("bash scripts/plugin-npm-publish.sh --pack"); + expect(channelLaneScript).toContain("OPENCLAW_ALLOW_PLUGIN_INSTALL_OVERRIDES=1"); + expect(channelLaneScript).toContain("npm-pack:$container_package"); + const candidateLane = getDockerLane("npm-onboard-discord-candidate-channel-agent"); + expect(candidateLane.command).toContain("OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR"); + expect(candidateLane.command).toContain( + 'OPENCLAW_LIVE_DOCKER_REPO_ROOT="${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$PWD}"', + ); }); it("keeps live-ish coverage outside provider-backed Docker lanes", () => {