Files
openclaw/.github/workflows/npm-telegram-beta-e2e.yml
Vincent Koc cbe3731f77 fix(release): make validation proof no-write (#103737)
* fix(release): make validation proof no-write

* test(release): align no-write workflow contracts
2026-07-10 08:18:44 -07:00

578 lines
23 KiB
YAML

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