name: NPM Telegram Beta E2E run-name: ${{ inputs.dispatch_id != '' && format('NPM Telegram Beta E2E {0}', inputs.dispatch_id) || 'NPM Telegram Beta E2E' }} on: workflow_dispatch: inputs: package_spec: 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 package_artifact_id: description: Immutable GitHub artifact id for package_artifact_name required: false default: "" type: string package_artifact_digest: description: GitHub artifact service SHA-256 digest without the sha256 prefix required: false default: "" type: string package_sha256: description: Expected SHA-256 for the OpenClaw package tarball required: false default: "" type: string package_artifact_run_id: description: Producer run id containing package_artifact_name required: false default: "" type: string package_artifact_run_attempt: description: Producer run attempt containing package_artifact_name required: false default: "" type: string package_file_name: description: Exact OpenClaw tarball filename inside package_artifact_name required: false default: "" type: string package_source_sha: description: Exact source commit recorded in the package tarball required: false default: "" type: string package_version: description: Exact OpenClaw package version required: false default: "" type: string harness_ref: description: Source ref for the private QA harness; defaults to the dispatched workflow ref required: false default: "" type: string provider_mode: description: QA provider mode required: true default: mock-openai type: choice options: - mock-openai - live-frontier scenario: description: Optional comma-separated Telegram scenario ids required: false type: string advisory: description: Treat package Telegram failures as advisory for the caller required: false default: false type: boolean dispatch_id: description: Optional parent workflow dispatch identifier required: false default: "" type: string workflow_call: inputs: advisory: description: Treat package Telegram failures as advisory for the caller required: false default: false type: boolean package_spec: 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 or specified workflow run required: false default: "" type: string package_artifact_digest: description: GitHub artifact service SHA-256 digest without the sha256 prefix required: false default: "" type: string package_artifact_run_id: description: Producer run id containing package_artifact_name required: false default: "" type: string package_artifact_run_attempt: description: Producer run attempt containing package_artifact_name required: false default: "" type: string package_artifact_id: description: Immutable GitHub artifact id for package_artifact_name required: false default: "" type: string package_sha256: description: Expected SHA-256 for the OpenClaw package tarball required: false default: "" type: string package_file_name: description: Exact OpenClaw tarball filename inside package_artifact_name required: false default: "" type: string package_source_sha: description: Exact source commit recorded in the package tarball required: false default: "" type: string package_version: description: Exact OpenClaw package version required: false default: "" type: string package_label: description: Optional display label for an artifact-backed package candidate required: false default: "" type: string harness_ref: description: Source ref for the private QA harness; defaults to the called workflow ref required: false default: "" type: string provider_mode: description: QA provider mode required: false default: mock-openai type: string scenario: description: Optional comma-separated Telegram scenario ids required: false default: "" type: string dispatch_id: description: Optional parent workflow dispatch identifier required: false default: "" type: string secrets: OPENAI_API_KEY: required: false OPENCLAW_QA_CONVEX_SITE_URL: required: false OPENCLAW_QA_CONVEX_SECRET_CI: required: false permissions: contents: read concurrency: group: npm-telegram-beta-e2e-${{ github.run_id }} cancel-in-progress: false env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" NODE_VERSION: "24.15.0" jobs: run_package_telegram_e2e: name: Run package Telegram E2E runs-on: blacksmith-32vcpu-ubuntu-2404 continue-on-error: ${{ inputs.advisory }} timeout-minutes: 60 environment: qa-live-shared permissions: actions: read contents: read env: DOCKER_BUILD_SUMMARY: "false" DOCKER_BUILD_RECORD_UPLOAD: "false" steps: - name: Checkout dispatch ref uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 with: ref: ${{ inputs.harness_ref || github.sha }} fetch-depth: 1 persist-credentials: false - name: Set up Blacksmith Docker Builder uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1 with: max-cache-size-mb: 800000 - name: Build Docker E2E image uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2 with: context: . file: ./scripts/e2e/Dockerfile target: build platforms: linux/amd64 tags: openclaw-docker-e2e:local load: true push: false provenance: false - name: Setup Node environment uses: ./.github/actions/setup-node-env with: node-version: ${{ env.NODE_VERSION }} install-bun: "true" - name: Validate inputs and secrets env: PACKAGE_SPEC: ${{ inputs.package_spec }} PACKAGE_ARTIFACT_DIGEST: ${{ inputs.package_artifact_digest || '' }} PACKAGE_ARTIFACT_ID: ${{ inputs.package_artifact_id || '' }} PACKAGE_ARTIFACT_NAME: ${{ inputs.package_artifact_name || '' }} PACKAGE_ARTIFACT_RUN_ATTEMPT: ${{ inputs.package_artifact_run_attempt || '' }} PACKAGE_ARTIFACT_RUN_ID: ${{ inputs.package_artifact_run_id || '' }} PACKAGE_FILE_NAME: ${{ inputs.package_file_name || '' }} PACKAGE_SHA256: ${{ inputs.package_sha256 || '' }} PACKAGE_SOURCE_SHA: ${{ inputs.package_source_sha || '' }} PACKAGE_VERSION: ${{ inputs.package_version || '' }} PROVIDER_MODE: ${{ inputs.provider_mode }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }} OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }} shell: bash run: | set -euo pipefail artifact_tuple_present=0 for value in \ "$PACKAGE_ARTIFACT_DIGEST" \ "$PACKAGE_ARTIFACT_ID" \ "$PACKAGE_ARTIFACT_NAME" \ "$PACKAGE_ARTIFACT_RUN_ATTEMPT" \ "$PACKAGE_ARTIFACT_RUN_ID" \ "$PACKAGE_FILE_NAME" \ "$PACKAGE_SHA256" \ "$PACKAGE_SOURCE_SHA" \ "$PACKAGE_VERSION"; do if [[ -n "${value// }" ]]; then artifact_tuple_present=1 fi done if [[ -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then if [[ "$artifact_tuple_present" == "1" ]]; then echo "Artifact-backed Telegram E2E requires all artifact identity fields or none." >&2 exit 1 fi if [[ ! "${PACKAGE_SPEC}" =~ ^openclaw@(alpha|beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-(alpha|beta)\.[1-9][0-9]*)?)$ ]]; then echo "package_spec must be openclaw@alpha, openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${PACKAGE_SPEC}" >&2 exit 1 fi elif [[ ! "$PACKAGE_ARTIFACT_DIGEST" =~ ^[0-9a-f]{64}$ || ! "$PACKAGE_ARTIFACT_ID" =~ ^[1-9][0-9]*$ || ! "$PACKAGE_ARTIFACT_RUN_ATTEMPT" =~ ^[1-9][0-9]*$ || ! "$PACKAGE_ARTIFACT_RUN_ID" =~ ^[1-9][0-9]*$ || ! "$PACKAGE_FILE_NAME" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*\.tgz$ || ! "$PACKAGE_SHA256" =~ ^[0-9a-f]{64}$ || ! "$PACKAGE_SOURCE_SHA" =~ ^[0-9a-f]{40}$ || -z "${PACKAGE_VERSION// }" ]]; then echo "Artifact-backed Telegram E2E requires the complete immutable artifact and package identity tuple." >&2 exit 1 fi case "${PROVIDER_MODE}" in mock-openai | live-frontier) ;; *) echo "provider_mode must be mock-openai or live-frontier; got: ${PROVIDER_MODE}" >&2 exit 1 ;; esac require_var() { local key="$1" if [[ -z "${!key:-}" ]]; then echo "Missing required ${key}." >&2 exit 1 fi } require_var OPENCLAW_QA_CONVEX_SITE_URL require_var OPENCLAW_QA_CONVEX_SECRET_CI if [[ "${PROVIDER_MODE}" == "live-frontier" ]]; then require_var OPENAI_API_KEY fi - name: Validate package artifact identity if: inputs.package_artifact_name != '' env: ARTIFACT_DIGEST: ${{ inputs.package_artifact_digest }} ARTIFACT_ID: ${{ inputs.package_artifact_id }} ARTIFACT_NAME: ${{ inputs.package_artifact_name }} ARTIFACT_RUN_ATTEMPT: ${{ inputs.package_artifact_run_attempt }} ARTIFACT_RUN_ID: ${{ inputs.package_artifact_run_id }} GH_TOKEN: ${{ github.token }} shell: bash run: | set -euo pipefail [[ "$ARTIFACT_NAME" == *"-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}" ]] || { echo "Package Telegram artifact name does not bind the declared producer run attempt." >&2 exit 1 } artifact_json="$(gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}")" jq -e \ --arg digest "sha256:${ARTIFACT_DIGEST}" \ --arg id "$ARTIFACT_ID" \ --arg name "$ARTIFACT_NAME" \ --arg run_id "$ARTIFACT_RUN_ID" \ ' (.id | tostring) == $id and .name == $name and .expired == false and .digest == $digest and (.workflow_run.id | tostring) == $run_id ' <<< "$artifact_json" >/dev/null || { echo "Package Telegram artifact identity does not match the requested immutable tuple." >&2 exit 1 } attempt_json="$( gh api \ "repos/${GITHUB_REPOSITORY}/actions/runs/${ARTIFACT_RUN_ID}/attempts/${ARTIFACT_RUN_ATTEMPT}" )" jq -e \ --arg attempt "$ARTIFACT_RUN_ATTEMPT" \ --arg run_id "$ARTIFACT_RUN_ID" \ '(.id | tostring) == $run_id and (.run_attempt | tostring) == $attempt' \ <<< "$attempt_json" >/dev/null || { echo "Package Telegram artifact producer run attempt does not match the requested tuple." >&2 exit 1 } - name: Download package-under-test artifact if: inputs.package_artifact_name != '' && inputs.package_artifact_run_id == github.run_id uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: artifact-ids: ${{ inputs.package_artifact_id }} path: .artifacts/telegram-package-under-test run-id: ${{ inputs.package_artifact_run_id }} github-token: ${{ github.token }} - name: Download package-under-test artifact from release run if: inputs.package_artifact_name != '' && inputs.package_artifact_run_id != github.run_id uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: artifact-ids: ${{ inputs.package_artifact_id }} path: .artifacts/telegram-package-under-test run-id: ${{ inputs.package_artifact_run_id }} github-token: ${{ github.token }} - name: Run package Telegram E2E id: run_lane shell: bash env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 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 OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }} OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }} OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "1800000" OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1" INPUT_SCENARIO: ${{ inputs.scenario }} PACKAGE_ARTIFACT_NAME: ${{ inputs.package_artifact_name || '' }} PACKAGE_FILE_NAME: ${{ inputs.package_file_name || '' }} PACKAGE_SHA256: ${{ inputs.package_sha256 || '' }} PACKAGE_SOURCE_SHA: ${{ inputs.package_source_sha || '' }} PACKAGE_VERSION: ${{ inputs.package_version || '' }} run: | set -euo pipefail output_dir=".artifacts/qa-e2e/npm-telegram-beta-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" echo "output_dir=${output_dir}" >> "$GITHUB_OUTPUT" export OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR="${output_dir}" append_telegram_summary() { local status=$? local report="${output_dir}/telegram-qa-report.md" if [[ -n "${GITHUB_STEP_SUMMARY:-}" && -f "${report}" ]]; then { echo "## Package Telegram E2E" echo echo "- Package: ${OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL:-${OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC}}" echo "- Provider mode: ${OPENCLAW_NPM_TELEGRAM_PROVIDER_MODE}" echo cat "${report}" } >> "${GITHUB_STEP_SUMMARY}" fi return "${status}" } trap append_telegram_summary EXIT if [[ -n "${PACKAGE_ARTIFACT_NAME// }" ]]; then package_dir=".artifacts/telegram-package-under-test" declared_package_tgz="${package_dir}/${PACKAGE_FILE_NAME}" [[ -f "$declared_package_tgz" ]] || { echo "Package Telegram artifact is missing the declared package tarball." >&2 exit 1 } manifest="${package_dir}/preflight-manifest.json" if [[ -f "${manifest}" ]]; then package_tgz="$( node - "${manifest}" "${package_dir}" <<'NODE' const crypto = require("node:crypto"); const childProcess = require("node:child_process"); const fs = require("node:fs"); const path = require("node:path"); const [manifestPath, packageDir] = process.argv.slice(2); const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); const entries = [ { packageName: "openclaw", packageVersion: manifest.packageVersion, tarballName: manifest.tarballName, tarballSha256: manifest.tarballSha256, }, ...(Array.isArray(manifest.dependencyTarballs) ? manifest.dependencyTarballs : []), ]; const packageNames = new Set(); const tarballNames = new Set(); for (const entry of entries) { if ( !entry.packageName || !entry.packageVersion || !entry.tarballName || !entry.tarballSha256 || entry.tarballName !== path.basename(entry.tarballName) ) { throw new Error("package artifact manifest contains invalid tarball metadata"); } if (packageNames.has(entry.packageName) || tarballNames.has(entry.tarballName)) { throw new Error("package artifact manifest contains duplicate package metadata"); } packageNames.add(entry.packageName); tarballNames.add(entry.tarballName); const tarballPath = path.join(packageDir, entry.tarballName); if (!fs.existsSync(tarballPath)) { throw new Error(`package artifact is missing ${entry.tarballName}`); } const digest = crypto.createHash("sha256").update(fs.readFileSync(tarballPath)).digest("hex"); if (digest !== entry.tarballSha256) { throw new Error(`package artifact digest mismatch for ${entry.packageName}`); } const packageJson = JSON.parse( childProcess.execFileSync("tar", ["-xOf", tarballPath, "package/package.json"], { encoding: "utf8", }), ); if (packageJson.name !== entry.packageName || packageJson.version !== entry.packageVersion) { throw new Error(`package artifact metadata mismatch for ${entry.packageName}`); } } const artifactTarballs = fs .readdirSync(packageDir, { withFileTypes: true }) .filter((entry) => entry.isFile() && entry.name.endsWith(".tgz")) .map((entry) => entry.name); if ( artifactTarballs.length !== tarballNames.size || artifactTarballs.some((name) => !tarballNames.has(name)) ) { throw new Error("package artifact tarball set does not match preflight manifest"); } process.stdout.write(path.join(packageDir, manifest.tarballName)); NODE )" export OPENCLAW_NPM_TELEGRAM_PACKAGE_DIR="${package_dir}" else mapfile -t package_tgzs < <(find "${package_dir}" -type f -name "*.tgz" | sort) if [[ "${#package_tgzs[@]}" -ne 1 ]]; then echo "package artifact ${PACKAGE_ARTIFACT_NAME} without a preflight manifest must contain exactly one tgz; found ${#package_tgzs[@]}" >&2 exit 1 fi package_tgz="${package_tgzs[0]}" candidate_manifest="${package_dir}/package-candidate.json" node - "${package_tgz}" "${candidate_manifest}" <<'NODE' const crypto = require("node:crypto"); const childProcess = require("node:child_process"); const fs = require("node:fs"); const path = require("node:path"); const [tarballPath, manifestPath] = process.argv.slice(2); const packageJson = JSON.parse( childProcess.execFileSync("tar", ["-xOf", tarballPath, "package/package.json"], { encoding: "utf8", }), ); if (packageJson.name !== "openclaw" || typeof packageJson.version !== "string") { throw new Error("package artifact tgz is not a versioned openclaw package"); } if (fs.existsSync(manifestPath)) { const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); const expectedTarballName = path.basename(manifest.tarball || ""); if ( manifest.name !== "openclaw" || manifest.version !== packageJson.version || expectedTarballName !== path.basename(tarballPath) || typeof manifest.sha256 !== "string" ) { throw new Error("package candidate manifest does not match the OpenClaw tarball"); } const digest = crypto.createHash("sha256").update(fs.readFileSync(tarballPath)).digest("hex"); if (digest !== manifest.sha256) { throw new Error("package candidate digest mismatch"); } } NODE fi [[ "$(basename "$package_tgz")" == "$PACKAGE_FILE_NAME" ]] || { echo "Package Telegram artifact tarball differs from package_file_name." >&2 exit 1 } actual_package_sha256="$(sha256sum "$package_tgz" | awk '{print $1}')" if [[ "$actual_package_sha256" != "$PACKAGE_SHA256" ]]; then echo "Package Telegram artifact SHA-256 differs from package_sha256." >&2 exit 1 fi actual_package_version="$( tar -xOf "$package_tgz" package/package.json | jq -er '.version | select(type == "string" and length > 0)' )" actual_package_source_sha="$( tar -xOf "$package_tgz" package/dist/build-info.json | jq -er '.commit | select(type == "string" and test("^[0-9a-f]{40}$"))' )" [[ "$actual_package_source_sha" == "$PACKAGE_SOURCE_SHA" && "$actual_package_version" == "$PACKAGE_VERSION" ]] || { echo "Package Telegram artifact source SHA/version differs from the declared identity." >&2 exit 1 } export OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ="${package_tgz}" if [[ -z "${OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL// }" ]]; then export OPENCLAW_NPM_TELEGRAM_PACKAGE_LABEL="$(basename "${package_tgz}")" 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 pnpm test:docker:npm-telegram-live - name: Upload npm Telegram E2E artifacts if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: npm-telegram-beta-e2e-${{ github.run_id }}-${{ github.run_attempt }} path: .artifacts/qa-e2e/ retention-days: 14 if-no-files-found: error