From 09107e0b7f4103b71d66bcf8c727f778534464f9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 05:09:10 +0100 Subject: [PATCH] ci: let telegram e2e use package artifacts --- .github/workflows/npm-telegram-beta-e2e.yml | 60 ++++++++++++++-- .github/workflows/package-acceptance.yml | 12 ++-- scripts/e2e/npm-telegram-live-docker.sh | 68 +++++++++++++++---- scripts/e2e/npm-telegram-live-runner.ts | 12 ++-- test/scripts/npm-telegram-live.test.ts | 23 +++++-- .../package-acceptance-workflow.test.ts | 15 +++- 6 files changed, 151 insertions(+), 39 deletions(-) diff --git a/.github/workflows/npm-telegram-beta-e2e.yml b/.github/workflows/npm-telegram-beta-e2e.yml index 960abc15c81..b9baf3e81fe 100644 --- a/.github/workflows/npm-telegram-beta-e2e.yml +++ b/.github/workflows/npm-telegram-beta-e2e.yml @@ -4,10 +4,20 @@ on: workflow_dispatch: inputs: package_spec: - description: Published OpenClaw package spec to test + description: Published OpenClaw package spec to test when no artifact is supplied required: true default: openclaw@beta type: string + package_label: + description: Optional display label for an artifact-backed package candidate + required: false + default: "" + type: string + package_artifact_name: + description: Advanced package-under-test artifact name; leave blank for registry install + required: false + default: "" + type: string provider_mode: description: QA provider mode required: true @@ -23,9 +33,19 @@ on: workflow_call: inputs: package_spec: - description: Published OpenClaw package spec to test + description: Published OpenClaw package spec to test when no artifact is supplied required: true type: string + package_artifact_name: + description: Optional package-under-test artifact from the current workflow run + required: false + default: "" + type: string + package_label: + description: Optional display label for an artifact-backed package candidate + required: false + default: "" + type: string provider_mode: description: QA provider mode required: false @@ -58,7 +78,7 @@ env: jobs: run_npm_telegram_beta_e2e: - name: Run published npm Telegram E2E + name: Run package Telegram E2E runs-on: blacksmith-32vcpu-ubuntu-2404 timeout-minutes: 60 environment: qa-live-shared @@ -101,6 +121,7 @@ jobs: - name: Validate inputs and secrets env: PACKAGE_SPEC: ${{ inputs.package_spec }} + PACKAGE_ARTIFACT_NAME: ${{ inputs.package_artifact_name || '' }} PROVIDER_MODE: ${{ inputs.provider_mode }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }} @@ -109,9 +130,11 @@ jobs: run: | set -euo pipefail - if [[ ! "${PACKAGE_SPEC}" =~ ^openclaw@(beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-beta\.[1-9][0-9]*)?)$ ]]; then - echo "package_spec must be openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${PACKAGE_SPEC}" >&2 - exit 1 + if [[ -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then + if [[ ! "${PACKAGE_SPEC}" =~ ^openclaw@(beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-beta\.[1-9][0-9]*)?)$ ]]; then + echo "package_spec must be openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${PACKAGE_SPEC}" >&2 + exit 1 + fi fi case "${PROVIDER_MODE}" in mock-openai | live-frontier) ;; @@ -135,7 +158,14 @@ jobs: require_var OPENAI_API_KEY fi - - name: Run npm Telegram beta E2E + - name: Download package-under-test artifact + if: inputs.package_artifact_name != '' + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.package_artifact_name }} + path: .artifacts/telegram-package-under-test + + - name: Run package Telegram E2E id: run_lane shell: bash env: @@ -143,6 +173,7 @@ jobs: OPENCLAW_SKIP_DOCKER_BUILD: "1" OPENCLAW_DOCKER_E2E_IMAGE: openclaw-docker-e2e:local OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC: ${{ inputs.package_spec }} + OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL: ${{ inputs.package_label }} OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE: ${{ inputs.provider_mode }} OPENCLAW_NPM_TELEGRAM_CREDENTIAL_SOURCE: convex OPENCLAW_NPM_TELEGRAM_CREDENTIAL_ROLE: ci @@ -151,6 +182,7 @@ jobs: OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1" OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT: "1" INPUT_SCENARIO: ${{ inputs.scenario }} + PACKAGE_ARTIFACT_NAME: ${{ inputs.package_artifact_name || '' }} run: | set -euo pipefail @@ -158,6 +190,20 @@ jobs: echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT" export OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR="${output_dir}" + if [[ -n "${PACKAGE_ARTIFACT_NAME// }" ]]; then + mapfile -t package_tgzs < <(find .artifacts/telegram-package-under-test -type f -name "*.tgz" | sort) + if [[ "${#package_tgzs[@]}" -ne 1 ]]; then + echo "package artifact ${PACKAGE_ARTIFACT_NAME} must contain exactly one .tgz; found ${#package_tgzs[@]}" >&2 + exit 1 + fi + export OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ="${package_tgzs[0]}" + if [[ -z "${OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL// }" ]]; then + export OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL="$(basename "${package_tgzs[0]}")" + fi + elif [[ -z "${OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL// }" ]]; then + export OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL="${OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC}" + fi + if [[ -n "${INPUT_SCENARIO// }" ]]; then export OPENCLAW_NPM_TELEGRAM_SCENARIOS="${INPUT_SCENARIO}" fi diff --git a/.github/workflows/package-acceptance.yml b/.github/workflows/package-acceptance.yml index bfa3f1807ed..53cc8ea5fbe 100644 --- a/.github/workflows/package-acceptance.yml +++ b/.github/workflows/package-acceptance.yml @@ -65,7 +65,7 @@ on: default: "" type: string telegram_mode: - description: Optional published-npm Telegram QA lane + description: Optional Telegram QA lane for the resolved package candidate required: true default: none type: choice @@ -125,7 +125,7 @@ on: default: "" type: string telegram_mode: - description: Optional published-npm Telegram QA lane + description: Optional Telegram QA lane for the resolved package candidate required: false default: none type: string @@ -366,10 +366,6 @@ jobs: telegram_enabled=false if [[ "$TELEGRAM_MODE" != "none" ]]; then - if [[ "$SOURCE" != "npm" ]]; then - echo "telegram_mode requires source=npm because the Telegram workflow installs a published package spec." >&2 - exit 1 - fi telegram_enabled=true fi @@ -476,12 +472,14 @@ jobs: FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }} npm_telegram: - name: Published npm Telegram acceptance + name: Telegram package acceptance needs: resolve_package if: needs.resolve_package.outputs.telegram_enabled == 'true' uses: ./.github/workflows/npm-telegram-beta-e2e.yml with: package_spec: ${{ inputs.package_spec }} + package_artifact_name: ${{ needs.resolve_package.outputs.package_artifact_name }} + package_label: openclaw@${{ needs.resolve_package.outputs.package_version }} provider_mode: ${{ needs.resolve_package.outputs.telegram_mode }} secrets: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} diff --git a/scripts/e2e/npm-telegram-live-docker.sh b/scripts/e2e/npm-telegram-live-docker.sh index 5cb4335973c..6d432f7422b 100755 --- a/scripts/e2e/npm-telegram-live-docker.sh +++ b/scripts/e2e/npm-telegram-live-docker.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Installs a published OpenClaw npm package in Docker, performs Telegram +# Installs an OpenClaw package candidate in Docker, performs Telegram # onboarding/doctor recovery, then runs the Telegram QA live harness. set -euo pipefail @@ -9,6 +9,8 @@ source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-npm-telegram-live-e2e" OPENCLAW_NPM_TELEGRAM_LIVE_E2E_IMAGE)" DOCKER_TARGET="${OPENCLAW_NPM_TELEGRAM_DOCKER_TARGET:-build}" PACKAGE_SPEC="${OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC:-openclaw@beta}" +PACKAGE_TGZ="${OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ:-${OPENCLAW_CURRENT_PACKAGE_TGZ:-}}" +PACKAGE_LABEL="${OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL:-}" OUTPUT_DIR="${OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR:-.artifacts/qa-e2e/npm-telegram-live}" resolve_credential_source() { @@ -46,7 +48,45 @@ validate_openclaw_package_spec() { exit 1 } -validate_openclaw_package_spec "$PACKAGE_SPEC" +resolve_package_tgz() { + local candidate="$1" + if [ -z "$candidate" ]; then + return 0 + fi + if [ ! -f "$candidate" ]; then + echo "OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ must point to an existing .tgz file; got: $candidate" >&2 + exit 1 + fi + case "$candidate" in + *.tgz) ;; + *) + echo "OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ must point to a .tgz file; got: $candidate" >&2 + exit 1 + ;; + esac + local dir + local base + dir="$(cd "$(dirname "$candidate")" && pwd)" + base="$(basename "$candidate")" + printf "%s/%s" "$dir" "$base" +} + +package_mount_args=() +package_install_source="$PACKAGE_SPEC" +resolved_package_tgz="$(resolve_package_tgz "$PACKAGE_TGZ")" +if [ -n "$resolved_package_tgz" ]; then + package_install_source="/package-under-test/$(basename "$resolved_package_tgz")" + package_mount_args=(-v "$resolved_package_tgz:$package_install_source:ro") +else + validate_openclaw_package_spec "$PACKAGE_SPEC" +fi +if [ -z "$PACKAGE_LABEL" ]; then + if [ -n "$resolved_package_tgz" ]; then + PACKAGE_LABEL="$(basename "$resolved_package_tgz")" + else + PACKAGE_LABEL="$PACKAGE_SPEC" + fi +fi docker_e2e_build_or_reuse "$IMAGE_NAME" npm-telegram-live "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "$DOCKER_TARGET" docker_e2e_harness_mount_args @@ -64,6 +104,7 @@ fi docker_env=( -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 -e OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC="$PACKAGE_SPEC" + -e OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL="$PACKAGE_LABEL" -e OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR="$OUTPUT_DIR" -e OPENCLAW_NPM_TELEGRAM_FAST="${OPENCLAW_NPM_TELEGRAM_FAST:-1}" ) @@ -124,10 +165,12 @@ run_logged() { >"$run_log" } -echo "Running published npm Telegram live Docker E2E ($PACKAGE_SPEC)..." +echo "Running package Telegram live Docker E2E ($PACKAGE_LABEL)..." run_logged docker run --rm \ -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ - -e OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC="$PACKAGE_SPEC" \ + -e OPENCLAW_NPM_TELEGRAM_INSTALL_SOURCE="$package_install_source" \ + -e OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL="$PACKAGE_LABEL" \ + "${package_mount_args[@]}" \ -v "$npm_prefix_host:/npm-global" \ -i "$IMAGE_NAME" bash -s <<'EOF' set -euo pipefail @@ -136,15 +179,16 @@ export HOME="$(mktemp -d "/tmp/openclaw-npm-telegram-install.XXXXXX")" export NPM_CONFIG_PREFIX="/npm-global" export PATH="$NPM_CONFIG_PREFIX/bin:$PATH" -package_spec="${OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC:?missing OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC}" -echo "Installing ${package_spec}..." -npm install -g "$package_spec" --no-fund --no-audit +install_source="${OPENCLAW_NPM_TELEGRAM_INSTALL_SOURCE:?missing OPENCLAW_NPM_TELEGRAM_INSTALL_SOURCE}" +package_label="${OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL:-$install_source}" +echo "Installing ${package_label} from ${install_source}..." +npm install -g "$install_source" --no-fund --no-audit command -v openclaw openclaw --version EOF -# Mount only test harness/plugin QA sources; the SUT itself is the npm install. +# Mount only test harness/plugin QA sources; the SUT itself is the installed package candidate. run_logged docker run --rm \ "${docker_env[@]}" \ -v "$ROOT_DIR/.artifacts:/app/.artifacts" \ @@ -161,7 +205,7 @@ export OPENCLAW_NPM_TELEGRAM_REPO_ROOT="/app" dump_hotpath_logs() { local status="$1" - echo "installed npm onboarding recovery hot path failed with exit code $status" >&2 + echo "installed-package onboarding recovery hot path failed with exit code $status" >&2 for file in \ /tmp/openclaw-npm-telegram-onboard.json \ /tmp/openclaw-npm-telegram-channel-add.log \ @@ -178,11 +222,11 @@ trap 'status=$?; dump_hotpath_logs "$status"; exit "$status"' ERR command -v openclaw openclaw --version # The mounted QA harness imports openclaw/plugin-sdk; point that package import -# at the installed npm package without copying source into the test image. +# at the installed package without copying source into the test image. mkdir -p /app/node_modules ln -sfn /npm-global/lib/node_modules/openclaw /app/node_modules/openclaw -echo "Running installed npm onboarding recovery hot path..." +echo "Running installed-package onboarding recovery hot path..." OPENAI_API_KEY="${OPENAI_API_KEY:-sk-openclaw-npm-telegram-hotpath}" openclaw onboard --non-interactive --accept-risk \ --mode local \ --auth-choice openai-api-key \ @@ -210,4 +254,4 @@ trap - ERR tsx scripts/e2e/npm-telegram-live-runner.ts EOF -echo "published npm Telegram live Docker E2E passed ($PACKAGE_SPEC)" +echo "package Telegram live Docker E2E passed ($PACKAGE_LABEL)" diff --git a/scripts/e2e/npm-telegram-live-runner.ts b/scripts/e2e/npm-telegram-live-runner.ts index ad5500968fa..367a10b2602 100644 --- a/scripts/e2e/npm-telegram-live-runner.ts +++ b/scripts/e2e/npm-telegram-live-runner.ts @@ -1,6 +1,6 @@ #!/usr/bin/env -S node --import tsx -// Telegram npm-live Docker harness. -// Runs QA live transport code against the published package installed in Docker. +// Telegram package Docker harness. +// Runs QA live transport code against the package candidate installed in Docker. import fs from "node:fs/promises"; import path from "node:path"; @@ -78,9 +78,9 @@ async function main() { credentialRole: resolveCredentialRole(process.env), }); - process.stdout.write(`NPM Telegram QA report: ${result.reportPath}\n`); - process.stdout.write(`NPM Telegram QA summary: ${result.summaryPath}\n`); - process.stdout.write(`NPM Telegram QA observed messages: ${result.observedMessagesPath}\n`); + process.stdout.write(`Package Telegram QA report: ${result.reportPath}\n`); + process.stdout.write(`Package Telegram QA summary: ${result.summaryPath}\n`); + process.stdout.write(`Package Telegram QA observed messages: ${result.observedMessagesPath}\n`); if ( !parseBoolean(process.env.OPENCLAW_NPM_TELEGRAM_ALLOW_FAILURES) && result.scenarios.some((scenario) => scenario.status === "fail") @@ -101,7 +101,7 @@ async function formatRunnerErrorMessage(error: unknown) { if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { main().catch(async (error) => { process.stderr.write( - `npm telegram live e2e failed: ${await formatRunnerErrorMessage(error)}\n`, + `package telegram live e2e failed: ${await formatRunnerErrorMessage(error)}\n`, ); process.exitCode = 1; }); diff --git a/test/scripts/npm-telegram-live.test.ts b/test/scripts/npm-telegram-live.test.ts index a5912fb7abc..95704c85681 100644 --- a/test/scripts/npm-telegram-live.test.ts +++ b/test/scripts/npm-telegram-live.test.ts @@ -7,7 +7,7 @@ import { __testing } from "../../scripts/e2e/npm-telegram-live-runner.ts"; const TEST_DIR = path.dirname(fileURLToPath(import.meta.url)); const DOCKER_SCRIPT_PATH = path.resolve(TEST_DIR, "../../scripts/e2e/npm-telegram-live-docker.sh"); -describe("npm Telegram live Docker E2E", () => { +describe("package Telegram live Docker E2E", () => { it("supports npm-specific Convex credential aliases", () => { const script = readFileSync(DOCKER_SCRIPT_PATH, "utf8"); @@ -28,18 +28,33 @@ describe("npm Telegram live Docker E2E", () => { expect(script).toContain('printf "convex"'); }); - it("installs the npm package before forwarding runtime secrets", () => { + it("installs the package candidate before forwarding runtime secrets", () => { const script = readFileSync(DOCKER_SCRIPT_PATH, "utf8"); - const installRunStart = script.indexOf('echo "Running published npm Telegram live Docker E2E'); + const installRunStart = script.indexOf('echo "Running package Telegram live Docker E2E'); const installRunEnd = script.indexOf('run_logged docker run --rm \\\n "${docker_env[@]}"'); const installRun = script.slice(installRunStart, installRunEnd); - expect(installRun).toContain('npm install -g "$package_spec" --no-fund --no-audit'); + expect(installRun).toContain('npm install -g "$install_source" --no-fund --no-audit'); + expect(installRun).toContain('"${package_mount_args[@]}"'); expect(installRun).not.toContain('"${docker_env[@]}"'); expect(script).toContain('if [ -z "$credential_role" ] && [ -n "${CI:-}" ]'); expect(script).toContain('credential_role="ci"'); }); + it("can install a resolved package tarball instead of a registry spec", () => { + const script = readFileSync(DOCKER_SCRIPT_PATH, "utf8"); + + expect(script).toContain("OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ"); + expect(script).toContain("OPENCLAW_CURRENT_PACKAGE_TGZ"); + expect(script).toContain( + 'package_mount_args=(-v "$resolved_package_tgz:$package_install_source:ro")', + ); + expect(script).toContain('validate_openclaw_package_spec "$PACKAGE_SPEC"'); + expect(script.indexOf('if [ -n "$resolved_package_tgz" ]; then')).toBeLessThan( + script.indexOf('validate_openclaw_package_spec "$PACKAGE_SPEC"'), + ); + }); + it("lets npm-specific credential aliases override shared QA env", () => { expect( __testing.resolveCredentialSource({ diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index bca77db6009..18b062ef892 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -34,15 +34,21 @@ describe("package acceptance workflow", () => { ); }); - it("offers bounded product profiles and keeps Telegram published-npm only", () => { + it("offers bounded product profiles and can run Telegram against the resolved artifact", () => { const workflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8"); 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("include_release_path_suites=true"); - expect(workflow).toContain("telegram_mode requires source=npm"); + expect(workflow).not.toContain("telegram_mode requires source=npm"); expect(workflow).toContain("uses: ./.github/workflows/npm-telegram-beta-e2e.yml"); + expect(workflow).toContain( + "package_artifact_name: ${{ needs.resolve_package.outputs.package_artifact_name }}", + ); + expect(workflow).toContain( + "package_label: openclaw@${{ needs.resolve_package.outputs.package_version }}", + ); }); }); @@ -62,10 +68,13 @@ describe("package artifact reuse", () => { expect(action).toContain("name: ${{ inputs.package-artifact-name }}"); }); - it("allows the npm Telegram lane to run from reusable package acceptance", () => { + it("allows the Telegram lane to run from reusable package acceptance artifacts", () => { const workflow = readFileSync(NPM_TELEGRAM_WORKFLOW, "utf8"); expect(workflow).toContain("workflow_call:"); + expect(workflow).toContain("package_artifact_name:"); + expect(workflow).toContain("Download package-under-test artifact"); + expect(workflow).toContain("OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ"); expect(workflow).toContain("provider_mode:"); expect(workflow).toContain("provider_mode must be mock-openai or live-frontier"); });