mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-12 19:00:43 +00:00
399 lines
16 KiB
YAML
399 lines
16 KiB
YAML
name: OpenClaw Release Publish
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: Release tag to publish, for example v2026.5.1-alpha.1 or v2026.5.1-beta.1
|
|
required: true
|
|
type: string
|
|
preflight_run_id:
|
|
description: Successful OpenClaw NPM Release preflight run id, required when publish_openclaw_npm=true
|
|
required: false
|
|
type: string
|
|
npm_dist_tag:
|
|
description: npm dist-tag for the OpenClaw package
|
|
required: true
|
|
default: beta
|
|
type: choice
|
|
options:
|
|
- alpha
|
|
- beta
|
|
- latest
|
|
plugin_publish_scope:
|
|
description: Plugin publish scope to run before OpenClaw publish
|
|
required: true
|
|
default: all-publishable
|
|
type: choice
|
|
options:
|
|
- selected
|
|
- all-publishable
|
|
plugins:
|
|
description: Comma-separated plugin package names when plugin_publish_scope=selected
|
|
required: false
|
|
type: string
|
|
publish_openclaw_npm:
|
|
description: Publish the OpenClaw npm package after plugin npm succeeds; ClawHub may still run
|
|
required: true
|
|
default: true
|
|
type: boolean
|
|
wait_for_clawhub:
|
|
description: Wait for ClawHub plugin publish before marking this workflow complete
|
|
required: true
|
|
default: false
|
|
type: boolean
|
|
|
|
permissions:
|
|
actions: write
|
|
contents: write
|
|
|
|
concurrency:
|
|
group: openclaw-release-publish-${{ inputs.tag }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
NODE_VERSION: "24.x"
|
|
PNPM_VERSION: "10.32.1"
|
|
|
|
jobs:
|
|
resolve_release_target:
|
|
name: Resolve release target
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
outputs:
|
|
sha: ${{ steps.ref.outputs.sha }}
|
|
steps:
|
|
- name: Validate inputs
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
|
|
PUBLISH_OPENCLAW_NPM: ${{ inputs.publish_openclaw_npm && 'true' || 'false' }}
|
|
PLUGIN_PUBLISH_SCOPE: ${{ inputs.plugin_publish_scope }}
|
|
PLUGINS: ${{ inputs.plugins }}
|
|
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
|
WORKFLOW_REF: ${{ github.ref }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-(alpha|beta)\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]]; then
|
|
echo "Invalid release tag: ${RELEASE_TAG}" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "${RELEASE_TAG}" == *"-alpha."* && "${RELEASE_NPM_DIST_TAG}" != "alpha" ]]; then
|
|
echo "Alpha prerelease tags must publish OpenClaw to npm dist-tag alpha." >&2
|
|
exit 1
|
|
fi
|
|
if [[ "${RELEASE_TAG}" == *"-beta."* && "${RELEASE_NPM_DIST_TAG}" != "beta" ]]; then
|
|
echo "Beta prerelease tags must publish OpenClaw to npm dist-tag beta." >&2
|
|
exit 1
|
|
fi
|
|
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && -z "${PREFLIGHT_RUN_ID}" ]]; then
|
|
echo "publish_openclaw_npm=true requires preflight_run_id." >&2
|
|
exit 1
|
|
fi
|
|
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${WORKFLOW_REF}" != "refs/heads/main" && ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]]; then
|
|
echo "publish_openclaw_npm=true requires dispatching this workflow from main or release/YYYY.M.D." >&2
|
|
exit 1
|
|
fi
|
|
if [[ "${PLUGIN_PUBLISH_SCOPE}" == "selected" && -z "${PLUGINS}" ]]; then
|
|
echo "plugin_publish_scope=selected requires plugins." >&2
|
|
exit 1
|
|
fi
|
|
if [[ "${PLUGIN_PUBLISH_SCOPE}" == "all-publishable" && -n "${PLUGINS}" ]]; then
|
|
echo "plugin_publish_scope=all-publishable must not include plugins." >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Checkout release tag
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: refs/tags/${{ inputs.tag }}
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
- name: Setup Node environment
|
|
uses: ./.github/actions/setup-node-env
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
pnpm-version: ${{ env.PNPM_VERSION }}
|
|
install-bun: "false"
|
|
|
|
- name: Resolve checked-out release ref
|
|
id: ref
|
|
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Validate release tag is reachable from main or release branch
|
|
run: |
|
|
set -euo pipefail
|
|
git fetch --no-tags origin \
|
|
+refs/heads/main:refs/remotes/origin/main \
|
|
'+refs/heads/release/*:refs/remotes/origin/release/*'
|
|
if git merge-base --is-ancestor HEAD origin/main; then
|
|
exit 0
|
|
fi
|
|
while IFS= read -r release_ref; do
|
|
if git merge-base --is-ancestor HEAD "${release_ref}"; then
|
|
exit 0
|
|
fi
|
|
done < <(git for-each-ref --format='%(refname)' refs/remotes/origin/release)
|
|
echo "Release tag must point to a commit reachable from main or release/*." >&2
|
|
exit 1
|
|
|
|
- name: Verify plugin versions were synced for this release
|
|
run: pnpm plugins:sync:check
|
|
|
|
- name: Summarize release target
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
TARGET_SHA: ${{ steps.ref.outputs.sha }}
|
|
run: |
|
|
{
|
|
echo "### Release target"
|
|
echo
|
|
echo "- Tag: \`${RELEASE_TAG}\`"
|
|
echo "- SHA: \`${TARGET_SHA}\`"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
publish:
|
|
name: Publish plugins, then OpenClaw
|
|
needs: [resolve_release_target]
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 360
|
|
steps:
|
|
- name: Dispatch publish workflows
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TARGET_SHA: ${{ needs.resolve_release_target.outputs.sha }}
|
|
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
|
|
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
|
PLUGIN_PUBLISH_SCOPE: ${{ inputs.plugin_publish_scope }}
|
|
PLUGINS: ${{ inputs.plugins }}
|
|
PUBLISH_OPENCLAW_NPM: ${{ inputs.publish_openclaw_npm && 'true' || 'false' }}
|
|
WAIT_FOR_CLAWHUB: ${{ inputs.wait_for_clawhub && 'true' || 'false' }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
dispatch_workflow() {
|
|
local workflow="$1"
|
|
shift
|
|
|
|
local before_json dispatch_output run_id
|
|
before_json="$(gh run list --repo "$GITHUB_REPOSITORY" --workflow "$workflow" --event workflow_dispatch --limit 100 --json databaseId --jq '[.[].databaseId]')"
|
|
|
|
dispatch_output="$(gh workflow run --repo "$GITHUB_REPOSITORY" "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"
|
|
printf '%s\n' "$dispatch_output" >&2
|
|
run_id="$(
|
|
printf '%s\n' "$dispatch_output" |
|
|
sed -nE 's#.*actions/runs/([0-9]+).*#\1#p' |
|
|
tail -n 1
|
|
)"
|
|
|
|
if [[ -z "$run_id" ]]; then
|
|
for _ in $(seq 1 60); do
|
|
run_id="$(
|
|
BEFORE_IDS="$before_json" gh run list --repo "$GITHUB_REPOSITORY" --workflow "$workflow" --event workflow_dispatch --limit 50 --json databaseId,createdAt \
|
|
--jq 'map(select(.databaseId as $id | (env.BEFORE_IDS | fromjson | index($id) | not))) | sort_by(.createdAt) | reverse | .[0].databaseId // empty'
|
|
)"
|
|
if [[ -n "$run_id" ]]; then
|
|
break
|
|
fi
|
|
sleep 5
|
|
done
|
|
fi
|
|
|
|
if [[ -z "${run_id:-}" ]]; then
|
|
echo "Could not find dispatched run for ${workflow}." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}" >&2
|
|
{
|
|
echo "- ${workflow}: dispatched (https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id})"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
printf '%s\n' "${run_id}"
|
|
}
|
|
|
|
wait_for_run() {
|
|
local workflow="$1"
|
|
local run_id="$2"
|
|
local status conclusion url updated_at last_state
|
|
|
|
last_state=""
|
|
while true; do
|
|
run_json="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json status,url,updatedAt)"
|
|
status="$(printf '%s' "$run_json" | jq -r '.status')"
|
|
if [[ "$status" == "completed" ]]; then
|
|
break
|
|
fi
|
|
url="$(printf '%s' "$run_json" | jq -r '.url')"
|
|
updated_at="$(printf '%s' "$run_json" | jq -r '.updatedAt')"
|
|
state="${status}:${updated_at}"
|
|
if [[ "$state" != "$last_state" ]]; then
|
|
echo "${workflow} still ${status} (updated ${updated_at}): ${url}"
|
|
last_state="$state"
|
|
fi
|
|
sleep 30
|
|
done
|
|
|
|
conclusion="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json conclusion --jq '.conclusion')"
|
|
url="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json url --jq '.url')"
|
|
echo "${workflow} finished with ${conclusion}: ${url}"
|
|
{
|
|
echo "- ${workflow}: ${conclusion} (${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
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
wait_for_run_background() {
|
|
local workflow="$1"
|
|
local run_id="$2"
|
|
local result_file="$3"
|
|
(
|
|
if wait_for_run "${workflow}" "${run_id}"; then
|
|
printf 'success\n' > "${result_file}"
|
|
else
|
|
printf 'failure\n' > "${result_file}"
|
|
fi
|
|
) &
|
|
wait_run_pid="$!"
|
|
}
|
|
|
|
create_or_update_github_release() {
|
|
local release_version notes_version title notes_file changelog_file latest_arg prerelease_args
|
|
release_version="${RELEASE_TAG#v}"
|
|
notes_version="${release_version}"
|
|
if [[ "${notes_version}" =~ ^([0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*)-(alpha|beta)\.[1-9][0-9]*$ ]]; then
|
|
notes_version="${BASH_REMATCH[1]}"
|
|
fi
|
|
title="openclaw ${release_version}"
|
|
changelog_file="${RUNNER_TEMP}/CHANGELOG.md"
|
|
notes_file="${RUNNER_TEMP}/release-notes.md"
|
|
|
|
gh api --repo "$GITHUB_REPOSITORY" "repos/${GITHUB_REPOSITORY}/contents/CHANGELOG.md?ref=${TARGET_SHA}" \
|
|
--jq '.content' | base64 --decode > "${changelog_file}"
|
|
awk -v version="${notes_version}" '
|
|
$0 == "## " version { in_section = 1; next }
|
|
/^## / && in_section { exit }
|
|
in_section { print }
|
|
' "${changelog_file}" > "${notes_file}"
|
|
if [[ ! -s "${notes_file}" ]]; then
|
|
echo "CHANGELOG.md does not contain release notes for ${notes_version}." >&2
|
|
exit 1
|
|
fi
|
|
|
|
prerelease_args=()
|
|
latest_arg="--latest=false"
|
|
if [[ "${RELEASE_TAG}" == *"-alpha."* || "${RELEASE_TAG}" == *"-beta."* ]]; then
|
|
prerelease_args=(--prerelease)
|
|
elif [[ "${RELEASE_NPM_DIST_TAG}" == "latest" ]]; then
|
|
latest_arg="--latest"
|
|
fi
|
|
|
|
if gh release view "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
|
gh release edit "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" \
|
|
--title "${title}" \
|
|
--notes-file "${notes_file}" \
|
|
"${prerelease_args[@]}"
|
|
else
|
|
gh release create "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" \
|
|
--verify-tag \
|
|
--title "${title}" \
|
|
--notes-file "${notes_file}" \
|
|
"${prerelease_args[@]}" \
|
|
"${latest_arg}"
|
|
fi
|
|
echo "- GitHub release: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${RELEASE_TAG}" >> "$GITHUB_STEP_SUMMARY"
|
|
}
|
|
|
|
{
|
|
echo "### Publish sequence"
|
|
echo
|
|
echo "- Workflow ref: \`${CHILD_WORKFLOW_REF}\`"
|
|
echo "- Release tag: \`${RELEASE_TAG}\`"
|
|
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"
|
|
else
|
|
echo "- OpenClaw npm publish: skipped by input"
|
|
fi
|
|
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
|
|
echo "- Workflow completion waits for ClawHub"
|
|
else
|
|
echo "- Workflow completion does not wait for ClawHub; monitor the dispatched ClawHub run separately"
|
|
fi
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
npm_args=(-f publish_scope="${PLUGIN_PUBLISH_SCOPE}" -f ref="${TARGET_SHA}")
|
|
clawhub_args=(-f publish_scope="${PLUGIN_PUBLISH_SCOPE}" -f ref="${TARGET_SHA}")
|
|
if [[ -n "${PLUGINS}" ]]; then
|
|
npm_args+=(-f plugins="${PLUGINS}")
|
|
clawhub_args+=(-f plugins="${PLUGINS}")
|
|
fi
|
|
|
|
plugin_npm_run_id="$(dispatch_workflow plugin-npm-release.yml "${npm_args[@]}")"
|
|
plugin_clawhub_run_id="$(dispatch_workflow plugin-clawhub-release.yml "${clawhub_args[@]}")"
|
|
|
|
if ! wait_for_run plugin-npm-release.yml "${plugin_npm_run_id}"; then
|
|
echo "Plugin npm publish failed; cancelling ClawHub publish child ${plugin_clawhub_run_id}." >&2
|
|
gh run cancel --repo "$GITHUB_REPOSITORY" "${plugin_clawhub_run_id}" >/dev/null 2>&1 || true
|
|
exit 1
|
|
fi
|
|
|
|
openclaw_npm_run_id=""
|
|
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
|
|
openclaw_npm_run_id="$(dispatch_workflow openclaw-npm-release.yml \
|
|
-f tag="${RELEASE_TAG}" \
|
|
-f preflight_only=false \
|
|
-f preflight_run_id="${PREFLIGHT_RUN_ID}" \
|
|
-f npm_dist_tag="${RELEASE_NPM_DIST_TAG}")"
|
|
else
|
|
echo "- OpenClaw npm publish: skipped by input" >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
|
|
clawhub_result=""
|
|
clawhub_pid=""
|
|
if [[ "${WAIT_FOR_CLAWHUB}" == "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}"
|
|
clawhub_pid="${wait_run_pid}"
|
|
else
|
|
echo "- plugin-clawhub-release.yml: not awaited (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
|
|
openclaw_result=""
|
|
openclaw_pid=""
|
|
if [[ -n "${openclaw_npm_run_id}" ]]; then
|
|
openclaw_result="$RUNNER_TEMP/openclaw-npm-result.txt"
|
|
wait_run_pid=""
|
|
wait_for_run_background openclaw-npm-release.yml "${openclaw_npm_run_id}" "${openclaw_result}"
|
|
openclaw_pid="${wait_run_pid}"
|
|
fi
|
|
|
|
failed=0
|
|
if [[ -n "${clawhub_pid}" ]] && ! wait "${clawhub_pid}"; then
|
|
failed=1
|
|
fi
|
|
if [[ -n "${openclaw_pid}" ]] && ! wait "${openclaw_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
|
|
fi
|
|
if [[ "${failed}" != "0" ]]; then
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -n "${openclaw_npm_run_id}" ]]; then
|
|
create_or_update_github_release
|
|
fi
|