mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 08:34:46 +00:00
ci: harden release publish evidence
This commit is contained in:
141
.github/workflows/openclaw-release-publish.yml
vendored
141
.github/workflows/openclaw-release-publish.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}` : ""
|
||||
}`,
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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}");
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]),
|
||||
|
||||
Reference in New Issue
Block a user