diff --git a/.github/workflows/openclaw-release-publish.yml b/.github/workflows/openclaw-release-publish.yml index 529b2d19e75..5d417c746eb 100644 --- a/.github/workflows/openclaw-release-publish.yml +++ b/.github/workflows/openclaw-release-publish.yml @@ -15,6 +15,10 @@ on: description: Successful Full Release Validation run id for this tag/SHA, required when publish_openclaw_npm=true required: false type: string + npm_telegram_run_id: + description: Optional successful NPM Telegram Beta E2E run id to include in final release evidence + required: false + type: string npm_dist_tag: description: npm dist-tag for the OpenClaw package required: true @@ -323,6 +327,12 @@ jobs: fetch-depth: 1 persist-credentials: false + - name: Setup Node environment + uses: ./.github/actions/setup-node-env + with: + install-bun: "false" + cache-key-suffix: release-publish + - name: Dispatch publish workflows env: GH_TOKEN: ${{ github.token }} @@ -337,6 +347,8 @@ jobs: PUBLISH_OPENCLAW_NPM: ${{ inputs.publish_openclaw_npm && 'true' || 'false' }} WAIT_FOR_CLAWHUB: ${{ inputs.wait_for_clawhub && 'true' || 'false' }} PREFLIGHT_ARTIFACT_NAME: ${{ needs.resolve_release_target.outputs.preflight_artifact_name }} + NPM_TELEGRAM_RUN_ID: ${{ inputs.npm_telegram_run_id }} + POSTPUBLISH_EVIDENCE_DIR: ${{ runner.temp }}/openclaw-release-postpublish-evidence run: | set -euo pipefail @@ -402,6 +414,56 @@ jobs: done < <(printf '%s' "${pending_json}" | jq -r '.[] | [.environment.id, .environment.name, .current_user_can_approve] | @tsv') } + approve_pending_deployments() { + local workflow="$1" + local run_id="$2" + local pending_json approved + + pending_json="$(gh api -X GET "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/pending_deployments" 2>/dev/null || true)" + if [[ -z "${pending_json}" ]] || ! printf '%s' "${pending_json}" | jq -e 'length > 0' >/dev/null 2>&1; then + return 0 + fi + + approved=0 + while IFS=$'\t' read -r env_id env_name; do + if [[ -z "${env_id}" ]]; then + continue + fi + echo "${workflow}: approving pending environment ${env_name} (${env_id})" + gh api -X POST "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/pending_deployments" \ + -F "environment_ids[]=${env_id}" \ + -f state=approved \ + -f comment="Approve release gate from OpenClaw Release Publish wrapper" >/dev/null + approved=1 + done < <(printf '%s' "${pending_json}" | jq -r '.[] | select(.current_user_can_approve == true) | [.environment.id, .environment.name] | @tsv') + + if [[ "${approved}" == "1" ]]; then + echo "${workflow}: approved available pending environment gates" + fi + } + + print_failed_run_summary() { + local run_id="$1" + local failed_json + + failed_json="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json jobs \ + --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {databaseId, name, conclusion, url}' || true)" + if [[ -z "${failed_json}" ]]; then + return 0 + fi + + echo "Failed child job summary:" + printf '%s\n' "${failed_json}" + while IFS=$'\t' read -r job_id job_name; do + if [[ -z "${job_id}" ]]; then + continue + fi + echo "--- ${job_name} (${job_id}) log tail ---" + gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --job "${job_id}" --log 2>/dev/null | + tail -200 || true + done < <(printf '%s\n' "${failed_json}" | jq -r '[.databaseId, .name] | @tsv' 2>/dev/null || true) + } + wait_for_run() { local workflow="$1" local run_id="$2" @@ -420,6 +482,7 @@ jobs: if [[ "$state" != "$last_state" ]]; then echo "${workflow} still ${status} (updated ${updated_at}): ${url}" print_pending_deployments "${workflow}" "${run_id}" + approve_pending_deployments "${workflow}" "${run_id}" last_state="$state" fi sleep 30 @@ -447,7 +510,7 @@ jobs: echo "- ${workflow}: ${conclusion} in ${duration_label} (${url})" } >> "$GITHUB_STEP_SUMMARY" if [[ "$conclusion" != "success" ]]; then - gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json jobs --jq '.jobs[] | select(.conclusion != "success" and .conclusion != "skipped") | {name, conclusion, url}' || true + print_failed_run_summary "${run_id}" return 1 fi } @@ -547,6 +610,42 @@ jobs: echo "- Dependency evidence asset: \`${asset_name}\`" >> "$GITHUB_STEP_SUMMARY" } + verify_published_release() { + local release_version evidence_path + local -a verify_args + + release_version="${RELEASE_TAG#v}" + evidence_path="${POSTPUBLISH_EVIDENCE_DIR}/release-postpublish-evidence.json" + mkdir -p "${POSTPUBLISH_EVIDENCE_DIR}" + + verify_args=( + release:verify-beta + -- + "${release_version}" + --tag "${RELEASE_TAG}" + --dist-tag "${RELEASE_NPM_DIST_TAG}" + --repo "${GITHUB_REPOSITORY}" + --workflow-ref "${CHILD_WORKFLOW_REF}" + --full-release-validation-run "${FULL_RELEASE_VALIDATION_RUN_ID}" + --plugin-npm-run "${plugin_npm_run_id}" + --plugin-clawhub-run "${plugin_clawhub_run_id}" + --openclaw-npm-run "${openclaw_npm_run_id}" + --evidence-out "${evidence_path}" + ) + if [[ -n "${PLUGINS// }" ]]; then + verify_args+=(--plugins "${PLUGINS}") + fi + if [[ -n "${NPM_TELEGRAM_RUN_ID// }" ]]; then + verify_args+=(--npm-telegram-run "${NPM_TELEGRAM_RUN_ID}") + fi + + pnpm "${verify_args[@]}" + { + echo "- Postpublish verification: passed" + echo "- Postpublish evidence: \`${evidence_path}\`" + } >> "$GITHUB_STEP_SUMMARY" + } + { echo "### Publish sequence" echo @@ -555,11 +654,11 @@ jobs: echo "- Release SHA: \`${TARGET_SHA}\`" echo "- Plugin npm and ClawHub publish: dispatched in parallel" if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then - echo "- OpenClaw npm publish: starts after plugin npm succeeds; ClawHub may still be running" + echo "- OpenClaw npm publish: starts after plugin npm succeeds; final verification waits for ClawHub" else echo "- OpenClaw npm publish: skipped by input" fi - if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then + if [[ "${WAIT_FOR_CLAWHUB}" == "true" || "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then echo "- Workflow completion waits for ClawHub" else echo "- Workflow completion does not wait for ClawHub; monitor the dispatched ClawHub run separately" @@ -601,7 +700,7 @@ jobs: clawhub_result="" clawhub_pid="" - if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then + if [[ "${WAIT_FOR_CLAWHUB}" == "true" || "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then clawhub_result="$RUNNER_TEMP/clawhub-result.txt" wait_run_pid="" wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${clawhub_result}" @@ -620,23 +719,39 @@ jobs: fi failed=0 - if [[ -n "${clawhub_pid}" ]] && ! wait "${clawhub_pid}"; then - failed=1 - fi + openclaw_failed=0 if [[ -n "${openclaw_pid}" ]] && ! wait "${openclaw_pid}"; then failed=1 + openclaw_failed=1 + fi + if [[ -n "${openclaw_result}" && -f "${openclaw_result}" && "$(cat "${openclaw_result}")" != "success" ]]; then + failed=1 + openclaw_failed=1 + fi + + if [[ -n "${openclaw_npm_run_id}" && "${openclaw_failed}" == "0" ]]; then + create_or_update_github_release + upload_dependency_evidence_release_asset + fi + + if [[ -n "${clawhub_pid}" ]] && ! wait "${clawhub_pid}"; then + failed=1 fi if [[ -f "${clawhub_result}" && "$(cat "${clawhub_result}")" != "success" ]]; then failed=1 fi - if [[ -n "${openclaw_result}" && -f "${openclaw_result}" && "$(cat "${openclaw_result}")" != "success" ]]; then - failed=1 + + if [[ "${failed}" == "0" && -n "${openclaw_npm_run_id}" ]]; then + verify_published_release fi if [[ "${failed}" != "0" ]]; then exit 1 fi - if [[ -n "${openclaw_npm_run_id}" ]]; then - create_or_update_github_release - upload_dependency_evidence_release_asset - fi + - name: Upload postpublish evidence + if: ${{ always() }} + uses: actions/upload-artifact@v7 + with: + name: openclaw-release-postpublish-evidence-${{ inputs.tag }} + path: ${{ runner.temp }}/openclaw-release-postpublish-evidence + if-no-files-found: ignore diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index c9908b7e8a5..4e905bf3a38 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -78,11 +78,16 @@ the maintainer-only release runbook. file, lane, workflow job, package profile, provider, or model allowlist that proves the fix. Rerun the full umbrella only when the changed surface makes prior evidence stale. -9. For beta, tag `vYYYY.M.D-beta.N`, then run `OpenClaw Release Publish` from - the matching `release/YYYY.M.D` branch. It verifies `pnpm plugins:sync:check`, - dispatches all publishable plugin packages to npm and the same set to - ClawHub in parallel, and then promotes the prepared OpenClaw npm preflight - artifact with the matching dist-tag as soon as plugin npm publish succeeds. +9. For beta, tag `vYYYY.M.D-beta.N`, then run `pnpm release:candidate -- --tag +vYYYY.M.D-beta.N` from the matching `release/YYYY.M.D` branch. The helper runs + the local generated-release checks, dispatches or verifies the full release + validation and npm preflight evidence, runs Parallels and Telegram package + proof, records plugin npm and ClawHub plans, and prints the exact + `OpenClaw Release Publish` command only after the evidence bundle is green. + `OpenClaw Release Publish` dispatches the selected or all-publishable plugin + packages to npm and the same set to ClawHub in parallel, and then promotes the + prepared OpenClaw npm preflight artifact with the matching dist-tag as soon as + plugin npm publish succeeds. After the OpenClaw npm publish child succeeds, it creates or updates the matching GitHub release/prerelease page from the complete matching `CHANGELOG.md` section. Stable releases published to npm `latest` become the @@ -90,22 +95,18 @@ the maintainer-only release runbook. created with GitHub `latest=false`. The workflow also uploads the preflight dependency evidence to the GitHub release as `openclaw--dependency-evidence.zip` for post-release incident - response. - ClawHub publishing may still be running while OpenClaw npm publishes, but the - release publish workflow prints the child run IDs immediately. By default it - does not wait for ClawHub after dispatching it, so OpenClaw npm availability - is not blocked by slower ClawHub approvals or registry work; set - `wait_for_clawhub=true` when ClawHub must block workflow completion. The - ClawHub path retries transient CLI dependency install failures, publishes - preview-passing plugins even when one preview cell flakes, and ends with - registry verification for every expected plugin version so partial publishes - remain visible and retryable. After publish, run - `pnpm release:verify-beta -- YYYY.M.D-beta.N --openclaw-npm-run --plugin-npm-run --plugin-clawhub-run ` - to verify the GitHub prerelease, npm `beta` dist-tags, npm integrity, - published install path, ClawHub exact versions, ClawHub artifacts, and child - workflow conclusions from one command. Add `--rerun-failed-clawhub` when the - ClawHub sidecar failed only in retryable jobs and should be rerun in place. - Then run the post-publish package acceptance against the published + response. The publish workflow prints child run IDs immediately, auto-approves + release environment gates the workflow token is allowed to approve, summarizes + failed child jobs with log tails, closes out the GitHub release and dependency + evidence as soon as OpenClaw npm publish succeeds, waits for ClawHub whenever + OpenClaw npm is being published, then runs `pnpm release:verify-beta` and + uploads postpublish evidence for the GitHub release, npm package, selected + plugin npm packages, selected ClawHub packages, child workflow run IDs, and + optional NPM Telegram run ID. The ClawHub path retries transient CLI + dependency install failures, publishes preview-passing plugins even when one + preview cell flakes, and ends with registry verification for every expected + plugin version so partial publishes remain visible and retryable. Then run the post-publish + package acceptance against the published `openclaw@YYYY.M.D-beta.N` or `openclaw@beta` package. If a pushed or published prerelease needs a fix, cut the next matching prerelease number; do not delete or rewrite the old diff --git a/scripts/lib/release-beta-verifier.ts b/scripts/lib/release-beta-verifier.ts index c02efa3bf36..eeff417c9b2 100644 --- a/scripts/lib/release-beta-verifier.ts +++ b/scripts/lib/release-beta-verifier.ts @@ -2,7 +2,10 @@ import { execFileSync } from "node:child_process"; import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { collectClawHubPublishablePluginPackages } from "./plugin-clawhub-release.ts"; -import { collectPublishablePluginPackages } from "./plugin-npm-release.ts"; +import { + collectPublishablePluginPackages, + parsePluginReleaseSelection, +} from "./plugin-npm-release.ts"; type JsonRecord = Record; @@ -12,6 +15,8 @@ export type ReleaseVerifyBetaArgs = { distTag: string; repo: string; registry: string; + workflowRef?: string; + pluginSelection: string[]; evidenceOut?: string; skipPostpublish: boolean; rerunFailedClawHub: boolean; @@ -108,7 +113,7 @@ export function parseReleaseVerifyBetaArgs(argv: string[]): ReleaseVerifyBetaArg const version = values.shift(); if (!version || version.startsWith("-")) { throw new Error( - "Usage: pnpm release:verify-beta -- [--full-release-validation-run ID] [--openclaw-npm-run ID] [--plugin-npm-run ID] [--plugin-clawhub-run ID] [--npm-telegram-run ID]", + "Usage: pnpm release:verify-beta -- [--workflow-ref REF] [--full-release-validation-run ID] [--openclaw-npm-run ID] [--plugin-npm-run ID] [--plugin-clawhub-run ID] [--npm-telegram-run ID]", ); } @@ -118,6 +123,8 @@ export function parseReleaseVerifyBetaArgs(argv: string[]): ReleaseVerifyBetaArg distTag: "beta", repo: DEFAULT_REPO, registry: DEFAULT_CLAWHUB_REGISTRY, + workflowRef: undefined, + pluginSelection: [], evidenceOut: undefined, skipPostpublish: false, rerunFailedClawHub: false, @@ -148,6 +155,15 @@ export function parseReleaseVerifyBetaArgs(argv: string[]): ReleaseVerifyBetaArg case "--registry": parsed.registry = next(); break; + case "--workflow-ref": + parsed.workflowRef = next(); + break; + case "--plugins": + parsed.pluginSelection = parsePluginReleaseSelection(next()); + if (parsed.pluginSelection.length === 0) { + throw new Error("--plugins requires at least one plugin package name."); + } + break; case "--evidence-out": parsed.evidenceOut = next(); break; @@ -319,6 +335,8 @@ function verifyWorkflowRun(params: { id: string; label: string; repo: string; + expectedWorkflowName: string; + expectedHeadBranch?: string; rerunFailed: boolean; }): WorkflowRunSummary { const raw = runCommand("gh", [ @@ -328,12 +346,30 @@ function verifyWorkflowRun(params: { "--repo", params.repo, "--json", - "status,conclusion,url,createdAt,updatedAt,jobs", + "workflowName,headBranch,event,status,conclusion,url,createdAt,updatedAt,jobs", ]); const run = parseJson(raw, `gh run view ${params.id}`); if (!isRecord(run)) { throw new Error(`${params.label}: workflow run returned an unsupported JSON shape.`); } + const workflowName = readString(run.workflowName); + if (workflowName !== params.expectedWorkflowName) { + throw new Error( + `${params.label}: run ${params.id} workflow is ${workflowName ?? ""}, expected ${params.expectedWorkflowName}.`, + ); + } + const event = readString(run.event); + if (event !== "workflow_dispatch") { + throw new Error( + `${params.label}: run ${params.id} event is ${event ?? ""}, expected workflow_dispatch.`, + ); + } + const headBranch = readString(run.headBranch); + if (params.expectedHeadBranch !== undefined && headBranch !== params.expectedHeadBranch) { + throw new Error( + `${params.label}: run ${params.id} branch is ${headBranch ?? ""}, expected ${params.expectedHeadBranch}.`, + ); + } const status = readString(run.status); const conclusion = readString(run.conclusion); const jobs = Array.isArray(run.jobs) ? run.jobs.filter(isRecord) : []; @@ -391,6 +427,21 @@ function formatDuration(seconds: number | undefined): string { return `${minutes}m${remainder.toString().padStart(2, "0")}s`; } +function assertSelectedPackagesResolved(params: { + label: string; + selection: readonly string[]; + packages: readonly { packageName: string }[]; +}): void { + if (params.selection.length === 0) { + return; + } + const resolved = new Set(params.packages.map((plugin) => plugin.packageName)); + const missing = params.selection.filter((packageName) => !resolved.has(packageName)); + if (missing.length > 0) { + throw new Error(`Unknown or non-publishable ${params.label} selection: ${missing.join(", ")}.`); + } +} + export async function verifyBetaRelease( args: ReleaseVerifyBetaArgs, options: { rootDir?: string } = {}, @@ -418,13 +469,27 @@ export async function verifyBetaRelease( lines.push("openclaw postpublish verifier OK"); } - const npmPlugins = collectPublishablePluginPackages(rootDir); + const npmPlugins = collectPublishablePluginPackages(rootDir, { + packageNames: args.pluginSelection.length > 0 ? args.pluginSelection : undefined, + }); + assertSelectedPackagesResolved({ + label: "npm plugin", + selection: args.pluginSelection, + packages: npmPlugins, + }); for (const plugin of npmPlugins) { verifyNpmPackage(plugin.packageName, args.version, args.distTag); } lines.push(`plugin npm OK: ${npmPlugins.length}`); - const clawHubPlugins = collectClawHubPublishablePluginPackages(rootDir); + const clawHubPlugins = collectClawHubPublishablePluginPackages(rootDir, { + packageNames: args.pluginSelection.length > 0 ? args.pluginSelection : undefined, + }); + assertSelectedPackagesResolved({ + label: "ClawHub plugin", + selection: args.pluginSelection, + packages: clawHubPlugins, + }); for (const plugin of clawHubPlugins) { await verifyClawHubPackage({ registry: args.registry, @@ -442,6 +507,8 @@ export async function verifyBetaRelease( id: args.workflowRuns.fullReleaseValidation, label: "Full Release Validation", repo: args.repo, + expectedWorkflowName: "Full Release Validation", + expectedHeadBranch: args.workflowRef, rerunFailed: false, }), ); @@ -452,6 +519,8 @@ export async function verifyBetaRelease( id: args.workflowRuns.pluginNpm, label: "Plugin NPM Release", repo: args.repo, + expectedWorkflowName: "Plugin NPM Release", + expectedHeadBranch: args.workflowRef, rerunFailed: false, }), ); @@ -462,6 +531,8 @@ export async function verifyBetaRelease( id: args.workflowRuns.pluginClawHub, label: "Plugin ClawHub Release", repo: args.repo, + expectedWorkflowName: "Plugin ClawHub Release", + expectedHeadBranch: args.workflowRef, rerunFailed: args.rerunFailedClawHub, }), ); @@ -472,6 +543,8 @@ export async function verifyBetaRelease( id: args.workflowRuns.openclawNpm, label: "OpenClaw NPM Release", repo: args.repo, + expectedWorkflowName: "OpenClaw NPM Release", + expectedHeadBranch: args.workflowRef, rerunFailed: false, }), ); @@ -482,6 +555,8 @@ export async function verifyBetaRelease( id: args.workflowRuns.npmTelegram, label: "NPM Telegram Beta E2E", repo: args.repo, + expectedWorkflowName: "NPM Telegram Beta E2E", + expectedHeadBranch: args.workflowRef, rerunFailed: false, }), ); @@ -503,6 +578,7 @@ export async function verifyBetaRelease( releaseVersion: args.version, releaseTag: args.tag, npmDistTag: args.distTag, + pluginSelection: args.pluginSelection, openclawNpmIntegrity: openclawNpm.integrity, githubReleaseUrl: releaseUrl, pluginNpmPackageCount: npmPlugins.length, diff --git a/scripts/openclaw-npm-postpublish-verify.ts b/scripts/openclaw-npm-postpublish-verify.ts index d4d33a73165..f9fcd369c9f 100644 --- a/scripts/openclaw-npm-postpublish-verify.ts +++ b/scripts/openclaw-npm-postpublish-verify.ts @@ -161,7 +161,7 @@ export function collectInstalledBundledRuntimeSidecarPaths(packageRoot: string): export function normalizeInstalledBinaryVersion(output: string): string { const trimmed = output.trim(); - const versionMatch = /\b\d{4}\.\d{1,2}\.\d{1,2}(?:-\d+|-beta\.\d+)?\b/u.exec(trimmed); + const versionMatch = /\b\d{4}\.\d{1,2}\.\d{1,2}(?:-\d+|-(?:alpha|beta)\.\d+)?\b/u.exec(trimmed); return versionMatch?.[0] ?? trimmed; } diff --git a/scripts/release-candidate-checklist.mjs b/scripts/release-candidate-checklist.mjs index 88f6cb0ccf0..b4710588898 100644 --- a/scripts/release-candidate-checklist.mjs +++ b/scripts/release-candidate-checklist.mjs @@ -26,6 +26,7 @@ Options: --full-release-run Reuse successful Full Release Validation run. --npm-preflight-run Reuse successful OpenClaw NPM Release preflight run. --skip-dispatch Require both run ids; do not dispatch workflows. + --skip-local-generated-check Do not run local generated release baseline checks before dispatch. --skip-parallels Do not run local Parallels fresh/update beta smoke. --skip-telegram Do not run NPM Telegram E2E against the prepared tarball. --telegram-provider-mode mock-openai|live-frontier. Default: ${DEFAULT_TELEGRAM_PROVIDER_MODE} @@ -57,6 +58,7 @@ export function parseArgs(argv) { pluginPublishScope: DEFAULT_PLUGIN_SCOPE, plugins: "", skipDispatch: false, + skipLocalGeneratedCheck: false, skipParallels: false, skipTelegram: false, telegramProviderMode: DEFAULT_TELEGRAM_PROVIDER_MODE, @@ -89,6 +91,9 @@ export function parseArgs(argv) { case "--skip-dispatch": options.skipDispatch = true; break; + case "--skip-local-generated-check": + options.skipLocalGeneratedCheck = true; + break; case "--skip-parallels": options.skipParallels = true; break; @@ -256,6 +261,14 @@ function runAndEcho(command, args) { return `${result.stdout ?? ""}${result.stderr ?? ""}`; } +function runLocalGeneratedCheckIfNeeded(options) { + if (options.skipLocalGeneratedCheck) { + return { status: "skipped", reason: "operator skipped --skip-local-generated-check" }; + } + run("pnpm", ["release:generated:check"]); + return { status: "passed", command: "pnpm release:generated:check" }; +} + export function parseRunIdFromDispatchOutput(output) { return output.match(/actions\/runs\/([0-9]+)/u)?.[1] ?? ""; } @@ -446,6 +459,9 @@ export function buildPublishCommand(options) { ["release_profile", options.releaseProfile], ["wait_for_clawhub", "false"], ]; + if (options.npmTelegramRunId) { + fields.push(["npm_telegram_run_id", options.npmTelegramRunId]); + } if (options.plugins.trim()) { fields.push(["plugins", options.plugins]); } @@ -506,7 +522,7 @@ function validateFullManifest(manifest, params) { async function runParallelsIfNeeded(options) { if (options.skipParallels) { - return { status: "skipped" }; + return { status: "skipped", reason: "operator skipped --skip-parallels" }; } const version = options.tag.replace(/^v/u, ""); run("pnpm", [ @@ -559,6 +575,7 @@ async function main() { options.workflowRef ||= currentBranch(); options.outputDir ||= join(".artifacts", "release-candidate", options.tag); const targetSha = gitRevParse(`${options.tag}^{}`); + const localGeneratedCheck = runLocalGeneratedCheckIfNeeded(options); if (!options.fullReleaseRunId && !options.skipDispatch) { const workflowFile = "full-release-validation.yml"; @@ -647,6 +664,7 @@ async function main() { const parallels = await runParallelsIfNeeded(options); const npmTelegram = await runTelegramIfNeeded(options, npmArtifactName); + options.npmTelegramRunId = npmTelegram.runId ?? ""; const pluginNpmPlan = await collectPluginPlanWithRetry( "scripts/plugin-npm-release-plan.ts", options, @@ -670,6 +688,7 @@ async function main() { npmPreflight: npmArtifactName, fullReleaseValidation: fullArtifactName, }, + localGeneratedCheck, tarball: { name: basename(tarballPath), sha256: actualTarballSha, @@ -695,12 +714,15 @@ async function main() { `- npm preflight: ${options.npmPreflightRunId} ${npmRun.url}`, `- npm preflight artifact: ${npmArtifactName}`, `- full release artifact: ${fullArtifactName}`, + `- local generated release checks: ${localGeneratedCheck.status}${ + localGeneratedCheck.reason ? ` (${localGeneratedCheck.reason})` : "" + }`, `- tarball: ${basename(tarballPath)}`, `- tarball sha256: ${actualTarballSha}`, `- npm dist-tag: ${options.npmDistTag}`, `- plugin npm plan: ${pluginNpmPlan.packages?.length ?? 0} packages`, `- ClawHub plan: ${pluginClawHubPlan.packages?.length ?? 0} packages`, - `- Parallels: ${parallels.status}`, + `- Parallels: ${parallels.status}${parallels.reason ? ` (${parallels.reason})` : ""}`, `- NPM Telegram E2E: ${npmTelegram.status}${ npmTelegram.runId ? ` ${npmTelegram.runId} ${npmTelegram.url}` : "" }`, diff --git a/test/openclaw-npm-postpublish-verify.test.ts b/test/openclaw-npm-postpublish-verify.test.ts index 659252922e6..0521fc99cba 100644 --- a/test/openclaw-npm-postpublish-verify.test.ts +++ b/test/openclaw-npm-postpublish-verify.test.ts @@ -152,6 +152,9 @@ describe("normalizeInstalledBinaryVersion", () => { expect(normalizeInstalledBinaryVersion("OpenClaw 2026.4.8-beta.1 (9ece252)")).toBe( "2026.4.8-beta.1", ); + expect(normalizeInstalledBinaryVersion("OpenClaw 2026.4.8-alpha.1 (9ece252)")).toBe( + "2026.4.8-alpha.1", + ); }); }); diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index fa2ea9c6da2..fe5f3b017ce 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -933,6 +933,20 @@ describe("package artifact reuse", () => { expect(releaseWorkflow).toContain("Plugin npm run ID"); expect(releaseWorkflow).toContain("Plugin ClawHub run ID"); expect(releaseWorkflow).toContain("OpenClaw npm run ID"); + expect(releaseWorkflow).toContain("npm_telegram_run_id"); + expect(releaseWorkflow).toContain("Approve release gate from OpenClaw Release Publish wrapper"); + expect(releaseWorkflow).toContain("release:verify-beta"); + expect(releaseWorkflow).toContain('--workflow-ref "${CHILD_WORKFLOW_REF}"'); + expect(releaseWorkflow).toContain('verify_args+=(--plugins "${PLUGINS}")'); + expect(releaseWorkflow).toContain("openclaw-release-postpublish-evidence"); + expect(releaseWorkflow).toContain("Failed child job summary"); + expect(releaseWorkflow).toContain("final verification waits for ClawHub"); + expect(releaseWorkflow).toContain( + '[[ "${WAIT_FOR_CLAWHUB}" == "true" || "${PUBLISH_OPENCLAW_NPM}" == "true" ]]', + ); + expect(releaseWorkflow.lastIndexOf("create_or_update_github_release")).toBeLessThan( + releaseWorkflow.indexOf('if [[ -n "${clawhub_pid}" ]] && ! wait "${clawhub_pid}"'), + ); expect(releaseWorkflow).toContain("finished with ${conclusion} in ${duration_label}"); }); diff --git a/test/scripts/release-beta-verifier.test.ts b/test/scripts/release-beta-verifier.test.ts index ef1c53583d9..323ec43364c 100644 --- a/test/scripts/release-beta-verifier.test.ts +++ b/test/scripts/release-beta-verifier.test.ts @@ -12,6 +12,8 @@ describe("parseReleaseVerifyBetaArgs", () => { distTag: "beta", repo: "openclaw/openclaw", registry: "https://clawhub.ai", + workflowRef: undefined, + pluginSelection: [], evidenceOut: undefined, skipPostpublish: false, rerunFailedClawHub: false, @@ -24,6 +26,10 @@ describe("parseReleaseVerifyBetaArgs", () => { parseReleaseVerifyBetaArgs([ "--", "2026.5.10-beta.3", + "--workflow-ref", + "release/2026.5.10", + "--plugins", + "@openclaw/plugin-a,@openclaw/plugin-b", "--full-release-validation-run", "10", "--openclaw-npm-run", @@ -45,6 +51,8 @@ describe("parseReleaseVerifyBetaArgs", () => { distTag: "beta", repo: "openclaw/openclaw", registry: "https://clawhub.ai", + workflowRef: "release/2026.5.10", + pluginSelection: ["@openclaw/plugin-a", "@openclaw/plugin-b"], evidenceOut: ".artifacts/release-evidence.json", skipPostpublish: true, rerunFailedClawHub: true, diff --git a/test/scripts/release-candidate-checklist.test.ts b/test/scripts/release-candidate-checklist.test.ts index eb7524b5f71..c5ab82a8814 100644 --- a/test/scripts/release-candidate-checklist.test.ts +++ b/test/scripts/release-candidate-checklist.test.ts @@ -35,6 +35,26 @@ describe("release candidate checklist", () => { expect(buildPublishCommand(options)).toContain("'plugin_publish_scope=all-publishable'"); }); + it("carries the Telegram proof run into the publish command when available", () => { + const options = { + ...parseArgs([ + "--tag", + "v2026.5.14-beta.3", + "--workflow-ref", + "release/2026.5.14", + "--full-release-run", + "111", + "--npm-preflight-run", + "222", + "--skip-dispatch", + ]), + workflowRef: "release/2026.5.14", + npmTelegramRunId: "333", + }; + + expect(buildPublishCommand(options)).toContain("'npm_telegram_run_id=333'"); + }); + it("requires explicit plugin names for selected plugin publish scope", () => { expect(() => parseArgs(["--tag", "v2026.5.14-beta.3", "--plugin-publish-scope", "selected"]),