ci: route extended-stable through shared release publish

This commit is contained in:
Dallin Romney
2026-08-13 13:39:10 +08:00
parent 7b9e793bf3
commit bcfcd9eca7
20 changed files with 421 additions and 215 deletions
@@ -273,7 +273,8 @@ Extended-stable has one release commit and one canonical GitHub Release body.
After version prep and approved backports, regenerate `## YYYY.M.P` with the
regular manifest and original-main-PR provenance rules. Land it by PR, then
validate the final branch tip before tagging. The release closeout renders that
tag-owned section into a non-Latest release page. Re-audit after a product
tag-owned section into the shared draft before the parent pipeline publishes
the non-Latest release page. Re-audit after a product
backport; a tooling-only repair needs no changelog entry. Never rewrite a
published tag or changelog.
+2 -2
View File
@@ -8,8 +8,8 @@ description: "Run or recover OpenClaw macOS release signing, notarization, appca
Use with `$release-openclaw-maintainer`, `$release-openclaw-ci`, `$one-password`, and `$release-private` if it exists when stable macOS assets, release-ops mac preflight, notarization, appcast promotion, or mac release recovery is involved.
This is a regular stable-release skill. Do not invoke it for extended-stable;
that track's notes-only GitHub Release does not inherit macOS assets or appcast
promotion.
that track's GitHub Release carries shared validation evidence but does not
inherit macOS assets or appcast promotion.
## Credentials
@@ -225,8 +225,9 @@ or publication work. Treat backport discovery and preparation as an ability of
this release skill, not as a separate release workflow.
The backport flow covers mainline inventory, private-security reconciliation,
approval, the staging PR, and proof handoff. After it lands, use the sequence
below. Never route `.33+` through regular beta/stable release steps.
approval, the staging PR, and proof handoff. After it lands, use the shared
release pipeline with the extended-stable track inputs below, never regular
beta/stable track inputs.
Extended-stable requires a visible **SDK/config backport warning** whenever a
candidate changes the public plugin SDK or a config/default/schema/migration
@@ -257,7 +258,9 @@ on pinned current `main` as the exact command and validation contract.
3. Run complete Full Release Validation against the canonical branch with
`release_profile=stable`; save its run ID and successful `run_attempt`.
Prefer the trusted main-pinned harness, which attests the immutable target
SHA in its v3 manifest. Any candidate branch change invalidates both gates.
SHA in its Full Release Validation evidence manifest with schema
`openclaw.release-validation-evidence/v3`. Any candidate branch change
invalidates both gates.
4. Require the tip still equals the frozen SHA, then create signed `vYYYY.M.P`.
Never move or delete a final tag; later source changes need a new patch.
5. Require the saved validation run to be complete and successful, bind its
@@ -265,38 +268,28 @@ on pinned current `main` as the exact command and validation contract.
canonical branch, a direct current-`main` run whose workflow SHA is still
reachable from main, or a trusted main-pinned `release-ci/*` harness. Reject
narrow reruns.
6. Dispatch `plugin-npm-release.yml` from the same branch with
`publish_scope=all-publishable`, the full release SHA as `ref`, and
`npm_dist_tag=extended-stable`. Require complete exact-version and selector
readback, then save the successful plugin run ID.
7. Publish core with the tag, `npm_dist_tag=extended-stable`, all three run IDs,
and `full_release_validation_run_attempt=<saved-attempt>`. Normally dispatch
from the canonical branch. For a workflow-only recovery after the candidate
is immutable, dispatch trusted current `main` with
`release_candidate_branch=extended-stable/YYYY.M.33`; it still publishes the
tag checkout and accepts canonical-branch, current-main, or trusted-pinned
validation evidence; the prepared tarball and every evidence identity must
still match the candidate SHA.
8. From a clean current-`main` checkout, run
6. Dispatch trusted current-main `OpenClaw Release Publish` with the tag,
`npm_dist_tag=extended-stable`, `publish_openclaw_npm=true`, the saved
preflight and Full Release Validation run IDs, and the saved validation run
attempt. The parent derives `release_candidate_branch`, creates the draft,
publishes every official npm plugin and core under `extended-stable`,
attaches release evidence, skips ClawHub/native publication, publishes
Docker, and finalizes the release with `latest=false`.
7. From a clean current-`main` checkout, run
`node --import tsx scripts/openclaw-npm-postpublish-verify.ts YYYY.M.P`.
Verify signatures, provenance, inventories, exact versions, and selectors.
Use the generated repair only for the root selector; repair other selectors
with approved credential-isolated tooling. Never republish a version.
9. Require `Docker Release` to verify default, slim, browser, and architecture
8. Require `Docker Release` to verify default, slim, browser, and architecture
images in GHCR and Docker Hub, including attestations and platform versions.
It must advance only
`extended-stable`, `extended-stable-slim`, and `extended-stable-browser` by
digest and refuse automatic rollback. For alias repair, dispatch the
approval-gated `docker-channel-promote.yml` from current `main` with the exact
tag; never rebuild or move the release tag.
10. Dispatch the extended-stable `OpenClaw Release Publish` closeout. It must
reverify the exact npm selector and tarball, then publish Docker. Only after
Docker succeeds does the finalizer render the canonical release notes and
create the public non-prerelease GitHub Release with `latest=false`. Re-run
failed jobs to retry a failed finalizer without repeating successful Docker
work; a fresh dispatch safely repeats same-version Docker verification and
promotion. Do not publish macOS, Windows, mobile, website, ClawHub, npm
`latest`, or private dist-tag artifacts from this path.
It must advance only the `extended-stable*` aliases by digest and refuse
automatic rollback. For alias repair, dispatch the approval-gated
`docker-channel-promote.yml` from current `main` with the exact tag; never
rebuild or move the release tag. Re-run failed jobs for finalizer recovery.
Use `publish_docker_only=true` only when npm was already published outside
the parent pipeline. This track selects npm `extended-stable`, leaving npm
`latest` unchanged, and does not publish macOS, Windows, mobile, website,
ClawHub, or private dist-tag artifacts.
## Keep release channel naming aligned
@@ -24,9 +24,9 @@ extended-stable package and publication constraints.
- Carry the complete current-main Docker release-channel unit in the tagged
tree: workflow, promoter, policy, shared release-version classifier, tests,
and workflow validation. GitHub evaluates tag-push workflows from that tree.
- Exclude ClawHub publication, GitHub Release assets, the macOS app, Windows
Hub, mobile apps, website downloads, npm `latest`, and private-repository
dist-tags. The release closeout creates a notes-only GitHub Release page.
- Exclude ClawHub publication, native-app artifacts, website downloads, npm
`latest`, and private-repository dist-tags. The shared release pipeline
attaches dependency and validation evidence to the non-Latest GitHub Release.
- Review the complete mainline delta using the shared evidence-driven audit.
Do not stop after the first obvious fixes or consider public PRs, titles, or
dependency bumps the complete source set.
@@ -155,7 +155,7 @@ fi
Do not use GitHub's latest nonprerelease Release as the source of truth. The
npm `extended-stable` selector remains authoritative for the active line; its
notes-only GitHub Release is always created with `latest=false`. In bootstrap
evidence-bearing GitHub Release is always created with `latest=false`. In bootstrap
mode, record the approving maintainer and approved base commit. Stop before
discovery or mutation if npm, the canonical branch, tags, package versions,
approved base, or protected `main` disagree.
@@ -365,9 +365,8 @@ Report:
harness compatibility repair, and superseded validation runs;
- remaining security, release, or maintainer approvals;
- the coordinated PR URL or why no PR was opened;
- exact intended Docker images and aliases, plus explicit confirmation that
the notes-only, non-Latest GitHub Release is the only other publication
planned.
- exact intended Docker images and aliases, GitHub Release evidence assets, and
explicit confirmation that no native or ClawHub artifacts are planned.
Then follow the parent skill's publish and recovery sequence. Keep exact
branch/tag/package/run identity, never republish for selector repair, and move
+15 -12
View File
@@ -13,10 +13,10 @@ publish skill; use `$release-openclaw-maintainer` before changing release state.
- Resolve short suffixes like `.27` to the concrete CalVer version from the
current date/context, then say the resolved version.
- Resolve the track first. Regular beta/stable uses a GitHub Release and the
platform graph; extended-stable uses a notes-only GitHub Release, its
canonical branch, npm selector, and Gateway surfaces. Do not require one
track's artifacts from the other.
- Resolve the track first. Both tracks use the shared GitHub Release evidence
ledger. Regular beta/stable also uses the platform graph; extended-stable
uses its canonical branch, npm selector, and Gateway surfaces. Do not require
one track's native or ClawHub artifacts from the other.
- Verify live state. Do not trust local checkout state, release notes, or old
memory as current truth.
- If the checkout is dirty or divergent, use it only for scripts/reference.
@@ -77,17 +77,20 @@ Use these checks only for the regular orchestrated release track.
## Extended-stable checks
Extended-stable has a notes-only GitHub Release ledger. Verify it alongside
Extended-stable has a GitHub Release with shared release evidence but no native
or ClawHub artifacts. Verify it alongside
the live tag, workflow, registry, provenance, and image state.
1. **Identity:** require final `v<VERSION>` at patch `33+`, with no suffix,
contained in `extended-stable/YYYY.M.33`. Only an active candidate must equal
the tip. Root and every publishable official plugin must declare `<VERSION>`.
Require the Git tag and a public, non-prerelease GitHub Release whose title
and canonical body match the tag. Require `isLatest=false` and no native or
platform assets.
2. **Workflow chain:** find successful preflight, complete validation, plugin
npm, and core publish runs on the canonical branch and SHA. Validation must
and canonical body match the tag. Require `isLatest=false`, the dependency
evidence, immutable Full Release Validation manifest, postpublish evidence,
and their checksums. Require no native or ClawHub assets.
2. **Workflow chain:** find the successful parent release run plus its
preflight, complete validation, plugin npm, and core publish children.
Validation must
use `rerun_group=all`, `release_profile=stable`, blocking soak/performance,
and the saved attempt. Core publish must reference all three run IDs and bind
its manifest, workflow ref, and tarball digest to the release SHA.
@@ -102,14 +105,14 @@ the live tag, workflow, registry, provenance, and image state.
5. **Docker:** verify exact default, slim, browser, and architecture images and
attestations in both registries. Only the three `extended-stable*` aliases may
resolve to those digests. Require the successful `OpenClaw Release Publish`
closeout run whose Docker job precedes GitHub Release publication. Repair
parent run whose Docker job precedes GitHub Release publication. Repair
aliases through current-main `Docker Channel Promotion` for the exact tag,
without rebuilding.
6. **Recovery:** never republish. Use the generated command only for the root
selector and approved credential-isolated tooling for others, then repeat
complete readback. Do not require ClawHub, native/mobile apps, website,
private dist-tags, or regular `latest`. Require the notes-only GitHub
Release, but do not require regular-release assets or platform evidence.
private dist-tags, or regular `latest`. Require shared release evidence, but
do not require regular native or ClawHub assets.
## Shared live smoke
@@ -979,6 +979,8 @@ jobs:
PLUGIN_NPM_RUN_ID: ${{ inputs.plugin_npm_run_id }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
EXPECTED_EXTENDED_STABLE_BRANCH: ${{ inputs.release_candidate_branch || github.ref_name }}
EXPECTED_ORCHESTRATOR_BRANCH: ${{ inputs.release_candidate_branch != '' && github.ref_name || '' }}
EXPECTED_ORCHESTRATOR_SHA: ${{ inputs.release_candidate_branch != '' && github.workflow_sha || '' }}
RUN_KIND: plugin
run: |
set -euo pipefail
+70 -38
View File
@@ -20,7 +20,7 @@ on:
required: false
type: string
windows_node_tag:
description: Exact openclaw-windows-node release tag, required for stable OpenClaw publish
description: Exact openclaw-windows-node release tag, required for regular stable OpenClaw publish
required: false
type: string
windows_node_installer_digests:
@@ -36,7 +36,7 @@ on:
required: false
type: string
npm_dist_tag:
description: npm dist-tag for the OpenClaw package
description: npm dist-tag passed to the plugin and core publishers; extended-stable leaves npm latest unchanged
required: true
default: beta
type: choice
@@ -58,12 +58,12 @@ on:
required: false
type: string
publish_openclaw_npm:
description: Publish the OpenClaw npm package after plugin npm succeeds; ClawHub may still run
description: Publish the OpenClaw npm package under npm_dist_tag after plugin npm succeeds; ClawHub may still run
required: true
default: true
type: boolean
publish_docker_only:
description: Complete an already-published extended-stable release by verifying npm, publishing Docker, then publishing its GitHub Release
description: Recover an already-published extended-stable release by verifying npm, publishing Docker, then publishing its missing GitHub Release
required: true
default: false
type: boolean
@@ -186,24 +186,24 @@ jobs:
exit 1
fi
fi
if [[ "${RELEASE_NPM_DIST_TAG}" == "extended-stable" && "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
echo "Extended-stable core npm publication stays on the canonical extended-stable release flow; use publish_docker_only=true only after its registry readback." >&2
exit 1
fi
stable_release=true
if [[ "${RELEASE_TAG}" == *"-alpha."* || "${RELEASE_TAG}" == *"-beta."* ]]; then
stable_release=false
fi
regular_stable_release="${stable_release}"
if [[ "${RELEASE_NPM_DIST_TAG}" == "extended-stable" ]]; then
regular_stable_release=false
fi
if [[ -n "${WINDOWS_NODE_TAG}" && ! "${WINDOWS_NODE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$ ]]; then
echo "windows_node_tag must be an explicit openclaw-windows-node release tag, not latest: ${WINDOWS_NODE_TAG}" >&2
exit 1
fi
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${stable_release}" == "true" && -z "${WINDOWS_NODE_TAG}" ]]; then
echo "Stable OpenClaw publish requires an explicit windows_node_tag." >&2
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${regular_stable_release}" == "true" && -z "${WINDOWS_NODE_TAG}" ]]; then
echo "Regular stable OpenClaw publish requires an explicit windows_node_tag." >&2
exit 1
fi
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${stable_release}" == "true" && -z "${WINDOWS_NODE_INSTALLER_DIGESTS}" ]]; then
echo "Stable OpenClaw publish requires candidate-approved windows_node_installer_digests." >&2
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${regular_stable_release}" == "true" && -z "${WINDOWS_NODE_INSTALLER_DIGESTS}" ]]; then
echo "Regular stable OpenClaw publish requires candidate-approved windows_node_installer_digests." >&2
exit 1
fi
tideclaw_alpha_publish=false
@@ -250,10 +250,10 @@ jobs:
;;
esac
expected_validation_branch="${WORKFLOW_REF#refs/heads/}"
if [[ "${PUBLISH_DOCKER_ONLY}" == "true" ]]; then
if [[ "${RELEASE_NPM_DIST_TAG}" == "extended-stable" ]]; then
if [[ ! "${RELEASE_TAG}" =~ ^v([0-9]{4})\.([1-9][0-9]*)\.([1-9][0-9]*)$ ]] || \
(( 10#${BASH_REMATCH[3]:-0} < 33 )); then
echo "Extended-stable closeout requires a final .33+ release tag." >&2
echo "Extended-stable publication requires a final .33+ release tag." >&2
exit 1
fi
expected_validation_branch="extended-stable/${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.33"
@@ -262,7 +262,7 @@ jobs:
- name: Validate stable Windows source release
id: windows_source
if: ${{ inputs.publish_openclaw_npm }}
if: ${{ inputs.publish_openclaw_npm && inputs.npm_dist_tag != 'extended-stable' }}
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
@@ -282,11 +282,11 @@ jobs:
exit 1
fi
if [[ "$(printf '%s' "${source_json}" | jq -r '.isDraft')" == "true" ]]; then
echo "Stable OpenClaw publish requires a published Windows source release." >&2
echo "Regular stable OpenClaw publish requires a published Windows source release." >&2
exit 1
fi
if [[ "$(printf '%s' "${source_json}" | jq -r '.isPrerelease')" == "true" ]]; then
echo "Stable OpenClaw publish requires a non-prerelease Windows source release." >&2
echo "Regular stable OpenClaw publish requires a non-prerelease Windows source release." >&2
exit 1
fi
@@ -550,7 +550,7 @@ jobs:
- name: Validate release tag is reachable from a trusted release branch
env:
EXPECTED_VALIDATION_BRANCH: ${{ steps.inputs.outputs.expected_validation_branch }}
PUBLISH_DOCKER_ONLY: ${{ inputs.publish_docker_only && 'true' || 'false' }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
RELEASE_TAG: ${{ inputs.tag }}
WORKFLOW_REF_NAME: ${{ github.ref_name }}
run: |
@@ -559,7 +559,7 @@ jobs:
+refs/heads/main:refs/remotes/origin/main \
'+refs/heads/release/*:refs/remotes/origin/release/*' \
'+refs/heads/extended-stable/*:refs/remotes/origin/extended-stable/*'
if [[ "${PUBLISH_DOCKER_ONLY}" == "true" ]]; then
if [[ "${RELEASE_NPM_DIST_TAG}" == "extended-stable" ]]; then
expected_ref="refs/remotes/origin/${EXPECTED_VALIDATION_BRANCH}"
if git show-ref --verify --quiet "${expected_ref}" && \
git merge-base --is-ancestor HEAD "${expected_ref}"; then
@@ -687,6 +687,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
PARENT_WORKFLOW_BRANCH: ${{ github.ref_name }}
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
PLUGIN_PUBLISH_SCOPE: ${{ inputs.plugin_publish_scope }}
PLUGINS: ${{ inputs.plugins }}
run: |
@@ -715,6 +716,9 @@ jobs:
if [[ -n "${PLUGINS// }" ]]; then
plan_args+=(--plugins "${PLUGINS}")
fi
if [[ "${RELEASE_NPM_DIST_TAG}" == "extended-stable" ]]; then
plan_args+=(--skip-clawhub)
fi
CLAWHUB_REGISTRY="${CLAWHUB_REGISTRY:-https://clawhub.ai}" \
node --import tsx \
@@ -809,7 +813,7 @@ jobs:
--output "${notes_file}"
- name: Write Android release approval
if: ${{ inputs.publish_openclaw_npm && !contains(inputs.tag, '-alpha.') && !contains(inputs.tag, '-beta.') }}
if: ${{ inputs.publish_openclaw_npm && inputs.npm_dist_tag != 'extended-stable' && !contains(inputs.tag, '-alpha.') && !contains(inputs.tag, '-beta.') }}
env:
RELEASE_PUBLISH_BRANCH: ${{ github.ref_name }}
RELEASE_PUBLISH_RUN_ID: ${{ github.run_id }}
@@ -839,13 +843,13 @@ jobs:
NODE
- name: Attest Android release approval
if: ${{ inputs.publish_openclaw_npm && !contains(inputs.tag, '-alpha.') && !contains(inputs.tag, '-beta.') }}
if: ${{ inputs.publish_openclaw_npm && inputs.npm_dist_tag != 'extended-stable' && !contains(inputs.tag, '-alpha.') && !contains(inputs.tag, '-beta.') }}
uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0
with:
subject-path: ${{ runner.temp }}/android-release-approval/approval.json
- name: Upload Android release approval
if: ${{ inputs.publish_openclaw_npm && !contains(inputs.tag, '-alpha.') && !contains(inputs.tag, '-beta.') }}
if: ${{ inputs.publish_openclaw_npm && inputs.npm_dist_tag != 'extended-stable' && !contains(inputs.tag, '-alpha.') && !contains(inputs.tag, '-beta.') }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: android-release-approval-${{ github.run_id }}
@@ -884,11 +888,11 @@ jobs:
openclaw_npm_expected_workflow_sha="${PARENT_WORKFLOW_SHA}"
is_stable_release() {
[[ "${RELEASE_TAG}" != *"-alpha."* && "${RELEASE_TAG}" != *"-beta."* ]]
[[ "${RELEASE_NPM_DIST_TAG}" != "extended-stable" && "${RELEASE_TAG}" != *"-alpha."* && "${RELEASE_TAG}" != *"-beta."* ]]
}
is_android_release() {
[[ "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*)?$ ]]
[[ "${RELEASE_NPM_DIST_TAG}" != "extended-stable" && "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*)?$ ]]
}
verify_child_run_sha() {
@@ -1932,7 +1936,7 @@ jobs:
local validation_target_sha validation_url validation_workflow_ref
local -a verify_args
skip_clawhub="${1:-false}"
skip_clawhub="${1:-${force_skip_clawhub}}"
release_version="${RELEASE_TAG#v}"
evidence_path="${POSTPUBLISH_EVIDENCE_DIR}/release-postpublish-evidence.json"
@@ -2044,7 +2048,7 @@ jobs:
telegram_line="- npm Telegram beta E2E: not supplied"
fi
clawhub_runtime_state_path="${RUNNER_TEMP}/openclaw-release-clawhub-runtime-state-proof.json"
write_clawhub_runtime_state false "${clawhub_runtime_state_path}"
write_clawhub_runtime_state "${force_skip_clawhub}" "${clawhub_runtime_state_path}"
clawhub_line="$(jq -r '.proofLines.normal' "${clawhub_runtime_state_path}")"
clawhub_bootstrap_line="$(jq -r '.proofLines.bootstrap' "${clawhub_runtime_state_path}")"
windows_line=""
@@ -2122,16 +2126,28 @@ jobs:
bootstrap_summary_ref="$(jq -er '.bootstrap.ref | select(type == "string" and length > 0)' "${CLAWHUB_PLAN_PATH}")"
bootstrap_summary_sha="$(jq -er '.bootstrapWorkflowSha | select(test("^[a-f0-9]{40}$"))' "${CLAWHUB_PLAN_PATH}")"
force_skip_clawhub=false
if [[ "${RELEASE_NPM_DIST_TAG}" == "extended-stable" ]]; then
force_skip_clawhub=true
fi
{
echo "### Publish sequence"
echo
echo "- Workflow ref: \`${CHILD_WORKFLOW_REF}\`"
echo "- Normal ClawHub workflow ref: release tag \`${RELEASE_TAG}\`"
echo "- ClawHub bootstrap workflow ref: \`${bootstrap_summary_ref}\` at \`${bootstrap_summary_sha}\`"
if [[ "${force_skip_clawhub}" == "true" ]]; then
echo "- ClawHub: skipped by release track"
else
echo "- Normal ClawHub workflow ref: release tag \`${RELEASE_TAG}\`"
echo "- ClawHub bootstrap workflow ref: \`${bootstrap_summary_ref}\` at \`${bootstrap_summary_sha}\`"
fi
echo "- Release tag: \`${RELEASE_TAG}\`"
echo "- Release SHA: \`${TARGET_SHA}\`"
echo "- Release approval: this workflow job"
echo "- Plugin npm and ClawHub publish: dispatched in parallel"
if [[ "${force_skip_clawhub}" == "true" ]]; then
echo "- Plugin npm publish: dispatched before core npm"
else
echo "- Plugin npm and ClawHub publish: dispatched in parallel"
fi
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
echo "- OpenClaw npm publish: starts after plugin npm succeeds"
else
@@ -2143,7 +2159,9 @@ jobs:
if is_stable_release && [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
echo "- Windows Hub promotion: promoted concurrently with the OpenClaw npm publish; required before the GitHub release can be published"
fi
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
if [[ "${force_skip_clawhub}" == "true" ]]; then
:
elif [[ "${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"
@@ -2172,6 +2190,12 @@ jobs:
if [[ -n "${PLUGINS}" ]]; then
npm_args+=(-f plugins="${PLUGINS}")
fi
if [[ "${RELEASE_NPM_DIST_TAG}" == "extended-stable" ]]; then
npm_args+=(
-f npm_dist_tag=extended-stable
-f release_candidate_branch="${{ needs.resolve_release_target.outputs.expected_validation_branch }}"
)
fi
plugin_npm_run_id="$(dispatch_workflow plugin-npm-release.yml "${npm_args[@]}")"
plugin_clawhub_run_id=""
@@ -2237,14 +2261,22 @@ jobs:
if [[ "${openclaw_npm_already_published}" == "true" ]]; then
echo "- OpenClaw npm publish: already on npm; resuming postpublish stages" >> "$GITHUB_STEP_SUMMARY"
else
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 full_release_validation_run_id="${FULL_RELEASE_VALIDATION_RUN_ID}" \
-f full_release_validation_run_attempt="${FULL_RELEASE_VALIDATION_RUN_ATTEMPT}" \
-f release_publish_run_id="${GITHUB_RUN_ID}" \
-f npm_dist_tag="${RELEASE_NPM_DIST_TAG}")"
openclaw_npm_args=(
-f tag="${RELEASE_TAG}"
-f preflight_only=false
-f preflight_run_id="${PREFLIGHT_RUN_ID}"
-f full_release_validation_run_id="${FULL_RELEASE_VALIDATION_RUN_ID}"
-f full_release_validation_run_attempt="${FULL_RELEASE_VALIDATION_RUN_ATTEMPT}"
-f release_publish_run_id="${GITHUB_RUN_ID}"
-f npm_dist_tag="${RELEASE_NPM_DIST_TAG}"
)
if [[ "${RELEASE_NPM_DIST_TAG}" == "extended-stable" ]]; then
openclaw_npm_args+=(
-f release_candidate_branch="${{ needs.resolve_release_target.outputs.expected_validation_branch }}"
-f plugin_npm_run_id="${plugin_npm_run_id}"
)
fi
openclaw_npm_run_id="$(dispatch_workflow openclaw-npm-release.yml "${openclaw_npm_args[@]}")"
echo "- OpenClaw npm run ID: \`${openclaw_npm_run_id}\`" >> "$GITHUB_STEP_SUMMARY"
fi
else
+29 -3
View File
@@ -46,6 +46,10 @@ on:
description: Approved OpenClaw Release Publish workflow run id
required: false
type: string
release_candidate_branch:
description: Canonical extended-stable branch when trusted main release tooling publishes its immutable target
required: false
type: string
preflight_only:
description: Prepare and verify immutable plugin npm artifacts without publishing
required: true
@@ -101,6 +105,7 @@ jobs:
PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
RELEASE_PUBLISH_RUN_ID: ${{ github.event_name == 'workflow_dispatch' && inputs.release_publish_run_id || '' }}
RELEASE_CANDIDATE_BRANCH: ${{ github.event_name == 'workflow_dispatch' && inputs.release_candidate_branch || '' }}
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
WORKFLOW_REF: ${{ github.ref }}
WORKFLOW_SHA: ${{ github.workflow_sha }}
@@ -119,6 +124,10 @@ jobs:
echo "Plugin npm preflight must not include release_publish_run_id." >&2
exit 1
fi
if [[ -n "${RELEASE_CANDIDATE_BRANCH// }" ]]; then
echo "Plugin npm preflight must not include release_candidate_branch." >&2
exit 1
fi
fi
if [[ "${NPM_DIST_TAG}" == "extended-stable" ]]; then
if [[ "${PUBLISH_SCOPE}" != "all-publishable" || -n "${RELEASE_PLUGINS// }" ]]; then
@@ -138,12 +147,32 @@ jobs:
release_month="${BASH_REMATCH[2]}"
extended_stable_branch="extended-stable/${release_year}.${release_month}.33"
timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags origin "+refs/heads/${extended_stable_branch}:refs/remotes/origin/${extended_stable_branch}"
if [[ -n "${RELEASE_CANDIDATE_BRANCH// }" ]]; then
if [[ "${RELEASE_CANDIDATE_BRANCH}" != "${extended_stable_branch}" || "${WORKFLOW_REF}" != "refs/heads/main" ]]; then
echo "release_candidate_branch must be ${extended_stable_branch} and is only valid with trusted main workflow tooling." >&2
exit 1
fi
timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
if ! git merge-base --is-ancestor "${WORKFLOW_SHA}" origin/main; then
echo "Extended-stable plugin workflow revision is not reachable from current main." >&2
exit 1
fi
if git merge-base --is-ancestor HEAD "refs/remotes/origin/${extended_stable_branch}"; then
exit 0
fi
echo "Extended-stable plugin target must be reachable from ${extended_stable_branch}." >&2
exit 1
fi
if [[ "${WORKFLOW_REF}" == "refs/heads/${extended_stable_branch}" ]] && [[ "$(git rev-parse HEAD)" == "$(git rev-parse "refs/remotes/origin/${extended_stable_branch}")" ]]; then
exit 0
fi
echo "Extended-stable plugin npm publishes must run from ${extended_stable_branch} at its exact branch tip." >&2
exit 1
fi
if [[ -n "${RELEASE_CANDIDATE_BRANCH// }" ]]; then
echo "release_candidate_branch is only valid for extended-stable publication." >&2
exit 1
fi
timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags origin \
+refs/heads/main:refs/remotes/origin/main \
'+refs/heads/release/*:refs/remotes/origin/release/*'
@@ -1274,9 +1303,6 @@ jobs:
cp \
scripts/lib/tsx-cli-shim.mjs \
.publication-target/scripts/lib/tsx-cli-shim.mjs
cp \
scripts/lib/tsx-cli-shim.mjs \
.publication-target/scripts/lib/tsx-cli-shim.mjs
- name: Setup OIDC publication target
if: steps.publication_evidence.outputs.publish_route == 'npm-oidc'
+12 -13
View File
@@ -171,11 +171,12 @@ Gateway extended-stable runs npm preflight, Full Release Validation, and plugin
npm release from `extended-stable/YYYY.M.33`; core publish consumes those three
run IDs plus the validation attempt. Complete evidence may come from the
canonical branch, reachable current-main tooling, or the trusted main-pinned
`release-ci/*` harness when its v3 manifest binds the exact target and attempt.
The post-npm `OpenClaw Release Publish` closeout publishes Gateway images and
only the `extended-stable*` aliases, then publishes the notes-only GitHub
Release with `latest=false`; it skips ClawHub, native-app, website, and private
dist-tag surfaces. See [Monthly Gateway extended-stable
`release-ci/*` harness when its Full Release Validation evidence manifest uses
schema `openclaw.release-validation-evidence/v3` and binds the exact target and
attempt. `OpenClaw Release Publish` then runs the shared draft, plugin npm, core
npm, evidence, Docker, and finalization stages with
`npm_dist_tag=extended-stable`. That selection leaves npm `latest` unchanged;
the track skips ClawHub, native-app, website, and private dist-tag surfaces. See [Monthly Gateway extended-stable
publication](/reference/RELEASING#monthly-gateway-extended-stable-publication)
for commands and recovery.
@@ -315,14 +316,12 @@ Focused plugin-only repairs use `plugin_publish_scope=selected` with a nonempty
package list. Plugin-only `all-publishable` runs require the same immutable npm
preflight and Full Release Validation evidence as a core publish.
Extended-stable uses this workflow only after npm publication, with
`publish_openclaw_npm=false` and `publish_docker_only=true`. That closeout
rechecks immutable npm evidence and the exact canonical validation branch,
publishes Docker, and only then renders canonical release notes and creates the
public notes-only non-Latest GitHub Release without entering plugin, ClawHub,
or native-app publication. Re-running failed jobs retries the finalizer without
repeating a successful Docker job; a fresh dispatch repeats the idempotent
same-version Docker verification and promotion.
Extended-stable uses the same workflow with `publish_openclaw_npm=true` and
`npm_dist_tag=extended-stable`. The parent publishes all official npm plugins
and core under that selector, creates the draft and attaches immutable release
evidence, skips ClawHub/native publication, publishes Docker, and finalizes the
non-Latest GitHub Release. `publish_docker_only=true` is reserved for recovery
when npm was already published outside the parent pipeline.
```bash
gh workflow run openclaw-release-publish.yml \
+46 -94
View File
@@ -18,7 +18,8 @@ OpenClaw exposes four user-facing update channels:
Extended-stable ships the trailing month's Gateway, official npm plugins, and
Docker images without moving regular `latest` or `main` selectors. Each release
also has a notes-only GitHub Release page that is never marked Latest.
also has a GitHub Release with shared validation evidence that is never marked
Latest.
Tideclaw alpha builds are a separate internal prerelease track (npm dist-tag `alpha`), covered under [NPM workflow inputs](#npm-workflow-inputs) and [Release test boxes](#release-test-boxes).
@@ -109,67 +110,35 @@ equals `RELEASE_SHA`, then push signed `vYYYY.M.P`. Later changes need the next
patch; never move or delete the tag. Tagging fixes the immutable release
identity; it does not publish Docker images.
### Publish the npm packages
### Publish the release
Publish every npm-publishable official plugin from the same SHA and save the
successful run ID:
Run the normal release orchestrator from trusted current `main`, selecting the
extended-stable npm track:
```bash
RELEASE_SHA="$(git rev-parse HEAD)"
gh workflow run plugin-npm-release.yml \
--ref extended-stable/YYYY.M.33 \
-f publish_scope=all-publishable \
-f ref="$RELEASE_SHA" \
-f npm_dist_tag=extended-stable
```
The workflow covers all `all-publishable` packages, including unchanged ones,
and verifies every exact version and selector. Reruns reuse published versions.
Then publish the prepared core tarball with all three saved run identities:
```bash
gh workflow run openclaw-npm-release.yml \
--ref extended-stable/YYYY.M.33 \
-f tag=vYYYY.M.P \
-f preflight_only=false \
-f npm_dist_tag=extended-stable \
-f preflight_run_id=<npm-preflight-run-id> \
-f full_release_validation_run_id=<full-validation-run-id> \
-f full_release_validation_run_attempt=<full-validation-run-attempt> \
-f plugin_npm_run_id=<plugin-npm-run-id>
```
If the immutable candidate has already passed its saved preflight and Full
Release Validation but core publication needs a workflow-only recovery, dispatch
the trusted current-`main` workflow instead. Keep the same tag and evidence
identities; do not move the tag or republish plugins:
```bash
gh workflow run openclaw-npm-release.yml \
gh workflow run openclaw-release-publish.yml \
--ref main \
-f tag=vYYYY.M.P \
-f preflight_only=false \
-f npm_dist_tag=extended-stable \
-f release_candidate_branch=extended-stable/YYYY.M.33 \
-f preflight_run_id=<npm-preflight-run-id> \
-f full_release_validation_run_id=<full-validation-run-id> \
-f full_release_validation_run_attempt=<full-validation-run-attempt> \
-f plugin_npm_run_id=<plugin-npm-run-id>
-f npm_dist_tag=extended-stable \
-f plugin_publish_scope=all-publishable \
-f publish_openclaw_npm=true
```
This recovery path checks out and publishes the immutable tag and requires the
canonical branch implied by that tag. It accepts Full Release Validation
evidence from the canonical candidate branch directly, from current `main`
directly when its workflow SHA is reachable from current `main`, or from the
trusted main-pinned harness. Every accepted form must attest the immutable
tag's SHA. Use it only when the candidate source and recorded evidence are
unchanged.
The parent derives the canonical `extended-stable/YYYY.M.33` branch from the
tag and passes it to both npm children. It creates the draft GitHub Release,
publishes every `all-publishable` official plugin and core under the
`extended-stable` selector, verifies registry bytes, attaches dependency and
validation evidence, publishes Docker, then finalizes the release with
`latest=false`. ClawHub and native-app stages are disabled by the selected
track. Use the lower-level plugin/core workflows only for an approved recovery;
never republish an immutable version.
For non-production rehearsal only, add
`-f bypass_extended_stable_guard=true` to preflight and publish. It bypasses the
month guard only, never canonical-ref, SHA/tag/version equality, provenance,
approval, or readback checks. Never use it for production.
For non-production child-workflow rehearsal only, the lower-level npm workflow
has `bypass_extended_stable_guard=true`. The normal parent publish does not
expose that bypass. Never use it for production.
### Verify and recover
@@ -194,43 +163,23 @@ them. Never republish an immutable version.
Require `Docker Release` to verify exact default, slim, browser, and architecture
images in GHCR and Docker Hub, including attestations and platform versions. It
must advance only
`extended-stable`, `extended-stable-slim`, and `extended-stable-browser` by
digest; regular aliases remain unchanged and automatic rollback is rejected.
After that core registry readback succeeds, start Docker publication only through
`OpenClaw Release Publish`. Its extended-stable closeout path rechecks the
saved npm preflight artifact, exact `Full Release Validation` evidence, exact npm
version and `extended-stable` selector, and published tarball digest. It then
calls the reusable `Docker Release` workflow. Only after Docker succeeds does
the finalizer render and validate the canonical release notes and create the
public non-prerelease GitHub Release with `latest=false`. Re-run failed jobs to
retry a failed release-page finalizer without repeating successful Docker work;
a fresh workflow dispatch safely repeats same-version Docker verification and
promotion. A tag push never publishes Docker images or a release page by
itself:
```bash
gh workflow run openclaw-release-publish.yml \
--ref main \
-f tag=vYYYY.M.P \
-f preflight_run_id=<npm-preflight-run-id> \
-f full_release_validation_run_id=<full-validation-run-id> \
-f full_release_validation_run_attempt=<full-validation-run-attempt> \
-f npm_dist_tag=extended-stable \
-f publish_openclaw_npm=false \
-f publish_docker_only=true
```
must advance only `extended-stable`, `extended-stable-slim`, and
`extended-stable-browser` by digest; regular aliases remain unchanged and
automatic rollback is rejected. Confirm the GitHub Release contains the shared
dependency, Full Release Validation, and postpublish evidence assets but no
native-app assets.
For alias repair, run approval-gated `Docker Channel Promotion` from current
`main` with the tag. It repeats digest, attestation, and platform checks, allows
an explicit rollback, and never rebuilds images.
Slack, Discord, and Codex are the initial documented support surfaces, not a
release allowlist: every npm-publishable official plugin ships. The regular
checklist alone owns beta/`latest`, GitHub Release assets, ClawHub, native apps,
mobile, website, and private dist-tags; do not run those steps for this Gateway
path. Extended-stable owns only its notes-only, non-Latest GitHub Release page.
release allowlist: every npm-publishable official plugin ships. The shared
pipeline attaches dependency, Full Release Validation, and postpublish evidence
to the extended-stable GitHub Release. The selected npm tag is
`extended-stable`, so npm `latest` remains unchanged. Do not publish ClawHub
packages, native apps, website artifacts, or private dist-tags from this Gateway
track.
## Regular release operator checklist
@@ -541,19 +490,18 @@ For package-candidate Telegram proof, enable `telegram_mode=mock-openai` or `tel
## Regular release publish automation
For beta, `latest`, plugin, GitHub Release, and platform publication,
`OpenClaw Release Publish` is the normal mutating entrypoint. The monthly
`.33+` Gateway extended-stable path also uses its closeout after npm
publication. The regular path orchestrates the trusted-publisher workflows in
the order the release needs:
For beta, `latest`, extended-stable, plugin, GitHub Release, and platform
publication, `OpenClaw Release Publish` is the normal mutating entrypoint. It
orchestrates the trusted-publisher workflows in the order the selected release
track needs:
1. Check out the release tag and resolve its commit SHA.
2. Verify the tag is reachable from `main` or `release/*` (or a Tideclaw alpha branch for alpha prereleases).
3. Run `pnpm plugins:sync:check`.
4. Dispatch `Plugin NPM Release` with `publish_scope=all-publishable` and `ref=<release-sha>`.
5. Dispatch `Plugin ClawHub Release` with the same scope and SHA.
5. Dispatch `Plugin ClawHub Release` with the same scope and SHA, except for the extended-stable track.
6. Dispatch `OpenClaw NPM Release` with the release tag, npm dist-tag, and saved `preflight_run_id` after verifying the saved `full_release_validation_run_id` and exact run attempt.
7. Verify the published npm package and selector readback, then call reusable `Docker Release` with the immutable tag and SHA. For stable releases, create or update the GitHub release as a draft, dispatch `Windows Node Release` with the explicit `windows_node_tag` and candidate-approved `windows_node_installer_digests`, and verify the canonical Windows installer/checksum assets. Also dispatch `Android Release` to build the exact-tag signed APK plus checksum and provenance. Finalize the GitHub release only after Docker and both native asset contracts succeed.
7. Verify the published npm package and selector readback, attach release evidence, then call reusable `Docker Release` with the immutable tag and SHA. Regular stable releases also dispatch Windows and Android promotion against the draft and require those asset contracts. Extended-stable skips those native stages. Finalize the same draft only after every enabled stage succeeds.
Beta publish example:
@@ -664,7 +612,9 @@ SHA-256, and npm integrity. A mismatch requires a new package version.
behavior or `npm_dist_tag=extended-stable` for the guarded monthly path. The
extended-stable option requires `publish_scope=all-publishable`, an empty
`plugins` input, a final patch at or above `33`, and the canonical
`extended-stable/YYYY.M.33` branch at its exact tip. It never moves plugin
`extended-stable/YYYY.M.33` branch at its exact tip, or the same immutable
target dispatched by trusted current-main `OpenClaw Release Publish` with that
canonical branch named in `release_candidate_branch`. It never moves plugin
`latest` or `beta`. New package versions receive `extended-stable` atomically
through OIDC trusted publication (`npm publish --tag extended-stable`); this
source workflow does not use token-authenticated `npm dist-tag add`. Retries
@@ -677,11 +627,11 @@ readback confirms that every exact package and `extended-stable` tag converged.
- `preflight_run_id`: successful `OpenClaw NPM Release` preflight run id; required when `publish_openclaw_npm=true` or `plugin_publish_scope=all-publishable`
- `full_release_validation_run_id`: successful `Full Release Validation` run id; required when `publish_openclaw_npm=true` or `plugin_publish_scope=all-publishable`
- `full_release_validation_run_attempt`: exact positive attempt paired with `full_release_validation_run_id`; required whenever the run id is provided
- `windows_node_tag`: exact non-prerelease `openclaw/openclaw-windows-node` release tag; required for stable OpenClaw publish
- `windows_node_tag`: exact non-prerelease `openclaw/openclaw-windows-node` release tag; required for regular stable OpenClaw publish
- `windows_node_installer_digests`: candidate-approved compact JSON map of the current Windows installer names to their pinned `sha256:` digests; required for stable OpenClaw publish
- `npm_telegram_run_id`: optional successful `NPM Telegram Beta E2E` run id to include in final release evidence
- `npm_dist_tag`: npm target tag for the OpenClaw package, one of `alpha`, `beta`, `latest`, or `extended-stable`
- `publish_docker_only`: legacy input name for the extended-stable recovery/closeout path. It requires `publish_openclaw_npm=false`, complete preflight and Full Release Validation evidence, then verifies the exact npm package, selector, and tarball digest, publishes Docker, and finally renders canonical notes and creates the public notes-only GitHub Release with `latest=false`.
- `npm_dist_tag`: npm target tag passed to the plugin and core publishers, one of `alpha`, `beta`, `latest`, or `extended-stable`; only `latest` moves npm `latest`
- `publish_docker_only`: recovery-only path for an extended-stable version whose npm publication already completed outside the parent pipeline. It requires `publish_openclaw_npm=false`, complete preflight and Full Release Validation evidence, then verifies the exact npm package, selector, and tarball digest, publishes Docker, and creates a missing public GitHub Release with `latest=false`. Do not use it for a normal extended-stable release.
- `plugin_publish_scope`: defaults to `all-publishable`; use `selected` only for focused plugin-only repair work with `publish_openclaw_npm=false`
- `plugins`: comma-separated `@openclaw/*` package names when `plugin_publish_scope=selected`
- `publish_openclaw_npm`: defaults to `true`; set `false` only when using the workflow as a plugin-only repair orchestrator
@@ -703,7 +653,9 @@ Rules:
## Regular beta/latest stable release sequence
This legacy sequence is for the regular orchestrated release that also owns plugins, GitHub Release, Windows, and other platform work. It is not the monthly `.33+` Gateway extended-stable path documented at the top of this page.
This sequence uses the same orchestrator as extended-stable. Its `beta` or
`latest` track additionally enables ClawHub and the applicable native/platform
stages.
When cutting a regular orchestrated stable release:
+2 -1
View File
@@ -58,7 +58,8 @@ passes that branch to the validation-evidence verifier. It does not substitute
the closeout workflow's `main` ref for the candidate branch.
The helper's temporary `release-ci/*` ref is valid only when every child uses
the pinned trusted workflow SHA and the v3 manifest binds the canonical branch,
the pinned trusted workflow SHA and the Full Release Validation evidence
manifest uses schema `openclaw.release-validation-evidence/v3` to bind the canonical branch,
exact release commit, run ID, and attempt. Reject narrow runs, stale attempts,
untrusted workflow SHAs, and mismatched targets.
@@ -32,6 +32,7 @@ type OpenClawReleaseClawHubPlanArgs = {
releasePublishRunId: string;
pluginPublishScope: PluginReleaseSelectionMode;
plugins: string[];
skipClawHub?: boolean;
};
type OpenClawReleaseClawHubPlan = {
@@ -244,6 +245,7 @@ export function parseOpenClawReleaseClawHubPlanArgs(
let pluginPublishScope: PluginReleaseSelectionMode | undefined;
let plugins: string[] = [];
let pluginsFlagProvided = false;
let skipClawHub = false;
for (let index = 0; index < values.length; index += 1) {
const arg = values[index];
@@ -285,6 +287,9 @@ export function parseOpenClawReleaseClawHubPlanArgs(
plugins = parsePluginReleaseSelection(next());
pluginsFlagProvided = true;
break;
case "--skip-clawhub":
skipClawHub = true;
break;
default:
throw new Error(`Unknown argument: ${arg}`);
}
@@ -314,6 +319,7 @@ export function parseOpenClawReleaseClawHubPlanArgs(
releasePublishRunId: requireArg(releasePublishRunId, "--release-publish-run-id"),
pluginPublishScope: resolvedPluginPublishScope,
plugins,
skipClawHub,
};
}
@@ -335,6 +341,38 @@ export async function buildOpenClawReleaseClawHubPlan(
"releasePublishRunAttempt",
);
const releasePublishRunId = requireArg(args.releasePublishRunId, "releasePublishRunId");
if (args.skipClawHub) {
return {
bootstrapWorkflowSha,
clawHubWorkflowRef: releaseTag,
releasePublishBranch,
normal: createDispatchTarget({
workflow: "plugin-clawhub-release.yml",
ref: releaseTag,
packages: [],
releasePublishRunId,
releasePublishBranch,
includePublishScope: true,
}),
bootstrap: createDispatchTarget({
workflow: "plugin-clawhub-new.yml",
ref: bootstrapWorkflowRef,
packages: [],
releasePublishRunId,
releasePublishBranch,
includePublishScope: false,
}),
summary: {
normalCount: 0,
bootstrapCount: 0,
missingTrustedPublisherCount: 0,
normalPlugins: "",
bootstrapPlugins: "",
missingTrustedPlugins: "",
},
verifier: { clawHubWorkflowRef: releaseTag },
};
}
const plan = await collectPluginClawHubReleasePlan({
rootDir: options.rootDir ?? resolve("."),
selection: args.plugins,
@@ -175,6 +175,8 @@ export function validateExtendedStableRunIdentity({
npmDistTag,
expectedBranch,
expectedSha,
expectedOrchestratorBranch,
expectedOrchestratorSha,
}) {
const expectedWorkflowName =
kind === "preflight"
@@ -198,10 +200,16 @@ export function validateExtendedStableRunIdentity({
);
}
}
if (
npmDistTag === "extended-stable" &&
(run.headBranch !== expectedBranch || run.headSha !== expectedSha)
) {
const directTargetIdentity = run.headBranch === expectedBranch && run.headSha === expectedSha;
const orchestratedPluginIdentity =
kind === "plugin" &&
typeof expectedOrchestratorBranch === "string" &&
expectedOrchestratorBranch.length > 0 &&
typeof expectedOrchestratorSha === "string" &&
expectedOrchestratorSha.length > 0 &&
run.headBranch === expectedOrchestratorBranch &&
run.headSha === expectedOrchestratorSha;
if (npmDistTag === "extended-stable" && !directTargetIdentity && !orchestratedPluginIdentity) {
throw new Error(
`Referenced extended-stable ${kind} run must have headBranch=${expectedBranch} and headSha=${expectedSha}; got ${run.headBranch ?? "<missing>"} and ${run.headSha ?? "<missing>"}.`,
);
@@ -490,6 +498,8 @@ async function main() {
npmDistTag: process.env.RELEASE_NPM_DIST_TAG,
expectedBranch: process.env.EXPECTED_EXTENDED_STABLE_BRANCH,
expectedSha: process.env.EXPECTED_RELEASE_SHA,
expectedOrchestratorBranch: process.env.EXPECTED_ORCHESTRATOR_BRANCH,
expectedOrchestratorSha: process.env.EXPECTED_ORCHESTRATOR_SHA,
});
console.log(`Verified referenced ${process.env.RUN_KIND} run.`);
return;
+3 -1
View File
@@ -1122,7 +1122,9 @@ export function validateTrustedProducerIdentity(evidence, client, verifier, trus
}
if (shaPinned) {
if (manifest.version !== 3) {
throw new Error("SHA-pinned release evidence requires a v3 manifest");
throw new Error(
"SHA-pinned release evidence requires a Full Release Validation manifest with schema openclaw.release-validation-evidence/v3",
);
}
if (!manifest.workflowRef.startsWith(`release-ci/${manifest.workflowSha.slice(0, 12)}-`)) {
throw new Error("SHA-pinned release evidence branch does not match its workflow SHA");
+52
View File
@@ -1426,6 +1426,58 @@ describe("buildOpenClawReleaseClawHubPlan", () => {
});
});
it("returns a zero-dispatch plan without reading ClawHub when the release track excludes it", async () => {
const plan = await buildOpenClawReleaseClawHubPlan(
{
bootstrapWorkflowRef: "main",
bootstrapWorkflowSha: "d".repeat(40),
releaseTag: "v2026.6.35",
releaseSha: "a".repeat(40),
releasePublishBranch: "main",
releasePublishRunAttempt: "1",
releasePublishRunId: "12345",
pluginPublishScope: "all-publishable",
plugins: [],
skipClawHub: true,
},
{
fetchImpl: () => {
throw new Error("ClawHub must not be queried for an excluded release track.");
},
},
);
expect(plan.normal).toMatchObject({ shouldDispatch: false, packages: [] });
expect(plan.bootstrap).toMatchObject({ shouldDispatch: false, packages: [] });
expect(plan.summary).toEqual({
normalCount: 0,
bootstrapCount: 0,
missingTrustedPublisherCount: 0,
normalPlugins: "",
bootstrapPlugins: "",
missingTrustedPlugins: "",
});
expect(
parseOpenClawReleaseClawHubPlanArgs([
"--bootstrap-workflow-ref",
"main",
"--bootstrap-workflow-sha",
"d".repeat(40),
"--release-tag",
"v2026.6.35",
"--release-sha",
"a".repeat(40),
"--release-publish-branch",
"main",
"--release-publish-run-attempt",
"1",
"--release-publish-run-id",
"12345",
"--skip-clawhub",
]).skipClawHub,
).toBe(true);
});
it("rejects incompatible all-publishable plugin selection args", () => {
expect(() =>
parseOpenClawReleaseClawHubPlanArgs([
@@ -367,6 +367,47 @@ describe("extended-stable npm run identity", () => {
}
});
it("accepts a plugin run dispatched by the trusted main orchestrator for the exact target", () => {
const workflowSha = "c".repeat(40);
const pluginRun = {
workflowName: "Plugin NPM Release",
displayTitle: `Plugin NPM Release [extended-stable] ${sha}`,
event: "workflow_dispatch",
status: "completed",
conclusion: "success",
headBranch: "main",
headSha: workflowSha,
};
expect(() =>
validateExtendedStableRunIdentity({
run: pluginRun,
kind: "plugin",
npmDistTag: "extended-stable",
expectedBranch: branch,
expectedSha: sha,
expectedOrchestratorBranch: "main",
expectedOrchestratorSha: workflowSha,
}),
).not.toThrow();
for (const changes of [
{ headBranch: "release/2026.6.35" },
{ headSha: "not-a-sha" },
{ displayTitle: `Plugin NPM Release [extended-stable] ${"b".repeat(40)}` },
]) {
expect(() =>
validateExtendedStableRunIdentity({
run: { ...pluginRun, ...changes },
kind: "plugin",
npmDistTag: "extended-stable",
expectedBranch: branch,
expectedSha: sha,
expectedOrchestratorBranch: "main",
expectedOrchestratorSha: workflowSha,
}),
).toThrow();
}
});
it.each([
["wrong branch", { headBranch: "main" }],
["missing branch", { headBranch: undefined }],
@@ -158,10 +158,11 @@ describe("minimal npm extended-stable workflow", () => {
const recheck = step(parsed.jobs?.publish_openclaw_npm, "Recheck npm release request");
expect(recheck.env?.NPM_WORKFLOW_REF).toBe(validate.env?.NPM_WORKFLOW_REF);
expect(releaseDocs).toContain("--ref main");
expect(releaseDocs).toContain("-f release_candidate_branch=extended-stable/YYYY.M.33");
expect(releaseDocs).toContain("canonical candidate branch directly");
expect(releaseDocs).toContain("workflow SHA is reachable from current `main`");
expect(releaseDocs).toContain("trusted main-pinned harness");
expect(releaseDocs).toContain(
"The parent derives the canonical `extended-stable/YYYY.M.33` branch",
);
expect(releaseDocs).toContain("workflow SHA remains reachable from `main`");
expect(releaseDocs).toContain("trusted main-pinned `release-ci/*` harness");
});
it("accepts arbitrary SHA preflight targets and exercises every publishable plugin package", () => {
@@ -278,6 +279,12 @@ describe("minimal npm extended-stable workflow", () => {
"Verify plugin npm release run metadata",
);
expect(verify.env?.RUN_KIND).toBe("plugin");
expect(verify.env?.EXPECTED_ORCHESTRATOR_BRANCH).toBe(
"${{ inputs.release_candidate_branch != '' && github.ref_name || '' }}",
);
expect(verify.env?.EXPECTED_ORCHESTRATOR_SHA).toBe(
"${{ inputs.release_candidate_branch != '' && github.workflow_sha || '' }}",
);
expect(verify.run).toContain(
"--json workflowName,displayTitle,headBranch,headSha,event,status,conclusion,url",
);
@@ -5370,10 +5370,10 @@ describe("package artifact reuse", () => {
const releaseSkill = readFileSync(RELEASE_MAINTAINER_SKILL, "utf8");
expect(releaseWorkflow).toContain(
"Stable OpenClaw publish requires an explicit windows_node_tag.",
"Regular stable OpenClaw publish requires an explicit windows_node_tag.",
);
expect(releaseWorkflow).toContain(
"Stable OpenClaw publish requires candidate-approved windows_node_installer_digests.",
"Regular stable OpenClaw publish requires candidate-approved windows_node_installer_digests.",
);
expect(releaseWorkflow).toContain("promote_windows_release_assets()");
expect(releaseWorkflow).toContain("dispatch_workflow windows-node-release.yml");
@@ -5459,7 +5459,7 @@ describe("package artifact reuse", () => {
"the selected `windows_node_tag`, its saved `windows_node_installer_digests`,",
);
expect(releaseDocs).toContain(
"candidate-approved `windows_node_installer_digests`, and verify the canonical",
"candidate-approved `windows_node_installer_digests` map. Before dispatching",
);
expect(releaseSkill).toContain(
"candidate-approved installer digest map as `windows_node_installer_digests`.",
@@ -79,6 +79,12 @@ describe("plugin npm extended-stable workflow", () => {
expect(inputs?.ref?.description).toBe(
"Exact commit SHA; preflight accepts main/release ancestry, while publish mode also supports canonical extended-stable or matching Tideclaw alpha branches",
);
expect(inputs?.release_candidate_branch).toEqual({
description:
"Canonical extended-stable branch when trusted main release tooling publishes its immutable target",
required: false,
type: "string",
});
});
it("uses one override for check, plan, preview, pack, and publish", () => {
@@ -151,7 +157,7 @@ describe("plugin npm extended-stable workflow", () => {
}
});
it("trusts only the canonical monthly branch at the exact checked-out SHA", () => {
it("accepts canonical monthly targets from the branch or trusted main orchestrator", () => {
const trusted = step(
workflow().jobs?.preview_plugins_npm,
"Validate ref is on a trusted publish branch",
@@ -164,6 +170,16 @@ describe("plugin npm extended-stable workflow", () => {
expect(trusted.run).toContain(
'[[ "$(git rev-parse HEAD)" == "$(git rev-parse "refs/remotes/origin/${extended_stable_branch}")" ]]',
);
expect(trusted.env?.RELEASE_CANDIDATE_BRANCH).toBe(
"${{ github.event_name == 'workflow_dispatch' && inputs.release_candidate_branch || '' }}",
);
expect(trusted.run).toContain(
'[[ "${RELEASE_CANDIDATE_BRANCH}" != "${extended_stable_branch}" || "${WORKFLOW_REF}" != "refs/heads/main" ]]',
);
expect(trusted.run).toContain(
'git merge-base --is-ancestor HEAD "refs/remotes/origin/${extended_stable_branch}"',
);
expect(trusted.run).toContain('git merge-base --is-ancestor "${WORKFLOW_SHA}" origin/main');
});
it("binds preflight to an exact source SHA without release-publish approval", () => {
@@ -199,6 +215,7 @@ describe("plugin npm extended-stable workflow", () => {
'[[ "$(git rev-parse HEAD)" != "$(git rev-parse "${SOURCE_REF}^{commit}")" ]]',
);
expect(trusted.run).toContain("preflight must not include release_publish_run_id");
expect(trusted.run).toContain("preflight must not include release_candidate_branch");
const preflightBranchRejection = trusted.run?.indexOf(
"Plugin npm preflight target must be reachable from main or release/*.",
);
@@ -378,7 +395,7 @@ describe("plugin npm extended-stable workflow", () => {
.split("\n")
.filter((line) => line.includes('npm publish "$TARBALL_PATH"'));
expect(gitFetchLines).toHaveLength(6);
expect(gitFetchLines).toHaveLength(7);
expect(
gitFetchLines.every((line) => line.includes("timeout --signal=TERM --kill-after=10s 120s")),
).toBe(true);
@@ -1021,6 +1021,10 @@ describe("release validation no-push transport", () => {
resolveTarget,
"Validate release tag is reachable from a trusted release branch",
);
const publishJob = job(releasePublish, "publish");
const resolveClawHubPlan = step(publishJob, "Resolve ClawHub release plan");
const dispatchPublish = step(publishJob, "Dispatch publish workflows");
const dispatchRun = dispatchPublish.run ?? "";
expect(dockerRelease.on?.push).toBeUndefined();
expect(dockerRelease.on?.workflow_dispatch).toBeUndefined();
@@ -1047,6 +1051,13 @@ describe("release validation no-push transport", () => {
expect(validateInputs.run).toContain(
'expected_validation_branch="extended-stable/${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.33"',
);
expect(validateInputs.run).not.toContain(
"Extended-stable core npm publication stays on the canonical extended-stable release flow",
);
expect(validateInputs.run).toContain(
'if [[ "${RELEASE_NPM_DIST_TAG}" == "extended-stable" ]]; then',
);
expect(publishJob.if).toBe("${{ !inputs.publish_docker_only }}");
expect(validateEvidence.env?.EXPECTED_WORKFLOW_BRANCH).toBe(
"${{ steps.inputs.outputs.expected_validation_branch }}",
);
@@ -1087,6 +1098,26 @@ describe("release validation no-push transport", () => {
expect(readFileSync(releasePublishPath, "utf8")).toContain(
"kept draft until Docker publication succeeds",
);
expect(resolveClawHubPlan.run).toContain("plan_args+=(--skip-clawhub)");
expect(dispatchRun).toContain("force_skip_clawhub=true");
expect(dispatchRun).toContain('elif [[ "${RELEASE_NPM_DIST_TAG}" == "latest" ]]; then');
expect(dispatchRun).toContain('latest_arg="--latest=false"');
expect(dispatchRun).toContain("-f npm_dist_tag=extended-stable");
expect(dispatchRun).toContain(
'-f release_candidate_branch="${{ needs.resolve_release_target.outputs.expected_validation_branch }}"',
);
expect(dispatchRun).toContain('-f plugin_npm_run_id="${plugin_npm_run_id}"');
expect(dispatchRun).toContain("upload_dependency_evidence_release_asset");
expect(dispatchRun).toContain("upload_release_evidence_assets");
expect(dispatchRun).toContain(
'[[ "${RELEASE_NPM_DIST_TAG}" != "extended-stable" && "${RELEASE_TAG}" != *"-alpha."*',
);
expect(step(resolveTarget, "Validate stable Windows source release").if).toContain(
"inputs.npm_dist_tag != 'extended-stable'",
);
expect(step(publishJob, "Attest Android release approval").if).toContain(
"inputs.npm_dist_tag != 'extended-stable'",
);
expect(job(releasePublish, "finalize_github_release").needs).toEqual([
"resolve_release_target",
"publish",