diff --git a/.agents/skills/openclaw-changelog-update/SKILL.md b/.agents/skills/openclaw-changelog-update/SKILL.md index 51bf7d6b7240..31ceff48c8c5 100644 --- a/.agents/skills/openclaw-changelog-update/SKILL.md +++ b/.agents/skills/openclaw-changelog-update/SKILL.md @@ -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. diff --git a/.agents/skills/release-openclaw-mac/SKILL.md b/.agents/skills/release-openclaw-mac/SKILL.md index 0d1e589e8d80..4af435984439 100644 --- a/.agents/skills/release-openclaw-mac/SKILL.md +++ b/.agents/skills/release-openclaw-mac/SKILL.md @@ -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 diff --git a/.agents/skills/release-openclaw-maintainer/SKILL.md b/.agents/skills/release-openclaw-maintainer/SKILL.md index 933a9c3ed2c5..8c078866e230 100644 --- a/.agents/skills/release-openclaw-maintainer/SKILL.md +++ b/.agents/skills/release-openclaw-maintainer/SKILL.md @@ -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=`. 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 diff --git a/.agents/skills/release-openclaw-maintainer/references/extended-stable-backports.md b/.agents/skills/release-openclaw-maintainer/references/extended-stable-backports.md index 846981da1895..a00eba4730fb 100644 --- a/.agents/skills/release-openclaw-maintainer/references/extended-stable-backports.md +++ b/.agents/skills/release-openclaw-maintainer/references/extended-stable-backports.md @@ -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 diff --git a/.agents/skills/verify-release/SKILL.md b/.agents/skills/verify-release/SKILL.md index 7db13ba19a6b..7839c6d4a0b3 100644 --- a/.agents/skills/verify-release/SKILL.md +++ b/.agents/skills/verify-release/SKILL.md @@ -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` 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 ``. 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 diff --git a/.github/workflows/openclaw-npm-release.yml b/.github/workflows/openclaw-npm-release.yml index f0ef6a7d1550..7eaff0f663bf 100644 --- a/.github/workflows/openclaw-npm-release.yml +++ b/.github/workflows/openclaw-npm-release.yml @@ -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 diff --git a/.github/workflows/openclaw-release-publish.yml b/.github/workflows/openclaw-release-publish.yml index 0ed8f3f2ae2d..ac2595718720 100644 --- a/.github/workflows/openclaw-release-publish.yml +++ b/.github/workflows/openclaw-release-publish.yml @@ -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 diff --git a/.github/workflows/plugin-npm-release.yml b/.github/workflows/plugin-npm-release.yml index dc85a9f30642..636e31fc33fc 100644 --- a/.github/workflows/plugin-npm-release.yml +++ b/.github/workflows/plugin-npm-release.yml @@ -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' diff --git a/docs/ci.md b/docs/ci.md index f3c2903a748b..5a9534a3dac6 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -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 \ diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index f69cacac9455..04e876932f56 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -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= \ - -f full_release_validation_run_id= \ - -f full_release_validation_run_attempt= \ - -f 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= \ -f full_release_validation_run_id= \ -f full_release_validation_run_attempt= \ - -f 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= \ - -f full_release_validation_run_id= \ - -f full_release_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=`. -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: diff --git a/docs/reference/full-release-validation.md b/docs/reference/full-release-validation.md index ddc561d9dc7c..5cac71dfb204 100644 --- a/docs/reference/full-release-validation.md +++ b/docs/reference/full-release-validation.md @@ -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. diff --git a/scripts/lib/openclaw-release-clawhub-plan.ts b/scripts/lib/openclaw-release-clawhub-plan.ts index a4620b8cc8ca..0730678784b0 100644 --- a/scripts/lib/openclaw-release-clawhub-plan.ts +++ b/scripts/lib/openclaw-release-clawhub-plan.ts @@ -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, diff --git a/scripts/openclaw-npm-extended-stable-release.mjs b/scripts/openclaw-npm-extended-stable-release.mjs index 3dcf38261168..a52a6524b48d 100644 --- a/scripts/openclaw-npm-extended-stable-release.mjs +++ b/scripts/openclaw-npm-extended-stable-release.mjs @@ -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 ?? ""} and ${run.headSha ?? ""}.`, ); @@ -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; diff --git a/scripts/release-ci-summary.mjs b/scripts/release-ci-summary.mjs index 91143c5e0ff9..ad2b60bec70f 100755 --- a/scripts/release-ci-summary.mjs +++ b/scripts/release-ci-summary.mjs @@ -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"); diff --git a/test/plugin-clawhub-release.test.ts b/test/plugin-clawhub-release.test.ts index f238f0a4b4e7..59908113f727 100644 --- a/test/plugin-clawhub-release.test.ts +++ b/test/plugin-clawhub-release.test.ts @@ -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([ diff --git a/test/scripts/openclaw-npm-extended-stable-release.test.ts b/test/scripts/openclaw-npm-extended-stable-release.test.ts index 5eb471601b86..aeed9395194d 100644 --- a/test/scripts/openclaw-npm-extended-stable-release.test.ts +++ b/test/scripts/openclaw-npm-extended-stable-release.test.ts @@ -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 }], diff --git a/test/scripts/openclaw-npm-extended-stable-workflow.test.ts b/test/scripts/openclaw-npm-extended-stable-workflow.test.ts index 3155162495dd..e24cb261d810 100644 --- a/test/scripts/openclaw-npm-extended-stable-workflow.test.ts +++ b/test/scripts/openclaw-npm-extended-stable-workflow.test.ts @@ -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", ); diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 495e4de04ee2..a2749863e6a9 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -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`.", diff --git a/test/scripts/plugin-npm-extended-stable-workflow.test.ts b/test/scripts/plugin-npm-extended-stable-workflow.test.ts index fd9b1b9db884..47483202f4d4 100644 --- a/test/scripts/plugin-npm-extended-stable-workflow.test.ts +++ b/test/scripts/plugin-npm-extended-stable-workflow.test.ts @@ -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); diff --git a/test/scripts/release-no-push-workflow.test.ts b/test/scripts/release-no-push-workflow.test.ts index 51845123b936..1e911ad7c1c7 100644 --- a/test/scripts/release-no-push-workflow.test.ts +++ b/test/scripts/release-no-push-workflow.test.ts @@ -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",