ci: harden release publish evidence

This commit is contained in:
Peter Steinberger
2026-05-17 06:29:27 +01:00
parent c4d8e0be18
commit 1ceebf8a01
9 changed files with 301 additions and 42 deletions

View File

@@ -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

View File

@@ -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-<version>-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 <run-id> --plugin-npm-run <run-id> --plugin-clawhub-run <run-id>`
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

View File

@@ -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<string, unknown>;
@@ -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 -- <version> [--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 -- <version> [--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 ?? "<missing>"}, expected ${params.expectedWorkflowName}.`,
);
}
const event = readString(run.event);
if (event !== "workflow_dispatch") {
throw new Error(
`${params.label}: run ${params.id} event is ${event ?? "<missing>"}, 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 ?? "<missing>"}, 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,

View File

@@ -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;
}

View File

@@ -26,6 +26,7 @@ Options:
--full-release-run <id> Reuse successful Full Release Validation run.
--npm-preflight-run <id> 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 <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}` : ""
}`,

View File

@@ -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",
);
});
});

View File

@@ -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}");
});

View File

@@ -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,

View File

@@ -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"]),