mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
feat(release): add non-Latest extended-stable releases (#154515)
* feat(release): add non-Latest extended-stable releases Reconcile #120522 with current main while preserving qualified artifacts, publication approvals, active-line checks, and supported recovery routes. * feat(release): add non-Latest extended-stable releases OpenClaw-Publication: c2238e25-fc84-40bf-ba4d-a6d9d11f4a5b --------- Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com>
This commit is contained in:
parent
3187f50238
commit
ca6a7d9818
38 changed files with 1313 additions and 334 deletions
|
|
@ -289,9 +289,9 @@ of this skill; initial generation must never overwrite them.
|
|||
--release-tag v<YYYY.M.PATCH> \
|
||||
--check-github
|
||||
```
|
||||
- add one `--release-tag` for every beta and stable page in the train; a
|
||||
`### Release verification` tail is permitted, but any other body drift
|
||||
fails the check
|
||||
- add one `--release-tag` for every beta, stable, and extended-stable page in
|
||||
the train; a `### Release verification` tail is permitted, but any other
|
||||
body drift fails the check
|
||||
- `scripts/render-github-release-notes.mts` is the canonical release-body
|
||||
renderer used by candidate validation, publish, and verification. When the
|
||||
complete `## YYYY.M.PATCH` section fits GitHub's 125,000-character limit and
|
||||
|
|
@ -372,11 +372,14 @@ workflow for their verification.
|
|||
|
||||
## Extended-Stable Variant
|
||||
|
||||
Extended-stable has one release commit and no 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. Re-audit after a product backport; a tooling-only
|
||||
repair needs no changelog entry. Never rewrite a published tag or changelog.
|
||||
Extended-stable has one release commit and one canonical GitHub Release body.
|
||||
After version prep and approved backports, regenerate `CHANGELOG/YYYY.M.P.md`
|
||||
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 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.
|
||||
|
||||
## Quota / API Outage Rule
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ user.
|
|||
|
||||
Before drafting focus areas, read real release evidence:
|
||||
|
||||
1. GitHub release body, or the immutable tag and publish run for extended-stable.
|
||||
1. GitHub release body and immutable tag; for extended-stable, also confirm the
|
||||
npm/container-only scope and non-Latest classification.
|
||||
2. The released base version's `CHANGELOG/<version>.md` and contribution
|
||||
record, resolved with `node scripts/release-changelog.mjs read --version <version> [--ref <sha-or-tag>]`
|
||||
(add `--record` for accounting). The shared
|
||||
|
|
@ -84,8 +85,8 @@ openclaw --version
|
|||
|
||||
Do not add `--yes`: users moving from newer regular stable must see the downgrade
|
||||
warning because older versions may not understand newer configuration. Link the
|
||||
tag or changelog; do not imply a GitHub Release or inherit regular stable
|
||||
macOS, Windows, ClawHub, `latest`, or website claims.
|
||||
GitHub Release, but do not inherit regular stable macOS, Windows, ClawHub,
|
||||
`latest`, or website claims.
|
||||
|
||||
## Style
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
interface:
|
||||
display_name: "OpenClaw Release Announcement"
|
||||
short_description: "Draft Discord beta/stable release announcements from evidence."
|
||||
default_prompt: "Use this skill to draft an OpenClaw beta or stable Discord announcement from changelog, release notes, npm/GitHub release proof, and validation evidence."
|
||||
short_description: "Draft evidence-backed Discord release announcements."
|
||||
default_prompt: "Use $release-openclaw-announcement to draft an OpenClaw beta, stable, or extended-stable Discord announcement from changelog, release notes, npm/GitHub release proof, and validation evidence."
|
||||
|
|
|
|||
|
|
@ -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 does not inherit macOS assets, appcast promotion, or a GitHub Release
|
||||
unless the current extended-stable release policy explicitly adds them.
|
||||
that track's GitHub Release carries shared validation evidence but does not
|
||||
inherit macOS assets or appcast promotion.
|
||||
|
||||
## Release authorization
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Read only the references needed for the selected phase:
|
|||
|
||||
- Regular beta/stable preparation or publication: [regular release](references/regular-release.md), which routes preparation and phase-specific proof. If the request does not specify stable/full, default to beta; beta authorization does not authorize later stable promotion.
|
||||
- Backport discovery: [candidate inventory](references/backport-discovery.md). For extended-stable also read [backport preparation](references/extended-stable-backports.md); SDK/config changes need a visible maintenance-risk warning and maintainer decision.
|
||||
- Extended-stable `.33+` Gateway publication: [extended-stable publication](references/extended-stable-publish.md). Do not use the regular release sequence or inherit GitHub Release/native-app publication.
|
||||
- Extended-stable `.33+` Gateway publication: [extended-stable publication](references/extended-stable-publish.md). Use the shared publisher with extended-stable inputs; its non-Latest GitHub Release carries evidence without native-app or ClawHub publication.
|
||||
- Validation selection or failed proof: [validation and confidence](references/validation.md), with `$release-openclaw-ci` for workflow execution and immutable manifests.
|
||||
- Interrupted publication or registry promotion: [publication recovery](references/publication-recovery.md).
|
||||
- Native assets: [platform publication](references/platform-publication.md), with `$release-openclaw-mac` for macOS operations.
|
||||
|
|
|
|||
|
|
@ -24,8 +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 Releases, the macOS app, Windows Hub,
|
||||
mobile apps, website downloads, and private-repository dist-tags.
|
||||
- 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.
|
||||
|
|
@ -161,7 +162,8 @@ fi
|
|||
```
|
||||
|
||||
Do not use GitHub's latest nonprerelease Release as the source of truth. The
|
||||
extended-stable lane intentionally creates no GitHub Release. In bootstrap
|
||||
npm `extended-stable` selector remains authoritative for the active line; its
|
||||
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.
|
||||
|
|
@ -238,6 +240,8 @@ path alone.
|
|||
out of scope.
|
||||
- Treat macOS-app-only, Windows-Hub-only, mobile-only, website-only, and GitHub
|
||||
Release-only fixes as `skip` for this Gateway extended-stable line.
|
||||
- Keep GitHub Release automation repairs in trusted current-main release
|
||||
tooling; they are not product backports for the maintenance branch.
|
||||
- Treat cross-repository or package-topology uncertainty as `blocked` until the
|
||||
shipped npm surface and release owner are proven.
|
||||
|
||||
|
|
@ -369,8 +373,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 no
|
||||
other non-npm publication is 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
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ 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.
|
||||
|
||||
Extended-stable requires a visible **SDK/config backport warning** whenever a
|
||||
candidate changes the public plugin SDK or a config/default/schema/migration
|
||||
|
|
@ -29,7 +29,7 @@ on pinned current `main` as the exact command and validation contract.
|
|||
|
||||
1. On `extended-stable/YYYY.M.33`, verify the root and every publishable official
|
||||
plugin have the intended version. Generate and commit the complete
|
||||
`## YYYY.M.P` changelog section with `### Highlights`, `### Changes`, and
|
||||
`CHANGELOG/YYYY.M.P.md` entry with `### Highlights`, `### Changes`, and
|
||||
`### Fixes`. Carry the full current-main Docker
|
||||
release-channel unit: workflow, promoter, policy, shared classifier, tests,
|
||||
and workflow validation. Run focused checks and freeze the untagged tip SHA.
|
||||
|
|
@ -37,37 +37,50 @@ on pinned current `main` as the exact command and validation contract.
|
|||
Release Validation derives `npm_dist_tag=extended-stable` from the version.
|
||||
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 manifest. Current manifests include qualified npm and prepared
|
||||
Docker artifacts; use that same run ID for npm preflight evidence. Historical
|
||||
manifests without them still need a separate npm preflight. Any candidate
|
||||
Use the trusted main-pinned helper's canonical `release-ci/*` producer,
|
||||
which attests the immutable target SHA in its manifest. Direct branch/main
|
||||
producers do not satisfy protected-tag shared publication. Current manifests
|
||||
include qualified npm and prepared Docker artifacts; use that same run ID
|
||||
and attempt for npm preflight publication evidence. Also run the supplemental
|
||||
trusted-main preflight described in `release-openclaw-ci`; that validation-only
|
||||
run does not replace the integrated publication artifact. 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
|
||||
manifest target SHA and attempt to the tag, and accept a direct run from the
|
||||
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. For a tooling-only failure,
|
||||
use [trusted-main recovery](#trusted-main-npm-recovery) below.
|
||||
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. A trusted-main plugin recovery run requires
|
||||
this trusted-main core route; pass its successful ID as `plugin_npm_run_id`.
|
||||
manifest target SHA and attempt to the tag, and require the canonical
|
||||
`release-ci/<sha12>-<epoch>` producer with trusted tooling identity. Reject
|
||||
direct canonical-branch/main producers and narrow reruns.
|
||||
6. With publication/tag-push authority, create and push a protected lightweight
|
||||
`release-publish/<tooling-sha12>-<epoch>` tag at the frozen trusted-main
|
||||
Tooling SHA, using the commands in `docs/reference/RELEASING.md`. Dispatch
|
||||
`OpenClaw Release Publish` with `--ref` set to that tooling tag, the product
|
||||
release tag as `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. If core npm already published, resume the parent from the same protected
|
||||
tooling tag with `openclaw_npm_resume_run_id` bound to the successful original core publish.
|
||||
It verifies the registry tarball against preflight before resuming evidence,
|
||||
Docker, and finalization. Docker-only recovery may dispatch from `main` with
|
||||
`publish_openclaw_npm=false` and `publish_docker_only=true`; that path does
|
||||
not attach evidence or finalize the release.
|
||||
8. 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.
|
||||
To promote an already-published core version to `extended-stable`, use
|
||||
`promote_extended_stable` in the `openclaw/releases` dist-tag workflow
|
||||
from that repository's `main`, after openclaw/releases#27 is merged. Follow
|
||||
[registry selector recovery](publication-recovery.md#registry-selectors),
|
||||
not the publication/resume path. The target must be a final extended-stable
|
||||
version with patch `33` or higher and no suffix; fixes increment the patch.
|
||||
Stable/beta promotion and sync reject that
|
||||
patch range. The same action can select an older extended-stable version
|
||||
for rollback. Repair other selectors separately with
|
||||
approved credential-isolated tooling. Never republish a version.
|
||||
9. 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
|
||||
|
|
@ -75,8 +88,9 @@ on pinned current `main` as the exact command and validation contract.
|
|||
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. Do not create a GitHub Release or publish macOS, Windows, mobile, website,
|
||||
ClawHub, or private dist-tag artifacts from this path.
|
||||
10. Verify the non-Latest GitHub Release and its dependency, validation, and
|
||||
postpublish evidence. Do not publish macOS, Windows, mobile, website,
|
||||
ClawHub, regular npm `latest`, or private dist-tag artifacts from this path.
|
||||
|
||||
## Trusted-main npm recovery
|
||||
|
||||
|
|
@ -86,7 +100,12 @@ for example an obsolete check rejecting validated dependency pins because npm
|
|||
A product defect, known vulnerable dependency, or changed candidate needs its
|
||||
own repair and fresh qualification; workflow recovery does not waive those gates.
|
||||
|
||||
This route uses existing inputs; #151282 added no workflow-dispatch inputs.
|
||||
Use this lower-level route only for an approved workflow recovery, not normal
|
||||
shared publication. It does not itself attach evidence or finalize the GitHub
|
||||
Release. Retain both child identities and their evidence for approved closeout;
|
||||
a direct-main recovery run is not automatically interchangeable with the
|
||||
protected parent's core-resume receipt.
|
||||
|
||||
In `gh workflow run`, `--ref main` selects trusted publishing **tooling**.
|
||||
The plugin input `-f ref=<release-sha>` selects the exact **package source**;
|
||||
never replace it with `main`, a branch name, or the tooling SHA.
|
||||
|
|
@ -111,7 +130,7 @@ Keep final tags immutable and use a new patch for source changes after tagging.
|
|||
|
||||
Save the successful plugin publication run ID after exact-version and selector
|
||||
readback. Dispatch `openclaw-npm-release.yml` with `--ref main` and the existing
|
||||
core recovery inputs from `docs/reference/RELEASING.md`:
|
||||
core recovery inputs:
|
||||
|
||||
- `tag=vYYYY.M.P`, `preflight_only=false`, and `npm_dist_tag=extended-stable`.
|
||||
- `release_candidate_branch=extended-stable/YYYY.M.33`, including for patches
|
||||
|
|
|
|||
|
|
@ -6,14 +6,15 @@ GitHub OIDC trusted publishing; never substitute `NPM_TOKEN` or plugin OTP
|
|||
commands. GitHub's `npm-release` environment must be approved by
|
||||
`@openclaw/openclaw-release-managers`.
|
||||
|
||||
The regular publish parent runs from the protected
|
||||
The regular and extended-stable publish parent runs from the protected
|
||||
`release-publish/<tooling-sha12>-<epoch>` tag minted at the pinned Tooling SHA;
|
||||
use the candidate helper's printed command. Do not dispatch npm/plugin/ClawHub
|
||||
use the regular candidate helper's printed command or the extended-stable
|
||||
publication reference for that track. Do not dispatch npm/plugin/ClawHub
|
||||
publication from a moving main parent. Docker-only recovery may use main.
|
||||
Extended-stable direct npm workflow recovery is a separate supported main route;
|
||||
follow [trusted-main npm recovery](extended-stable-publish.md#trusted-main-npm-recovery)
|
||||
for plugin source inputs and the matching core evidence handoff. It does not use
|
||||
the regular publish parent or authorize ClawHub publication.
|
||||
the shared publish parent or authorize ClawHub publication.
|
||||
Tideclaw alpha uses its matching alpha branch and its owning skill.
|
||||
|
||||
Publication promotes previously qualified bytes. Bind the successful Full
|
||||
|
|
@ -67,9 +68,41 @@ packaging recovery keeps the original tag and follows
|
|||
Promote through the restricted release-ops
|
||||
`openclaw/releases/.github/workflows/openclaw-npm-dist-tags.yml` workflow.
|
||||
Unlike package publication, npm selector management requires `NPM_TOKEN`.
|
||||
Prefer repairing that workflow's token path. Point `latest` or `beta` only at
|
||||
the operator-approved already-published version, then verify cache-bypassed
|
||||
registry readback.
|
||||
Prefer repairing that workflow's token path. Point `latest`, `beta`, or
|
||||
`extended-stable` only at the operator-approved already-published version, then
|
||||
verify cache-bypassed registry readback.
|
||||
|
||||
To promote an already-published core version to `extended-stable`, use
|
||||
`mode=promote_extended_stable` with an exact public final release tag after
|
||||
[openclaw/releases#27](https://github.com/openclaw/releases/pull/27) is merged
|
||||
and available on the release repository's `main`:
|
||||
|
||||
```bash
|
||||
gh workflow run openclaw-npm-dist-tags.yml \
|
||||
--repo openclaw/releases --ref main \
|
||||
-f mode=promote_extended_stable -f tag=vYYYY.M.PATCH
|
||||
```
|
||||
|
||||
Replace `vYYYY.M.PATCH` with the approved final extended-stable release tag
|
||||
(patch `33` or higher, without a suffix). Extended-stable fixes increment the
|
||||
patch (`33`, `34`, `35`, and so on), never a correction suffix. Regular stable/beta
|
||||
promotion and sync reject patch `33`
|
||||
or higher, including the scheduled beta floor. Promotion can
|
||||
select a newer version or roll back to an older one, including historical
|
||||
unsuffixed extended-stable final versions; new-publication eligibility does not
|
||||
apply, but the channel/patch boundary still does. This mode
|
||||
writes only core `openclaw`'s
|
||||
`extended-stable` selector, leaving `latest`, `beta`, plugins, other prepared-core
|
||||
packages, Docker, Git tags, and GitHub Releases untouched. It neither republishes
|
||||
nor changes installed clients. Do not use publish resume to roll back a rejected
|
||||
release. Coordinate separately with any active publisher before retagging.
|
||||
|
||||
Wait for successful readback and retain the run's previous/target summary. An
|
||||
already-correct selector is a no-op; readback retries never repeat the write.
|
||||
If a write is unconfirmed or readback fails, inspect the live registry before
|
||||
retrying. Docker channel promotion remains a separate approval-gated
|
||||
`docker-channel-promote.yml` dispatch from `openclaw/openclaw` main with an
|
||||
existing extended-stable image tag; its channel is derived from that version.
|
||||
|
||||
Immediately after publishing or promoting to `latest`, dispatch that same
|
||||
release-ledger workflow to repair the beta floor: raise missing or older beta
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ operator steering. Do not preserve superseded scope.
|
|||
- Plugin NPM Release: `<run id / URL or none>`
|
||||
- publish parent: `<run id / URL or none>`
|
||||
- Docker release/repair: `<run ids / tag / aliases or none>`
|
||||
- GitHub Release: `<public URL / non-Latest readback or none>`
|
||||
- immutable successful children: `<run ids / artifacts or none>`
|
||||
- registry/provenance readback: `<artifact or command result>`
|
||||
|
||||
|
|
@ -45,7 +46,8 @@ operator steering. Do not preserve superseded scope.
|
|||
Keep one row per selected surface, with its exact run/attempt or immutable
|
||||
receipt, current state, and next action. Remove unselected rows rather than
|
||||
reporting them as passed. Stable/full includes macOS unless explicitly scoped
|
||||
out; extended-stable does not inherit ClawHub, GitHub Release, or native apps.
|
||||
out; extended-stable carries non-Latest GitHub Release evidence but does not inherit
|
||||
ClawHub or native apps.
|
||||
|
||||
| Surface | Evidence and state | Next action or blocker |
|
||||
| ------------------------- | ------------------------------------------------------------------------ | ---------------------- |
|
||||
|
|
|
|||
|
|
@ -115,8 +115,10 @@ Use `source=npm -f package_spec=openclaw@beta` for published beta proof. Keep
|
|||
`workflow_ref` as trusted current harness code unless the release process says
|
||||
otherwise.
|
||||
|
||||
For extended-stable, branch-owned Full Release Validation is publication
|
||||
evidence; Package Acceptance is a post-publish selector smoke:
|
||||
For extended-stable shared publication, require complete exact-target Full
|
||||
Release Validation from the trusted main-pinned `release-ci/*` harness. Direct
|
||||
canonical-branch or `main` producers do not satisfy the protected publisher.
|
||||
Package Acceptance is a post-publish selector smoke:
|
||||
|
||||
```bash
|
||||
gh workflow run package-acceptance.yml \
|
||||
|
|
|
|||
|
|
@ -13,9 +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 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.
|
||||
|
|
@ -84,15 +85,21 @@ Use these checks only for the regular orchestrated release track.
|
|||
|
||||
## Extended-stable checks
|
||||
|
||||
Extended-stable has no GitHub Release ledger. Verify live tag, workflow,
|
||||
registry, provenance, and image state directly.
|
||||
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 no GitHub Release.
|
||||
2. **Workflow chain:** find successful preflight, complete validation, plugin
|
||||
npm, and core publish runs on the canonical branch and SHA. Validation must
|
||||
Require the Git tag and a public, non-prerelease GitHub Release whose title
|
||||
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.
|
||||
Require a protected `release-publish/*` parent and canonical `release-ci/*`
|
||||
validation producer with verified workflow SHA provenance. 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.
|
||||
|
|
@ -106,12 +113,17 @@ registry, provenance, and image state directly.
|
|||
digest binding to the release SHA. Preserve output and workflow URLs.
|
||||
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. 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
|
||||
resolve to those digests. Require the successful `OpenClaw Release Publish`
|
||||
parent run and its completed Docker verification. The normal route finalizes
|
||||
afterward; an explicitly requested fast path may activate GitHub first. Repair
|
||||
aliases through current-main `Docker Channel Promotion` for the exact tag,
|
||||
without rebuilding.
|
||||
6. **Recovery:** never republish. Use `promote_extended_stable` in the
|
||||
`openclaw/releases` dist-tag workflow for the root selector (an unsuffixed
|
||||
final patch `33+`) and approved credential-isolated tooling for others, then repeat
|
||||
complete readback. Do not require ClawHub, native/mobile apps, website,
|
||||
private dist-tags, regular `latest`, or a GitHub Release.
|
||||
private dist-tags, or regular `latest`. Require shared release evidence, but
|
||||
do not require regular native or ClawHub assets.
|
||||
|
||||
## Shared live smoke
|
||||
|
||||
|
|
|
|||
8
.github/workflows/openclaw-npm-release.yml
vendored
8
.github/workflows/openclaw-npm-release.yml
vendored
|
|
@ -73,7 +73,7 @@ on:
|
|||
required: false
|
||||
type: string
|
||||
release_candidate_branch:
|
||||
description: Canonical extended-stable branch when a trusted main workflow promotes its immutable tag
|
||||
description: Canonical extended-stable branch when protected release tooling or trusted-main recovery promotes its immutable tag
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
|
|
@ -173,8 +173,8 @@ jobs:
|
|||
extended_stable_publish=true
|
||||
fi
|
||||
if [[ -n "${release_candidate_branch}" ]]; then
|
||||
if [[ "${RELEASE_NPM_DIST_TAG}" != "extended-stable" || "${WORKFLOW_REF}" != "refs/heads/main" ]]; then
|
||||
echo "release_candidate_branch is only valid for an extended-stable publish dispatched from main." >&2
|
||||
if [[ "${RELEASE_NPM_DIST_TAG}" != "extended-stable" ]] || { [[ "${WORKFLOW_REF}" != "refs/heads/main" ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/tags/release-publish/[a-f0-9]{12}-[1-9][0-9]*$ ]]; }; then
|
||||
echo "release_candidate_branch requires extended-stable publication from trusted main or protected release-publish tooling." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! "${RELEASE_TAG}" =~ ^v([0-9]{4})\.([1-9][0-9]*)\.[1-9][0-9]*$ ]]; then
|
||||
|
|
@ -589,6 +589,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 != '' && startsWith(github.ref, 'refs/tags/release-publish/') && github.ref_name || '' }}
|
||||
EXPECTED_ORCHESTRATOR_SHA: ${{ inputs.release_candidate_branch != '' && startsWith(github.ref, 'refs/tags/release-publish/') && github.workflow_sha || '' }}
|
||||
RUN_KIND: plugin
|
||||
WORKFLOW_REF: ${{ github.ref }}
|
||||
WORKFLOW_SHA: ${{ github.workflow_sha }}
|
||||
|
|
|
|||
|
|
@ -365,6 +365,7 @@ jobs:
|
|||
PARENT_WORKFLOW_SHA="$(jq -er '.tooling.sha' <<< "$RELEASE_REQUEST")"
|
||||
WINDOWS_NODE_TAG="$(jq -r '.inputs.windows_node_tag // ""' <<< "$RELEASE_REQUEST")"
|
||||
WINDOWS_NODE_INSTALLER_DIGESTS="$(jq -r '.inputs.windows_node_installer_digests // ""' <<< "$RELEASE_REQUEST")"
|
||||
RELEASE_NPM_DIST_TAG="$(jq -er '.inputs.npm_dist_tag' <<< "$RELEASE_REQUEST")"
|
||||
source scripts/lib/release-publish-children.sh
|
||||
jq -n --arg tag "$RELEASE_TAG" --arg source "$WINDOWS_NODE_TAG" \
|
||||
--arg digests "$WINDOWS_NODE_INSTALLER_DIGESTS" \
|
||||
|
|
|
|||
75
.github/workflows/openclaw-release-publish.yml
vendored
75
.github/workflows/openclaw-release-publish.yml
vendored
|
|
@ -66,7 +66,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
|
||||
|
|
@ -88,7 +88,7 @@ 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
|
||||
|
|
@ -138,6 +138,7 @@ jobs:
|
|||
timeout-minutes: 20
|
||||
outputs:
|
||||
sha: ${{ steps.manifest.outputs.sha || steps.ref.outputs.sha }}
|
||||
expected_validation_branch: ${{ steps.inputs.outputs.expected_validation_branch }}
|
||||
preflight_artifact_name: ${{ steps.preflight_artifact.outputs.name }}
|
||||
preflight_artifact_run_id: ${{ steps.preflight_artifact.outputs.run_id }}
|
||||
preflight_tarball_sha256: ${{ steps.manifest.outputs.tarball_sha256 }}
|
||||
|
|
@ -157,6 +158,7 @@ jobs:
|
|||
android_release_note: ${{ steps.ref.outputs.android_release_note }}
|
||||
steps:
|
||||
- name: Validate inputs
|
||||
id: inputs
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
|
|
@ -277,10 +279,6 @@ jobs:
|
|||
echo "Docker-only latest recovery requires a regular stable release tag." >&2
|
||||
exit 1
|
||||
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
|
||||
tideclaw_alpha_publish=false
|
||||
if [[ "${RELEASE_TAG}" == *"-alpha."* && "${RELEASE_NPM_DIST_TAG}" == "alpha" && "${WORKFLOW_REF}" =~ ^refs/heads/tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
|
||||
tideclaw_alpha_publish=true
|
||||
|
|
@ -329,6 +327,16 @@ jobs:
|
|||
exit 1
|
||||
;;
|
||||
esac
|
||||
expected_validation_branch="${WORKFLOW_REF#refs/*/}"
|
||||
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 publication requires a final .33+ release tag." >&2
|
||||
exit 1
|
||||
fi
|
||||
expected_validation_branch="extended-stable/${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.33"
|
||||
fi
|
||||
echo "expected_validation_branch=${expected_validation_branch}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout release tag
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
|
@ -348,6 +356,7 @@ jobs:
|
|||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
PUBLISH_OPENCLAW_NPM: ${{ inputs.publish_openclaw_npm && 'true' || 'false' }}
|
||||
PUBLISH_DOCKER_ONLY: ${{ inputs.publish_docker_only && 'true' || 'false' }}
|
||||
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
release_version="${RELEASE_TAG#v}"
|
||||
|
|
@ -362,7 +371,7 @@ jobs:
|
|||
android_release_note=""
|
||||
if [[ "${android_pin_version}" == "${release_version%%-*}" ]]; then
|
||||
android_pin_matches=true
|
||||
elif [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${PUBLISH_DOCKER_ONLY}" != "true" && "${RELEASE_TAG}" != *"-alpha."* && "${RELEASE_TAG}" != *"-beta."* ]]; then
|
||||
elif [[ "${RELEASE_NPM_DIST_TAG}" != "extended-stable" && "${PUBLISH_OPENCLAW_NPM}" == "true" && "${PUBLISH_DOCKER_ONLY}" != "true" && "${RELEASE_TAG}" != *"-alpha."* && "${RELEASE_TAG}" != *"-beta."* ]]; then
|
||||
android_release_note="- Android APK: skipped — apps/android/version.json is ${android_pin_version}, release train is ${release_version%%-*}; run the shared mobile cutter (scripts/mobile-release-version.ts --prepare) before the next tag."
|
||||
echo "${android_release_note}" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
|
@ -879,6 +888,8 @@ jobs:
|
|||
|
||||
- name: Validate release tag is reachable from a trusted release branch
|
||||
env:
|
||||
EXPECTED_VALIDATION_BRANCH: ${{ steps.inputs.outputs.expected_validation_branch }}
|
||||
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
WORKFLOW_REF_NAME: ${{ github.ref_name }}
|
||||
run: |
|
||||
|
|
@ -887,6 +898,15 @@ 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 [[ "${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
|
||||
exit 0
|
||||
fi
|
||||
echo "Extended-stable tag ${RELEASE_TAG} must be reachable from ${EXPECTED_VALIDATION_BRANCH}." >&2
|
||||
exit 1
|
||||
fi
|
||||
if git merge-base --is-ancestor HEAD origin/main; then
|
||||
exit 0
|
||||
fi
|
||||
|
|
@ -972,7 +992,7 @@ jobs:
|
|||
qualify_android_native:
|
||||
name: Qualify native Android release source
|
||||
needs: [resolve_release_target]
|
||||
if: ${{ !inputs.publish_docker_only && inputs.publish_openclaw_npm && !contains(inputs.tag, '-alpha.') && !contains(inputs.tag, '-beta.') && needs.resolve_release_target.outputs.android_pin_matches == 'true' && needs.resolve_release_target.outputs.coverage_policy == 'npm-stable-v1' }}
|
||||
if: ${{ !inputs.publish_docker_only && inputs.publish_openclaw_npm && inputs.npm_dist_tag != 'extended-stable' && !contains(inputs.tag, '-alpha.') && !contains(inputs.tag, '-beta.') && needs.resolve_release_target.outputs.android_pin_matches == 'true' && needs.resolve_release_target.outputs.coverage_policy == 'npm-stable-v1' }}
|
||||
# Native failure blocks its approval receipt, while core publication proceeds.
|
||||
continue-on-error: true
|
||||
permissions:
|
||||
|
|
@ -1062,7 +1082,7 @@ jobs:
|
|||
publish_android:
|
||||
name: Approve and dispatch qualified Android
|
||||
needs: [resolve_release_target, publish, qualify_android_native]
|
||||
if: ${{ always() && !cancelled() && !inputs.publish_docker_only && inputs.publish_openclaw_npm && !contains(inputs.tag, '-alpha.') && !contains(inputs.tag, '-beta.') && needs.resolve_release_target.outputs.android_pin_matches == 'true' && needs.resolve_release_target.result == 'success' && needs.publish.result == 'success' && (needs.resolve_release_target.outputs.coverage_policy != 'npm-stable-v1' || needs.qualify_android_native.outputs.qualified == 'true') }}
|
||||
if: ${{ always() && !cancelled() && !inputs.publish_docker_only && inputs.publish_openclaw_npm && inputs.npm_dist_tag != 'extended-stable' && !contains(inputs.tag, '-alpha.') && !contains(inputs.tag, '-beta.') && needs.resolve_release_target.outputs.android_pin_matches == 'true' && needs.resolve_release_target.result == 'success' && needs.publish.result == 'success' && (needs.resolve_release_target.outputs.coverage_policy != 'npm-stable-v1' || needs.qualify_android_native.outputs.qualified == 'true') }}
|
||||
continue-on-error: true
|
||||
permissions:
|
||||
actions: write
|
||||
|
|
@ -1147,6 +1167,7 @@ jobs:
|
|||
PARENT_WORKFLOW_FULL_REF: ${{ github.ref }}
|
||||
PARENT_WORKFLOW_SHA: ${{ github.workflow_sha }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
||||
TARGET_SHA: ${{ needs.resolve_release_target.outputs.sha }}
|
||||
working-directory: .release-harness
|
||||
run: |
|
||||
|
|
@ -1324,6 +1345,7 @@ jobs:
|
|||
PARENT_WORKFLOW_FULL_REF: ${{ github.ref }}
|
||||
PARENT_WORKFLOW_SHA: ${{ github.workflow_sha }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
||||
PLUGIN_PUBLISH_SCOPE: ${{ inputs.plugin_publish_scope }}
|
||||
PLUGINS: ${{ inputs.plugins }}
|
||||
WORKFLOW_FULL_REF: ${{ github.ref }}
|
||||
|
|
@ -1364,6 +1386,9 @@ jobs:
|
|||
if [[ -n "${PLUGINS// }" ]]; then
|
||||
plan_args+=(--plugins "${PLUGINS}")
|
||||
fi
|
||||
if [[ "${RELEASE_NPM_DIST_TAG}" == "extended-stable" ]]; then
|
||||
plan_args+=(--skip-clawhub)
|
||||
fi
|
||||
if [[ -n "${PREPARED_PLUGINS}" ]]; then
|
||||
plan_args+=(--prepared-artifact "$(jq -cer '.clawhub' <<< "$PREPARED_PLUGINS")")
|
||||
fi
|
||||
|
|
@ -1554,18 +1579,18 @@ jobs:
|
|||
run: |
|
||||
set -euo pipefail
|
||||
source "${GITHUB_WORKSPACE}/.release-harness/scripts/lib/release-publish-children.sh"
|
||||
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}")"
|
||||
{
|
||||
echo "### Publish sequence"
|
||||
echo "### Publish"
|
||||
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}\`"
|
||||
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 [[ "${RELEASE_NPM_DIST_TAG}" == "extended-stable" ]]; then
|
||||
echo "- ClawHub: skipped by release track"
|
||||
else
|
||||
echo "- ClawHub: dispatched in parallel"
|
||||
fi
|
||||
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" ]]; then
|
||||
echo "- OpenClaw npm publish: starts after plugin npm succeeds"
|
||||
else
|
||||
|
|
@ -1584,6 +1609,11 @@ jobs:
|
|||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
if [[ "${RELEASE_NPM_DIST_TAG}" == "extended-stable" ]]; then
|
||||
PACKAGE_VERSION="${RELEASE_TAG#v}" \
|
||||
node "${GITHUB_WORKSPACE}/.release-harness/scripts/openclaw-npm-extended-stable-release.mjs" validate-active-line
|
||||
fi
|
||||
|
||||
prepared_release_notes_file="${RUNNER_TEMP}/release-notes-prepublish.md"
|
||||
prepared_release_notes_metadata_file="${RUNNER_TEMP}/release-notes-prepublish.json"
|
||||
verify_release_tag_target
|
||||
|
|
@ -1613,6 +1643,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=""
|
||||
|
|
@ -1737,6 +1773,12 @@ jobs:
|
|||
-f full_release_validation_run_attempt="${FULL_RELEASE_VALIDATION_RUN_ATTEMPT}"
|
||||
)
|
||||
fi
|
||||
if [[ "${RELEASE_NPM_DIST_TAG}" == "extended-stable" ]]; then
|
||||
evidence_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 \
|
||||
-f tag="${RELEASE_TAG}" \
|
||||
-f preflight_only=false \
|
||||
|
|
@ -2278,7 +2320,7 @@ jobs:
|
|||
publish_windows:
|
||||
name: Dispatch Windows assets after publication
|
||||
needs: [resolve_release_target, finalize_github_release]
|
||||
if: ${{ !cancelled() && needs.finalize_github_release.result == 'success' && (inputs.windows_node_tag != '' || inputs.windows_node_installer_digests != '') && !contains(inputs.tag, '-alpha.') && !contains(inputs.tag, '-beta.') }}
|
||||
if: ${{ !cancelled() && needs.finalize_github_release.result == 'success' && inputs.npm_dist_tag != 'extended-stable' && (inputs.windows_node_tag != '' || inputs.windows_node_installer_digests != '') && !contains(inputs.tag, '-alpha.') && !contains(inputs.tag, '-beta.') }}
|
||||
# App failures are reported by their own workflow; neither npm nor the
|
||||
# published GitHub release depends on native asset availability.
|
||||
continue-on-error: true
|
||||
|
|
@ -2301,6 +2343,7 @@ jobs:
|
|||
GH_TOKEN: ${{ github.token }}
|
||||
PARENT_WORKFLOW_SHA: ${{ github.workflow_sha }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
||||
TARGET_SHA: ${{ needs.resolve_release_target.outputs.sha }}
|
||||
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
|
||||
WINDOWS_NODE_INSTALLER_DIGESTS: ${{ inputs.windows_node_installer_digests }}
|
||||
|
|
|
|||
|
|
@ -74,8 +74,7 @@ jobs:
|
|||
}
|
||||
BASH
|
||||
|
||||
- name: Checkout pushed main
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
- name: Checkout trusted release tooling
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
|
|
@ -99,9 +98,25 @@ jobs:
|
|||
. "$RUNNER_TEMP/github-api-backoff.sh"
|
||||
if [[ "$EVENT_NAME" == "push" ]]; then
|
||||
main_ref="$TRIGGER_SHA"
|
||||
tag="$(gh_with_retry release list --repo "$GITHUB_REPOSITORY" --exclude-drafts --limit 100 \
|
||||
--json tagName,isPrerelease,publishedAt \
|
||||
--jq '[.[] | select(.isPrerelease | not) | select(.tagName | test("^v[0-9]{4}\\.[0-9]+\\.[0-9]+(-[0-9]+)?$"))] | sort_by(.publishedAt) | last | .tagName // empty')"
|
||||
releases_file="$RUNNER_TEMP/published-releases.json"
|
||||
gh_with_retry release list --repo "$GITHUB_REPOSITORY" --exclude-drafts --limit 100 \
|
||||
--json tagName,isPrerelease,publishedAt > "$releases_file"
|
||||
tag="$(RELEASES_FILE="$releases_file" node --input-type=module <<'NODE'
|
||||
import { readFileSync } from "node:fs";
|
||||
import { classifyReleaseTrain, parseReleaseVersion } from "./scripts/lib/release-version.mjs";
|
||||
|
||||
const releases = JSON.parse(readFileSync(process.env.RELEASES_FILE, "utf8"));
|
||||
const candidates = releases
|
||||
.filter((release) => release?.isPrerelease === false)
|
||||
.filter((release) => {
|
||||
const tag = typeof release?.tagName === "string" ? release.tagName : "";
|
||||
const parsed = tag.startsWith("v") ? parseReleaseVersion(tag.slice(1)) : null;
|
||||
return parsed !== null && classifyReleaseTrain(parsed) === "stable";
|
||||
})
|
||||
.toSorted((left, right) => String(left.publishedAt).localeCompare(String(right.publishedAt)));
|
||||
process.stdout.write(candidates.at(-1)?.tagName ?? "");
|
||||
NODE
|
||||
)"
|
||||
if [[ -z "$tag" ]]; then
|
||||
echo "should_closeout=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
|
|
@ -109,12 +124,21 @@ jobs:
|
|||
else
|
||||
tag="$MANUAL_TAG"
|
||||
fi
|
||||
if [[ ! "$tag" =~ ^v[0-9]{4}\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then
|
||||
if ! RELEASE_TAG="$tag" node --input-type=module <<'NODE'
|
||||
import { classifyReleaseTrain, parseReleaseVersion } from "./scripts/lib/release-version.mjs";
|
||||
|
||||
const tag = process.env.RELEASE_TAG ?? "";
|
||||
const parsed = tag.startsWith("v") ? parseReleaseVersion(tag.slice(1)) : null;
|
||||
if (parsed === null || classifyReleaseTrain(parsed) !== "stable") {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
NODE
|
||||
then
|
||||
if [[ "$EVENT_NAME" == "push" ]]; then
|
||||
echo "should_closeout=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "Stable main closeout accepts only a stable vYYYY.M.PATCH or vYYYY.M.PATCH-N tag, got $tag." >&2
|
||||
echo "Stable main closeout accepts only a regular stable vYYYY.M.PATCH or vYYYY.M.PATCH-N tag below the extended-stable .33 boundary, got $tag." >&2
|
||||
exit 1
|
||||
fi
|
||||
release_asset_version="${tag#v}"
|
||||
|
|
|
|||
44
.github/workflows/plugin-npm-release.yml
vendored
44
.github/workflows/plugin-npm-release.yml
vendored
|
|
@ -29,6 +29,7 @@ on:
|
|||
- "scripts/lib/plugin-publication-collector.ts"
|
||||
- "scripts/lib/plugin-publication-target.mjs"
|
||||
- "scripts/lib/actions-artifact-archive.mjs"
|
||||
- "scripts/openclaw-npm-extended-stable-release.mjs"
|
||||
- "scripts/plugin-npm-publish.sh"
|
||||
- "scripts/plugin-npm-prepared-release.mjs"
|
||||
- "scripts/plugin-publication-artifact.mjs"
|
||||
|
|
@ -83,6 +84,10 @@ on:
|
|||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
release_candidate_branch:
|
||||
description: Canonical extended-stable branch when protected release tooling publishes its immutable target
|
||||
required: false
|
||||
type: string
|
||||
preflight_only:
|
||||
description: Prepare and verify immutable plugin npm artifacts without publishing
|
||||
required: true
|
||||
|
|
@ -164,7 +169,7 @@ jobs:
|
|||
PYTHON
|
||||
|
||||
- name: Verify trusted preflight or recovery tooling identity
|
||||
if: github.event_name == 'workflow_dispatch' && (inputs.preflight_only || (inputs.npm_dist_tag == 'extended-stable' && github.ref == 'refs/heads/main'))
|
||||
if: github.event_name == 'workflow_dispatch' && (inputs.preflight_only || inputs.release_candidate_branch != '' || (inputs.npm_dist_tag == 'extended-stable' && github.ref == 'refs/heads/main'))
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
WORKFLOW_FULL_REF: ${{ github.ref }}
|
||||
|
|
@ -188,6 +193,7 @@ jobs:
|
|||
RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
|
||||
RELEASE_PUBLISH_RUN_ATTEMPT: ${{ github.event_name == 'workflow_dispatch' && inputs.release_publish_run_attempt || '' }}
|
||||
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 }}
|
||||
|
|
@ -215,9 +221,9 @@ jobs:
|
|||
return False
|
||||
return left == right
|
||||
|
||||
def is_ancestor(ref):
|
||||
def is_ancestor(ref, source="HEAD"):
|
||||
try:
|
||||
run_git(workspace, "merge-base", "--is-ancestor", "HEAD", ref)
|
||||
run_git(workspace, "merge-base", "--is-ancestor", source, ref)
|
||||
return True
|
||||
except GitFailure as error:
|
||||
if error.code == 1:
|
||||
|
|
@ -234,6 +240,7 @@ jobs:
|
|||
fail("Prepared npm publication requires the exact source SHA.")
|
||||
run_id = os.environ["RELEASE_PUBLISH_RUN_ID"]
|
||||
run_attempt = os.environ["RELEASE_PUBLISH_RUN_ATTEMPT"]
|
||||
candidate_branch = os.environ["RELEASE_CANDIDATE_BRANCH"]
|
||||
if preflight:
|
||||
if not re.fullmatch(r"[0-9a-fA-F]{40}", source_ref) or not exact_ref_match(
|
||||
"HEAD",
|
||||
|
|
@ -242,6 +249,8 @@ jobs:
|
|||
fail("Plugin npm preflight requires ref to be the exact 40-character source SHA.")
|
||||
if run_id.strip() or run_attempt.strip():
|
||||
fail("Plugin npm preflight must not include a release publish parent run tuple.")
|
||||
if candidate_branch.strip():
|
||||
fail("Plugin npm preflight must not include release_candidate_branch.")
|
||||
if run_id.strip() and not re.fullmatch(r"[1-9][0-9]*", run_attempt):
|
||||
fail("release_publish_run_id requires the exact positive release_publish_run_attempt.")
|
||||
|
||||
|
|
@ -283,6 +292,31 @@ jobs:
|
|||
timeout=120,
|
||||
reclaim_locks=True,
|
||||
)
|
||||
if candidate_branch.strip():
|
||||
if candidate_branch != extended_branch or not re.fullmatch(
|
||||
r"refs/tags/release-publish/[a-f0-9]{12}-[1-9][0-9]*",
|
||||
os.environ["WORKFLOW_REF"],
|
||||
):
|
||||
fail(
|
||||
f"release_candidate_branch must be {extended_branch} and requires "
|
||||
"protected release-publish workflow tooling."
|
||||
)
|
||||
# The preceding identity check binds the protected tag to WORKFLOW_SHA.
|
||||
# Source and tooling ancestry are separate release admission boundaries.
|
||||
run_git(
|
||||
workspace,
|
||||
"fetch",
|
||||
"--no-tags",
|
||||
"origin",
|
||||
"+refs/heads/main:refs/remotes/origin/main",
|
||||
timeout=120,
|
||||
reclaim_locks=True,
|
||||
)
|
||||
if not is_ancestor("origin/main", os.environ["WORKFLOW_SHA"]):
|
||||
fail("Extended-stable plugin workflow revision is not reachable from current main.")
|
||||
if is_ancestor(f"refs/remotes/origin/{extended_branch}"):
|
||||
raise SystemExit(0)
|
||||
fail(f"Extended-stable plugin target must be reachable from {extended_branch}.")
|
||||
if not exact_ref_match(
|
||||
"HEAD",
|
||||
f"refs/remotes/origin/{extended_branch}",
|
||||
|
|
@ -297,6 +331,9 @@ jobs:
|
|||
"or trusted main, targeting the exact canonical branch tip."
|
||||
)
|
||||
|
||||
if candidate_branch.strip():
|
||||
fail("release_candidate_branch is only valid for extended-stable publication.")
|
||||
|
||||
run_git(
|
||||
workspace,
|
||||
"fetch",
|
||||
|
|
@ -1797,6 +1834,7 @@ jobs:
|
|||
--workflow-full-ref "refs/heads/$candidate_branch" \
|
||||
--workflow-sha "$RELEASE_TARGET_SHA" \
|
||||
--allow-prevalidated-ref
|
||||
node scripts/openclaw-npm-extended-stable-release.mjs validate-active-line
|
||||
fi
|
||||
NPM_CONFIG_USERCONFIG="$npmrc" \
|
||||
NPM_CONFIG_GLOBALCONFIG=/dev/null \
|
||||
|
|
|
|||
|
|
@ -33,13 +33,19 @@ gh workflow run full-release-validation.yml --ref main \
|
|||
-f expected_sha="$VALIDATION_SHA"
|
||||
```
|
||||
|
||||
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. `release-ci/*` evidence is invalid because
|
||||
publish binds every run to the canonical branch and release SHA. The tag
|
||||
publishes Gateway images and only the `extended-stable*` aliases; the path skips
|
||||
the regular orchestrator and its ClawHub, native-app, GitHub Release, website,
|
||||
and private dist-tag surfaces. See [Monthly Gateway extended-stable
|
||||
Gateway extended-stable shared publication requires complete exact-target Full
|
||||
Release Validation from the trusted main-pinned `release-ci/*` harness targeting
|
||||
the frozen `extended-stable/YYYY.M.33` tip. Direct canonical-branch and `main`
|
||||
producers do not satisfy the protected publisher. Current
|
||||
manifests also supply qualified npm preflight artifacts. The shared
|
||||
`OpenClaw Release Publish` parent dispatches from a protected lightweight
|
||||
`release-publish/<sha12>-<epoch>` tag at the frozen trusted-main Tooling SHA and
|
||||
uses `npm_dist_tag=extended-stable` to publish official npm plugins and core, attach evidence, publish Docker, and
|
||||
finalize a non-Latest GitHub Release. Only `extended-stable*` container aliases
|
||||
advance; ClawHub, native-app, website, regular npm `latest`, and private
|
||||
dist-tag surfaces are excluded. Core-resume recovery verifies existing registry
|
||||
bytes before resuming evidence and finalization; Docker-only recovery leaves
|
||||
GitHub finalization untouched. See [Monthly Gateway extended-stable
|
||||
publication](/reference/RELEASING#monthly-gateway-extended-stable-publication)
|
||||
for commands and recovery.
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ OpenClaw exposes four user-facing update channels:
|
|||
- dev: the moving head of `main`
|
||||
|
||||
Extended-stable ships the trailing month's Gateway, official npm plugins, and
|
||||
Docker images without moving regular `latest` or `main` selectors.
|
||||
Docker images without moving regular `latest` or `main` selectors. Each release
|
||||
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).
|
||||
|
||||
|
|
@ -165,6 +167,10 @@ validation must identify one commit. Before `.33`, protected `main` must contain
|
|||
a final version below patch `33` exactly one calendar month later, making the
|
||||
release the trailing completed month. Maintenance patches remain eligible only
|
||||
while that holds; the older line retires when `main` advances another month.
|
||||
The shared publisher checks live `main` before dispatching publication children;
|
||||
each plugin checks it again immediately before npm publication, including
|
||||
trusted-main recovery. Saved qualification does not authorize a retired line.
|
||||
A missing or unreadable current-main version blocks publication.
|
||||
|
||||
### Prepare and stabilize the candidate
|
||||
|
||||
|
|
@ -246,75 +252,41 @@ equals `VALIDATION_SHA`, then push signed `vYYYY.M.P`. Later changes need the ne
|
|||
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 shared release orchestrator from a protected lightweight tooling tag
|
||||
at the frozen trusted-main Tooling SHA, selecting the extended-stable npm track.
|
||||
With publication/tag-push authority, create and push that tooling tag before
|
||||
dispatch; keep it distinct from the immutable product release tag:
|
||||
|
||||
```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.
|
||||
|
||||
For a plugin workflow-only recovery, use the same command with `--ref main`.
|
||||
The trusted workflow still requires `ref` to equal the canonical monthly branch
|
||||
tip and runs its tooling against that frozen source. This retains validated
|
||||
dependency pins when the candidate's older tooling rejects later npm `latest`
|
||||
drift. Save the successful recovery run ID and use the trusted-main core
|
||||
recovery command below; it verifies the plugin workflow's main ancestry and
|
||||
exact candidate-bound run identity.
|
||||
|
||||
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 \
|
||||
TOOLING_SHA="<recorded-full-main-ancestor-sha>"
|
||||
PUBLISH_REF="release-publish/$(printf '%s' "$TOOLING_SHA" | cut -c1-12)-$(date +%s)"
|
||||
git tag "$PUBLISH_REF" "$TOOLING_SHA"
|
||||
git push origin "refs/tags/$PUBLISH_REF"
|
||||
gh workflow run openclaw-release-publish.yml \
|
||||
--ref "$PUBLISH_REF" \
|
||||
-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 \
|
||||
--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 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
|
||||
|
||||
|
|
@ -331,24 +303,15 @@ preflight, and tarball-digest binding to the release SHA. Both commands must
|
|||
return `YYYY.M.P`. Verify every prepared core package and `all-publishable`
|
||||
official plugin at its exact version and selector.
|
||||
|
||||
If only the root selector fails, use the generated
|
||||
`npm dist-tag add openclaw@YYYY.M.P extended-stable` repair command printed in
|
||||
the workflow summary. Repair existing plugin or other prepared-core selectors
|
||||
through approved credential-isolated tooling; the OIDC source cannot mutate
|
||||
them. Never republish an immutable version.
|
||||
If core npm published but the parent failed afterward, repeat the same
|
||||
`OpenClaw Release Publish` command with
|
||||
`-f openclaw_npm_resume_run_id=<successful-core-publish-run-id>`. The parent
|
||||
must prove the live registry tarball is the preflight artifact before it resumes
|
||||
release evidence, Docker, and the shared finalizer.
|
||||
|
||||
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 Docker-only extended-stable path rechecks the
|
||||
saved npm preflight artifact, exact `Full Release Validation` evidence, exact npm
|
||||
version and `extended-stable` selector, and published tarball digest before it
|
||||
calls the reusable `Docker Release` workflow. A tag push never publishes Docker
|
||||
images by itself:
|
||||
If npm publication and its selector are already complete but only Docker
|
||||
publication needs recovery, use the narrower Docker-only path from current
|
||||
`main`:
|
||||
|
||||
```bash
|
||||
gh workflow run openclaw-release-publish.yml \
|
||||
|
|
@ -362,14 +325,73 @@ gh workflow run openclaw-release-publish.yml \
|
|||
-f publish_docker_only=true
|
||||
```
|
||||
|
||||
This path rechecks the exact npm version, `extended-stable` selector, preflight
|
||||
tarball digest, and validation evidence before invoking `Docker Release`. It
|
||||
does not run the shared GitHub Release finalizer; use the core-resume path when
|
||||
the draft release also needs evidence attachment or publication.
|
||||
|
||||
To promote an already-published core version to `extended-stable`, use
|
||||
**OpenClaw NPM Dist-Tag Operations** in
|
||||
`openclaw/releases`, not the publish or resume path. The `promote_extended_stable`
|
||||
mode requires [openclaw/releases#27](https://github.com/openclaw/releases/pull/27)
|
||||
to be merged and available on that repository's `main`:
|
||||
|
||||
```bash
|
||||
gh workflow run openclaw-npm-dist-tags.yml \
|
||||
--repo openclaw/releases --ref main \
|
||||
-f mode=promote_extended_stable \
|
||||
-f tag=vYYYY.M.PATCH
|
||||
```
|
||||
|
||||
Replace `vYYYY.M.PATCH` with the exact approved final extended-stable release tag
|
||||
(patch `33` or higher, without a suffix). Extended-stable fixes increment the
|
||||
patch (`33`, `34`, `35`, and so on), never a correction suffix. Regular stable/beta
|
||||
promotion and sync reject patch `33`
|
||||
or higher, including the scheduled beta floor. Promotion can select a newer version or roll back to an older one. The action checks
|
||||
that the public Git tag and exact npm version exist, permits older monthly lines
|
||||
and historical unsuffixed final versions, and changes only core `openclaw`'s
|
||||
`extended-stable` selector. It uses the release repository's `NPM_TOKEN`; no local
|
||||
npm login or source-repository publish credentials are needed. It does not write
|
||||
`latest`, `beta`, plugin or other prepared-core selectors, Docker aliases, Git
|
||||
tags, or GitHub Releases, and does not republish packages.
|
||||
|
||||
Wait for the run to succeed and verify the intended target:
|
||||
|
||||
```bash
|
||||
npm view openclaw dist-tags --json --prefer-online --registry=https://registry.npmjs.org/
|
||||
```
|
||||
|
||||
The job summary records the previous and target versions. The action skips an
|
||||
already-correct selector and retries registry readback, not the tag write. After
|
||||
an unconfirmed write or exhausted readback, inspect the live selector before
|
||||
retrying. Repair plugin or other prepared-core selectors separately through
|
||||
approved credential-isolated tooling. A selector rollback neither repairs the
|
||||
bad version's published bytes nor downgrades existing installations. Do not
|
||||
resume publication of a rejected release as part of rollback.
|
||||
|
||||
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. 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.
|
||||
an explicit rollback, and never rebuilds images. npm retagging does not invoke
|
||||
this action; if Docker aliases must also move, dispatch it separately with an
|
||||
existing extended-stable image tag and verify all three aliases on both
|
||||
registries. Docker derives the channel from the target version,
|
||||
so a historical regular-stable tag is not an extended-stable Docker rollback.
|
||||
|
||||
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 Releases, ClawHub, native apps, mobile,
|
||||
website, and private dist-tags; do not run those steps for this Gateway path.
|
||||
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
|
||||
|
||||
|
|
@ -987,8 +1009,9 @@ For package-candidate Telegram proof, enable `telegram_mode=mock-openai` or `tel
|
|||
|
||||
Run the read-only publish preflight before regular beta or stable publication
|
||||
through the protected `OpenClaw Release Publish` route, including after a failed
|
||||
attempt. Alpha uses its matching Tideclaw workflow branch; extended-stable retains
|
||||
its separate owner workflows and is not admitted by this command. Use the same
|
||||
attempt. Alpha uses its matching Tideclaw workflow branch; extended-stable uses
|
||||
the shared publisher with its dedicated track inputs but is not admitted by
|
||||
this regular-release preflight command. Use the same
|
||||
tag, validation run and attempt, channel, plugin selection, waiver, and frozen
|
||||
publication tooling ref as the intended dispatch:
|
||||
|
||||
|
|
@ -1121,8 +1144,9 @@ This button covers core and plugin npm, ClawHub, the existing Docker/Windows
|
|||
contracts, and GitHub release visibility. It does **not** claim that independent
|
||||
macOS signing/feed promotion, Android completion, app-store submission, or
|
||||
website publication is ready. Those owners retain their existing release steps.
|
||||
Alpha, extended-stable, selected-plugin repairs, and historical releases without
|
||||
a readiness receipt continue to use their existing owner workflows.
|
||||
Alpha, selected-plugin repairs, and historical releases without a readiness
|
||||
receipt continue to use their existing owner workflows. Extended-stable uses
|
||||
the shared direct publisher with its dedicated track inputs, not this button.
|
||||
|
||||
### Recover a failed download
|
||||
|
||||
|
|
@ -1230,9 +1254,9 @@ non-publishing child, then rerun only the outer seal.
|
|||
|
||||
For beta, `latest`, plugin, GitHub Release, and platform publication,
|
||||
`OpenClaw Release Publish` remains the protected mutating owner. The monthly
|
||||
`.33+` Gateway extended-stable path does not use this orchestrator. The
|
||||
regular workflow orchestrates the trusted-publisher workflows in the order the
|
||||
release needs. Linux cross-OS validation remains blocking; Windows/macOS
|
||||
`.33+` Gateway extended-stable path uses this same publisher with its own
|
||||
track inputs, non-Latest GitHub release, and no ClawHub or native publication.
|
||||
The workflow orchestrates the trusted publishers for the selected track. Linux cross-OS validation remains blocking; Windows/macOS
|
||||
cross-OS conclusions are advisory and cannot block the saved validation
|
||||
evidence. macOS app signing, notarization, appcast updates, and Windows Hub asset
|
||||
promotion can run in parallel with or after npm publication and never delay
|
||||
|
|
@ -1240,13 +1264,13 @@ npm. Their artifact contracts still govern platform readiness and GitHub
|
|||
release closeout. Full Release Validation and qualified package artifacts must already be green; no app artifact is a prerequisite:
|
||||
|
||||
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).
|
||||
2. Verify the tag is reachable from `main` or `release/*`, a Tideclaw alpha branch for alpha prereleases, or the canonical `extended-stable/YYYY.M.33` branch for extended-stable.
|
||||
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 extended-stable.
|
||||
6. After plugin npm succeeds, 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. ClawHub proceeds in parallel.
|
||||
7. Verify the published npm package and selector readback, then call reusable `Docker Release` with the immutable tag and SHA. By default, finalize the draft GitHub release after npm and Docker evidence succeeds. The explicitly requested `finalize_release_before_docker=true` fast path activates after npm verification and evidence uploads, then publishes Docker; Docker remains part of the Gateway distribution.
|
||||
8. For stable, optionally dispatch `Windows Node Release` after finalization with both `windows_node_tag` and candidate-approved `windows_node_installer_digests`. It attaches signed installers and checksums to the public release as a detached child. Omit both inputs to skip Windows dispatch. When the tagged `apps/android/version.json` matches the release train, qualify and dispatch `Android Release` independently for its exact-tag signed APK, checksum, and provenance; run macOS validation/preflight/publish through `openclaw/releases` in parallel or afterward. No app workflow delays npm or GitHub release finalization. Track app failures through their summaries and evidence, then recover only the failed platform.
|
||||
7. Verify the published npm package and selector readback, then call reusable `Docker Release` with the immutable tag and SHA. By default, finalize the draft GitHub release after npm and Docker evidence succeeds. The explicitly requested `finalize_release_before_docker=true` fast path activates after npm verification and evidence uploads, then publishes Docker; Docker remains part of the Gateway distribution. Extended-stable finalization uses the shared finalizer with `latest=false` and skips native stages.
|
||||
8. For regular stable, optionally dispatch `Windows Node Release` after finalization with both `windows_node_tag` and candidate-approved `windows_node_installer_digests`. It attaches signed installers and checksums to the public release as a detached child. Omit both inputs to skip Windows dispatch. When the tagged `apps/android/version.json` matches the release train, qualify and dispatch `Android Release` independently for its exact-tag signed APK, checksum, and provenance; run macOS validation/preflight/publish through `openclaw/releases` in parallel or afterward. No app workflow delays npm or GitHub release finalization. Track app failures through their summaries and evidence, then recover only the failed platform.
|
||||
|
||||
The Android train is pinned independently. If its tagged version differs from
|
||||
the stable tag's base version, the parent skips both native qualification and
|
||||
|
|
@ -1551,8 +1575,11 @@ 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. The workflow may run from
|
||||
that branch or trusted `main` for workflow-only recovery. It never moves plugin
|
||||
`extended-stable/YYYY.M.33` branch at its exact tip, or the same immutable
|
||||
target dispatched by `OpenClaw Release Publish` from its protected
|
||||
`release-publish/<sha12>-<epoch>` tooling tag with that canonical branch named
|
||||
in `release_candidate_branch`. The direct workflow may also run from trusted
|
||||
`main` for approved workflow-only recovery. 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
|
||||
|
|
@ -1568,6 +1595,7 @@ readback confirms that every exact package and `extended-stable` tag converged.
|
|||
- `windows_node_tag`: optional exact non-prerelease `openclaw/openclaw-windows-node` release tag for detached Windows promotion after stable GitHub publication; omit both Windows inputs to skip dispatch
|
||||
- `windows_node_installer_digests`: candidate-approved compact JSON map of the current Windows installer names to pinned `sha256:` digests; required only when `windows_node_tag` is supplied
|
||||
- `npm_telegram_run_id`: optional successful `NPM Telegram Beta E2E` run id to include in final release evidence
|
||||
- `openclaw_npm_resume_run_id`: successful original core publish run ID; verifies the registry tarball against preflight before resuming release evidence, Docker, and finalization without republishing core
|
||||
- `npm_dist_tag`: npm target tag for the OpenClaw package, one of `alpha`, `beta`, `latest`, or `extended-stable`
|
||||
- `finalize_release_before_docker`: explicit direct-publication fast path; default `false`. Activates the verified GitHub release before Docker, preserving the same environment approval and latest policy. Requires `publish_openclaw_npm=true` and no `prepared_plugins`. Docker failure leaves the release public for Docker-only recovery.
|
||||
- `publish_docker_only`: beta, regular stable (`latest`), or extended-stable recovery/closeout path. It requires `publish_openclaw_npm=false`, complete preflight and Full Release Validation evidence, then verifies the exact npm package, selected dist-tag, and tarball digest before invoking Docker publication.
|
||||
|
|
@ -1592,7 +1620,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:
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ its exact prepared publication artifacts for both roles. For a later
|
|||
changelog-only Release SHA using evidence reuse, also record the reuse policy,
|
||||
complete changed-path set, green Code SHA parent run, and Release SHA parent
|
||||
run. For extended-stable, record the canonical branch, exact release SHA,
|
||||
fresh parent run id and attempt, workflow ref, every child run, and any
|
||||
accepted producer identity, parent run id and attempt, workflow ref, every child run, and any
|
||||
frozen-target compatibility repair or intentional omission.
|
||||
|
||||
Useful artifacts:
|
||||
|
|
|
|||
|
|
@ -41,6 +41,15 @@ canonical-branch dispatch is valid only when its head is also the trusted
|
|||
workflow implementation. Current extended-stable validation uses distinct
|
||||
trusted-main tooling and therefore requires the immutable helper.
|
||||
|
||||
The shared publisher requires this canonical `release-ci/*` producer and binds
|
||||
its trusted workflow SHA separately from the exact candidate SHA. Its protected
|
||||
`release-publish/*` ref does not replace the canonical candidate branch. Retain
|
||||
the complete `rerun_group=all` manifest, exact run ID and successful attempt;
|
||||
reject direct canonical-branch/main producers, narrow runs, stale attempts, and
|
||||
mismatched targets. If a reviewed tooling repair changes the publication SHA,
|
||||
the validation tooling must remain reachable from current `main` and all
|
||||
candidate and evidence identities must still match.
|
||||
|
||||
Backport product failures; make the smallest behavior-preserving repair for
|
||||
frozen-target tooling; retry provider, approval, or runner failures without a
|
||||
source change. Any branch change needs a complete new run. Do not omit required
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ type OpenClawReleaseClawHubPlanArgs = {
|
|||
releasePublishRunId: string;
|
||||
pluginPublishScope: PluginReleaseSelectionMode;
|
||||
plugins: string[];
|
||||
skipClawHub?: boolean;
|
||||
preparedArtifact?: string;
|
||||
};
|
||||
|
||||
|
|
@ -266,6 +267,7 @@ export function parseOpenClawReleaseClawHubPlanArgs(
|
|||
let pluginPublishScope: PluginReleaseSelectionMode | undefined;
|
||||
let plugins: string[] = [];
|
||||
let pluginsFlagProvided = false;
|
||||
let skipClawHub = false;
|
||||
let preparedArtifact: string | undefined;
|
||||
|
||||
for (let index = 0; index < values.length; index += 1) {
|
||||
|
|
@ -314,6 +316,9 @@ export function parseOpenClawReleaseClawHubPlanArgs(
|
|||
plugins = parsePluginReleaseSelection(next());
|
||||
pluginsFlagProvided = true;
|
||||
break;
|
||||
case "--skip-clawhub":
|
||||
skipClawHub = true;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
|
|
@ -344,6 +349,7 @@ export function parseOpenClawReleaseClawHubPlanArgs(
|
|||
releasePublishRunId: requireArg(releasePublishRunId, "--release-publish-run-id"),
|
||||
pluginPublishScope: resolvedPluginPublishScope,
|
||||
plugins,
|
||||
skipClawHub,
|
||||
...(preparedArtifact ? { preparedArtifact } : {}),
|
||||
};
|
||||
}
|
||||
|
|
@ -367,34 +373,37 @@ export async function buildOpenClawReleaseClawHubPlan(
|
|||
"releasePublishRunAttempt",
|
||||
);
|
||||
const releasePublishRunId = requireArg(args.releasePublishRunId, "releasePublishRunId");
|
||||
const prepared = args.preparedArtifact
|
||||
? await resolvePreparedClawHubMatrix({
|
||||
descriptor: JSON.parse(args.preparedArtifact),
|
||||
candidateSha: releaseSha,
|
||||
toolingSha: bootstrapWorkflowSha,
|
||||
selectionMode: args.pluginPublishScope,
|
||||
plugins: args.plugins,
|
||||
sourceRoot: options.rootDir ?? resolve("."),
|
||||
token: process.env.GH_TOKEN,
|
||||
fetchImpl: options.fetchImpl,
|
||||
})
|
||||
: undefined;
|
||||
const plan = prepared
|
||||
? {
|
||||
// Prepared publication requires established normal trusted publishers;
|
||||
// the resolver rejects bootstrap/repair needs before this routing.
|
||||
candidates: prepared,
|
||||
bootstrapCandidates: [],
|
||||
missingTrustedPublisher: [],
|
||||
warnings: [],
|
||||
}
|
||||
: await collectPluginClawHubReleasePlan({
|
||||
rootDir: options.rootDir ?? resolve("."),
|
||||
selection: args.plugins,
|
||||
selectionMode: args.pluginPublishScope,
|
||||
fetchImpl: options.fetchImpl,
|
||||
registryBaseUrl: options.registryBaseUrl,
|
||||
});
|
||||
const prepared =
|
||||
!args.skipClawHub && args.preparedArtifact
|
||||
? await resolvePreparedClawHubMatrix({
|
||||
descriptor: JSON.parse(args.preparedArtifact),
|
||||
candidateSha: releaseSha,
|
||||
toolingSha: bootstrapWorkflowSha,
|
||||
selectionMode: args.pluginPublishScope,
|
||||
plugins: args.plugins,
|
||||
sourceRoot: options.rootDir ?? resolve("."),
|
||||
token: process.env.GH_TOKEN,
|
||||
fetchImpl: options.fetchImpl,
|
||||
})
|
||||
: undefined;
|
||||
const plan = args.skipClawHub
|
||||
? { candidates: [], bootstrapCandidates: [], missingTrustedPublisher: [], warnings: [] }
|
||||
: prepared
|
||||
? {
|
||||
// Prepared publication requires established normal trusted publishers;
|
||||
// the resolver rejects bootstrap/repair needs before this routing.
|
||||
candidates: prepared,
|
||||
bootstrapCandidates: [],
|
||||
missingTrustedPublisher: [],
|
||||
warnings: [],
|
||||
}
|
||||
: await collectPluginClawHubReleasePlan({
|
||||
rootDir: options.rootDir ?? resolve("."),
|
||||
selection: args.plugins,
|
||||
selectionMode: args.pluginPublishScope,
|
||||
fetchImpl: options.fetchImpl,
|
||||
registryBaseUrl: options.registryBaseUrl,
|
||||
});
|
||||
|
||||
const normalPackages = packageNames(plan.candidates);
|
||||
const bootstrapPackages = [
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export const PLUGIN_NPM_RELEASE_AUTHORITY_PATHS = [
|
|||
"scripts/lib/plugin-npm-release.ts",
|
||||
"scripts/lib/tsx-cli-shim.mjs",
|
||||
"scripts/tsx.mjs",
|
||||
"scripts/openclaw-npm-extended-stable-release.mjs",
|
||||
"scripts/plugin-npm-publish.sh",
|
||||
"scripts/plugin-npm-prepared-release.mjs",
|
||||
"scripts/plugin-npm-release-check.ts",
|
||||
|
|
|
|||
|
|
@ -28,11 +28,11 @@ print_release_resume_command() {
|
|||
}
|
||||
|
||||
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]*)?$ ]]
|
||||
}
|
||||
|
||||
resolve_child_workflow_ref() {
|
||||
|
|
@ -765,7 +765,7 @@ write_clawhub_runtime_state() {
|
|||
local output_path="$1"
|
||||
local force_skip_clawhub=false
|
||||
# Verification and release notes project the same joined child outcomes.
|
||||
if [[ "${clawhub_failed}" != "0" ]]; then
|
||||
if [[ "${RELEASE_NPM_DIST_TAG}" == "extended-stable" || "${clawhub_failed}" != "0" ]]; then
|
||||
force_skip_clawhub=true
|
||||
fi
|
||||
node --import tsx \
|
||||
|
|
|
|||
|
|
@ -732,13 +732,18 @@ function mirror(github, publicKey, target) {
|
|||
function finalizeCore(github, options) {
|
||||
const version = options.tag.slice(1);
|
||||
const parsed = parseReleaseVersion(version);
|
||||
const train = parsed && classifyReleaseTrain(parsed);
|
||||
assert(
|
||||
parsed &&
|
||||
parsed.version === version &&
|
||||
["stable", "alpha", "beta"].includes(classifyReleaseTrain(parsed)),
|
||||
["stable", "alpha", "beta", "extended-stable"].includes(train),
|
||||
"Unsupported core GitHub release train",
|
||||
);
|
||||
assert(["true", "false"].includes(options.latest), "Expected explicit core latest intent");
|
||||
assert(
|
||||
train !== "extended-stable" || options.latest === "false",
|
||||
"Extended-stable releases cannot become core latest",
|
||||
);
|
||||
const prerelease = parsed.channel !== "stable";
|
||||
assert(!prerelease || options.latest === "false", "Prereleases cannot become core latest");
|
||||
github.authorize();
|
||||
|
|
|
|||
|
|
@ -186,7 +186,14 @@ export function validateExtendedStableNpmReleaseRequest(request) {
|
|||
};
|
||||
}
|
||||
|
||||
const mainVersion = parseReleaseVersion(request.mainPackageVersion);
|
||||
validateActiveExtendedStableLine(releaseVersion, request.mainPackageVersion);
|
||||
return { extendedStable: true, releaseVersion, extendedStableBranch };
|
||||
}
|
||||
|
||||
// Core admission, parent dispatch, and plugin mutation share one retirement policy.
|
||||
export function validateActiveExtendedStableLine(releaseVersion, mainPackageVersion) {
|
||||
const releaseVersionParsed = validateNpmPublishBoundary(releaseVersion, "extended-stable");
|
||||
const mainVersion = parseReleaseVersion(mainPackageVersion);
|
||||
if (
|
||||
mainVersion === null ||
|
||||
mainVersion.channel !== "stable" ||
|
||||
|
|
@ -195,19 +202,18 @@ export function validateExtendedStableNpmReleaseRequest(request) {
|
|||
throw new Error("Protected main package version must be an exact final YYYY.M.P version.");
|
||||
}
|
||||
const mainCalendarMonth = mainVersion.year * 12 + mainVersion.month;
|
||||
const releaseCalendarMonth = taggedVersion.year * 12 + taggedVersion.month;
|
||||
const releaseCalendarMonth = releaseVersionParsed.year * 12 + releaseVersionParsed.month;
|
||||
// Keep one active trailing-month line; advancing main another month retires the older line.
|
||||
if (mainCalendarMonth - releaseCalendarMonth !== 1) {
|
||||
const expectedYear = mainVersion.month === 1 ? mainVersion.year - 1 : mainVersion.year;
|
||||
const expectedMonth = mainVersion.month === 1 ? 12 : mainVersion.month - 1;
|
||||
throw new Error(
|
||||
`Extended-stable publishes only the trailing completed month: protected main ${request.mainPackageVersion} allows ${expectedYear}.${expectedMonth}.PATCH, not ${releaseVersion}. Retire the older line or dispatch with BYPASS_EXTENDED_STABLE_GUARD for an explicitly approved exception.`,
|
||||
`Extended-stable publishes only the trailing completed month: protected main ${mainPackageVersion} allows ${expectedYear}.${expectedMonth}.PATCH, not ${releaseVersion}. Retire the older line; publishing a retired line requires an explicit maintainer decision.`,
|
||||
);
|
||||
}
|
||||
if (classifyReleaseTrain(mainVersion) !== "stable") {
|
||||
throw new Error("Protected main must remain on a daily patch below 33.");
|
||||
}
|
||||
return { extendedStable: true, releaseVersion, extendedStableBranch };
|
||||
}
|
||||
|
||||
export function validateExtendedStableRunIdentity({
|
||||
|
|
@ -221,6 +227,8 @@ export function validateExtendedStableRunIdentity({
|
|||
fullReleaseRunId = "",
|
||||
fullReleaseRunAttempt = "",
|
||||
workflowPath = "",
|
||||
expectedOrchestratorBranch = "",
|
||||
expectedOrchestratorSha = "",
|
||||
trustedPluginWorkflowSha = "",
|
||||
}) {
|
||||
const fullReleasePreflight =
|
||||
|
|
@ -260,6 +268,15 @@ export function validateExtendedStableRunIdentity({
|
|||
);
|
||||
}
|
||||
}
|
||||
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;
|
||||
// FRV runs trusted tooling against a separately pinned release source; its
|
||||
// qualified manifest, not the workflow head, binds that source SHA.
|
||||
// A main-branch plugin recovery likewise separates tooling from source. The
|
||||
|
|
@ -277,7 +294,8 @@ export function validateExtendedStableRunIdentity({
|
|||
!fullReleasePreflight &&
|
||||
!trustedPluginRecovery &&
|
||||
npmDistTag === "extended-stable" &&
|
||||
(run.headBranch !== expectedBranch || run.headSha !== expectedSha)
|
||||
!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>"}.`,
|
||||
|
|
@ -539,6 +557,20 @@ function appendOutput(values) {
|
|||
|
||||
async function main() {
|
||||
const command = process.argv[2];
|
||||
if (command === "validate-active-line") {
|
||||
const repository = process.env.GITHUB_REPOSITORY ?? "";
|
||||
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(repository)) {
|
||||
throw new Error("GITHUB_REPOSITORY must identify the publication repository.");
|
||||
}
|
||||
const content = execFileSync(
|
||||
"gh",
|
||||
["api", `repos/${repository}/contents/package.json?ref=refs/heads/main`, "--jq", ".content"],
|
||||
{ encoding: "utf8", timeout: 30_000 },
|
||||
);
|
||||
const mainPackageVersion = JSON.parse(Buffer.from(content, "base64").toString("utf8")).version;
|
||||
validateActiveExtendedStableLine(process.env.PACKAGE_VERSION ?? "", mainPackageVersion);
|
||||
return;
|
||||
}
|
||||
if (command === "validate-request") {
|
||||
const result = validateRequestFromRepository();
|
||||
console.log(
|
||||
|
|
@ -573,6 +605,8 @@ async function main() {
|
|||
fullReleaseRunId: process.env.FULL_RELEASE_VALIDATION_RUN_ID,
|
||||
fullReleaseRunAttempt: process.env.FULL_RELEASE_VALIDATION_RUN_ATTEMPT,
|
||||
workflowPath: process.env.RUN_WORKFLOW_PATH,
|
||||
expectedOrchestratorBranch: process.env.EXPECTED_ORCHESTRATOR_BRANCH,
|
||||
expectedOrchestratorSha: process.env.EXPECTED_ORCHESTRATOR_SHA,
|
||||
trustedPluginWorkflowSha: process.env.TRUSTED_PLUGIN_WORKFLOW_SHA,
|
||||
});
|
||||
console.log(`Verified referenced ${process.env.RUN_KIND} run.`);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,11 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|||
import { fileURLToPath } from "node:url";
|
||||
import { normalizeUpgradeSurvivorBaselineSpec } from "./lib/docker-e2e-plan.mts";
|
||||
import { resolveNpmJsonEntries } from "./lib/npm-json-output.mts";
|
||||
import { compareReleaseVersions, parseReleaseVersion } from "./lib/release-version.mjs";
|
||||
import {
|
||||
classifyReleaseTrain,
|
||||
compareReleaseVersions,
|
||||
parseReleaseVersion,
|
||||
} from "./lib/release-version.mjs";
|
||||
import { OLDEST_SUPPORTED_UPGRADE_SURVIVOR_BASELINE } from "./lib/upgrade-survivor-policy.mjs";
|
||||
|
||||
type ReleaseRecord = Partial<Record<"isPrerelease" | "publishedAt" | "tagName", unknown>>;
|
||||
|
|
@ -85,16 +89,16 @@ function readPublishedVersions(file: string | undefined) {
|
|||
|
||||
function stableVersionFromTag(tagName: unknown) {
|
||||
const version = typeof tagName === "string" ? tagName.replace(/^v/u, "") : "";
|
||||
return parseStableVersion(version) ? version : undefined;
|
||||
return parseVersionForTrain(version) ? version : undefined;
|
||||
}
|
||||
|
||||
function parseStableVersion(version: unknown) {
|
||||
function parseVersionForTrain(version: unknown, train: "stable" | "extended-stable" = "stable") {
|
||||
const parsed = parseReleaseVersion(typeof version === "string" ? version : "");
|
||||
return parsed?.channel === "stable" ? parsed : undefined;
|
||||
return parsed && classifyReleaseTrain(parsed) === train ? parsed : undefined;
|
||||
}
|
||||
|
||||
function compareStableVersions(left: string, right: string) {
|
||||
if (!parseStableVersion(left) || !parseStableVersion(right)) {
|
||||
if (!parseVersionForTrain(left) || !parseVersionForTrain(right)) {
|
||||
throw new Error(`cannot compare release versions: ${left} ${right}`);
|
||||
}
|
||||
const comparison = compareReleaseVersions(left, right);
|
||||
|
|
@ -220,11 +224,13 @@ function resolveSupportedLines(args: Map<string, string>) {
|
|||
throw new Error("npm dist-tags must be a JSON object");
|
||||
}
|
||||
const latest = "latest" in tags ? tags.latest : undefined;
|
||||
if (typeof latest !== "string" || !parseStableVersion(latest) || !versions.has(latest)) {
|
||||
if (typeof latest !== "string" || !parseVersionForTrain(latest) || !versions.has(latest)) {
|
||||
throw new Error("npm latest must name a published stable version");
|
||||
}
|
||||
const previous = [...versions]
|
||||
.filter((version) => parseStableVersion(version) && compareStableVersions(version, latest) < 0)
|
||||
.filter(
|
||||
(version) => parseVersionForTrain(version) && compareStableVersions(version, latest) < 0,
|
||||
)
|
||||
.toSorted((left, right) => compareStableVersions(right, left))[0];
|
||||
if (!previous) {
|
||||
throw new Error(`no previous stable npm version before ${latest}`);
|
||||
|
|
@ -237,9 +243,13 @@ function resolveSupportedLines(args: Map<string, string>) {
|
|||
const extended = "extended-stable" in tags ? tags["extended-stable"] : undefined;
|
||||
if (
|
||||
extended !== undefined &&
|
||||
(typeof extended !== "string" || !parseStableVersion(extended) || !versions.has(extended))
|
||||
(typeof extended !== "string" ||
|
||||
!parseVersionForTrain(extended, "extended-stable") ||
|
||||
!versions.has(extended))
|
||||
) {
|
||||
throw new Error("npm extended-stable must name a published stable version when present");
|
||||
throw new Error(
|
||||
"npm extended-stable must name a published extended-stable version when present",
|
||||
);
|
||||
}
|
||||
return omitUnpublishedCandidateBaseline(
|
||||
args,
|
||||
|
|
@ -276,7 +286,7 @@ export function resolveBaselines(args: Map<string, string>) {
|
|||
resolved.push(...resolveLastStable(args, count));
|
||||
} else if (token.startsWith("all-since-")) {
|
||||
const minimumVersion = token.slice("all-since-".length);
|
||||
if (!parseStableVersion(minimumVersion)) {
|
||||
if (!parseVersionForTrain(minimumVersion)) {
|
||||
throw new Error(`invalid all-since baseline token: ${token}`);
|
||||
}
|
||||
resolved.push(...resolveAllSince(args, minimumVersion));
|
||||
|
|
|
|||
|
|
@ -1496,6 +1496,65 @@ describe("buildOpenClawReleaseClawHubPlan", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it.each([undefined, '{"unusedPreparedArtifact":true}'])(
|
||||
"returns a zero-dispatch plan without reading ClawHub when the release track excludes it (%s)",
|
||||
async (preparedArtifact) => {
|
||||
const plan = await buildOpenClawReleaseClawHubPlan(
|
||||
{
|
||||
bootstrapWorkflowRef: "main",
|
||||
bootstrapWorkflowSha: "d".repeat(40),
|
||||
releaseTag: "v2026.6.35",
|
||||
releaseSha: "a".repeat(40),
|
||||
releasePublishBranch: "main",
|
||||
releasePublishFullRef: "refs/heads/main",
|
||||
releasePublishRunAttempt: "1",
|
||||
releasePublishRunId: "12345",
|
||||
pluginPublishScope: "all-publishable",
|
||||
plugins: [],
|
||||
skipClawHub: true,
|
||||
...(preparedArtifact ? { preparedArtifact } : {}),
|
||||
},
|
||||
{
|
||||
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-full-ref",
|
||||
"refs/heads/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([
|
||||
|
|
|
|||
123
test/scripts/extended-stable-active-line.test.ts
Normal file
123
test/scripts/extended-stable-active-line.test.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import { spawnSync } from "node:child_process";
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
import { validateActiveExtendedStableLine } from "../../scripts/openclaw-npm-extended-stable-release.mjs";
|
||||
import { resolveTestNodeExecPath } from "../../src/test-utils/node-process.js";
|
||||
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
||||
|
||||
const nodeExecutable = resolveTestNodeExecPath();
|
||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
describe("extended-stable live publication eligibility", () => {
|
||||
it("retires the same qualified candidate when main advances, including the year boundary", () => {
|
||||
expect(() => validateActiveExtendedStableLine("2026.12.34", "2027.1.1")).not.toThrow();
|
||||
expect(() => validateActiveExtendedStableLine("2026.12.34", "2027.2.1")).toThrow(
|
||||
"only the trailing completed month",
|
||||
);
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === "win32").each([
|
||||
{ mainVersion: "2026.8.1", expectedStatus: 42, expectedError: "" },
|
||||
{
|
||||
mainVersion: "2026.9.1",
|
||||
expectedStatus: 1,
|
||||
expectedError: "only the trailing completed month",
|
||||
},
|
||||
{
|
||||
mainVersion: "unavailable",
|
||||
expectedStatus: 1,
|
||||
expectedError: "fixture main API unavailable",
|
||||
},
|
||||
])(
|
||||
"checks live main $mainVersion before entering the parent's mutation-capable publication phase",
|
||||
({ mainVersion, expectedStatus, expectedError }) => {
|
||||
const root = tempDirs.make("extended-stable-dispatch-");
|
||||
const bin = join(root, "bin");
|
||||
const harness = join(root, ".release-harness/scripts");
|
||||
mkdirSync(bin);
|
||||
mkdirSync(join(harness, "lib"), { recursive: true });
|
||||
for (const script of [
|
||||
"openclaw-npm-extended-stable-release.mjs",
|
||||
"lib/release-version.mjs",
|
||||
]) {
|
||||
writeFileSync(join(harness, script), readFileSync(join("scripts", script)));
|
||||
}
|
||||
// Keep the real sourced helper and stop at the next phase boundary. This
|
||||
// prevents all publishing while proving the active case crosses admission.
|
||||
writeFileSync(
|
||||
join(harness, "lib/release-publish-children.sh"),
|
||||
`${readFileSync("scripts/lib/release-publish-children.sh", "utf8")}\nverify_release_tag_target() { echo admitted >> "$EVENTS"; exit 42; }\n`,
|
||||
);
|
||||
const events = join(root, "events");
|
||||
writeFileSync(events, "");
|
||||
writeFileSync(join(bin, "node"), `#!/bin/sh\nexec "${nodeExecutable}" "$@"\n`, {
|
||||
mode: 0o755,
|
||||
});
|
||||
writeFileSync(
|
||||
join(bin, "gh"),
|
||||
`#!${nodeExecutable}
|
||||
const fs = require("node:fs");
|
||||
const args = process.argv.slice(2);
|
||||
fs.appendFileSync(process.env.EVENTS, JSON.stringify(args) + "\\n");
|
||||
if (JSON.stringify(args) !== JSON.stringify(["api", "repos/openclaw/openclaw/contents/package.json?ref=refs/heads/main", "--jq", ".content"])) {
|
||||
process.stderr.write("Unexpected GitHub operation");
|
||||
process.exit(90);
|
||||
}
|
||||
if (${JSON.stringify(mainVersion)} === "unavailable") {
|
||||
process.stderr.write("fixture main API unavailable");
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write(${JSON.stringify(Buffer.from(JSON.stringify({ version: mainVersion })).toString("base64"))});
|
||||
`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
const workflow = parse(
|
||||
readFileSync(".github/workflows/openclaw-release-publish.yml", "utf8"),
|
||||
) as { jobs: Record<string, { steps?: { name?: string; run?: string }[] }> };
|
||||
const dispatch = Object.values(workflow.jobs)
|
||||
.flatMap((job) => job.steps ?? [])
|
||||
.find((step) => step.name === "Dispatch publish workflows");
|
||||
expect(dispatch?.run).toBeTruthy();
|
||||
// GitHub resolves expressions before passing the run body to bash.
|
||||
const run = dispatch!.run!.replaceAll(/\$\{\{[^}]*\}\}/gu, "fixture");
|
||||
const result = spawnSync("/bin/bash", ["--noprofile", "--norc", "-c", run], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
timeout: 15_000,
|
||||
env: {
|
||||
PATH: `${bin}:/usr/bin:/bin`,
|
||||
EVENTS: events,
|
||||
GITHUB_WORKSPACE: root,
|
||||
GITHUB_REPOSITORY: "openclaw/openclaw",
|
||||
GITHUB_REF: "refs/tags/release-publish/bbbbbbbbbbbb-123",
|
||||
GITHUB_STEP_SUMMARY: join(root, "summary"),
|
||||
PARENT_WORKFLOW_SHA: "b".repeat(40),
|
||||
CHILD_WORKFLOW_REF: "release-publish/bbbbbbbbbbbb-123",
|
||||
RELEASE_TAG: "v2026.7.34",
|
||||
TARGET_SHA: "a".repeat(40),
|
||||
RELEASE_NPM_DIST_TAG: "extended-stable",
|
||||
PUBLISH_OPENCLAW_NPM: "true",
|
||||
WAIT_FOR_CLAWHUB: "false",
|
||||
RUNNER_TEMP: root,
|
||||
BYPASS_EXTENDED_STABLE_GUARD: "true",
|
||||
},
|
||||
});
|
||||
expect(result.status, result.stderr).toBe(expectedStatus);
|
||||
if (expectedError) {
|
||||
expect(result.stderr).toContain(expectedError);
|
||||
}
|
||||
const calls = readFileSync(events, "utf8").trim().split("\n");
|
||||
expect(calls).toEqual([
|
||||
JSON.stringify([
|
||||
"api",
|
||||
"repos/openclaw/openclaw/contents/package.json?ref=refs/heads/main",
|
||||
"--jq",
|
||||
".content",
|
||||
]),
|
||||
...(expectedStatus === 42 ? ["admitted"] : []),
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -1571,6 +1571,8 @@ it.each([null, tag])(
|
|||
|
||||
it.each([
|
||||
{ releaseTag: nextTag, prerelease: false },
|
||||
{ releaseTag: "v2026.6.33", prerelease: false },
|
||||
{ releaseTag: "v2026.8.35", prerelease: false },
|
||||
{ releaseTag: "v2026.9.4-alpha.1", prerelease: true },
|
||||
{ releaseTag: "v2026.9.4-beta.1", prerelease: true },
|
||||
])("honors explicit non-latest finalization of $releaseTag", ({ releaseTag, prerelease }) => {
|
||||
|
|
@ -1641,8 +1643,8 @@ it("refuses non-latest finalization that would demote the current latest", () =>
|
|||
it.each([
|
||||
{
|
||||
releaseTag: "v2026.6.33",
|
||||
latest: "false",
|
||||
message: "Unsupported core GitHub release train",
|
||||
latest: "true",
|
||||
message: "Extended-stable releases cannot become core latest",
|
||||
},
|
||||
{
|
||||
releaseTag: "v2026.9.04",
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@ describe("extended-stable npm release request", () => {
|
|||
["main a year-plus ahead", "2028.12.32", "2028.11"],
|
||||
])("rejects %s", (_label, mainPackageVersion, expectedMonth) => {
|
||||
expect(() => validateExtendedStableNpmReleaseRequest({ ...valid, mainPackageVersion })).toThrow(
|
||||
`Extended-stable publishes only the trailing completed month: protected main ${mainPackageVersion} allows ${expectedMonth}.PATCH, not 2026.6.33. Retire the older line or dispatch with BYPASS_EXTENDED_STABLE_GUARD for an explicitly approved exception.`,
|
||||
`Extended-stable publishes only the trailing completed month: protected main ${mainPackageVersion} allows ${expectedMonth}.PATCH, not 2026.6.33. Retire the older line; publishing a retired line requires an explicit maintainer decision.`,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -494,6 +494,48 @@ describe("extended-stable npm run identity", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("accepts a plugin run dispatched by the trusted protected-tag orchestrator for the exact target", () => {
|
||||
const workflowSha = "c".repeat(40);
|
||||
const toolingRef = `release-publish/${workflowSha.slice(0, 12)}-123`;
|
||||
const pluginRun = {
|
||||
workflowName: "Plugin NPM Release",
|
||||
displayTitle: `Plugin NPM Release [extended-stable] ${sha}`,
|
||||
event: "workflow_dispatch",
|
||||
status: "completed",
|
||||
conclusion: "success",
|
||||
headBranch: toolingRef,
|
||||
headSha: workflowSha,
|
||||
};
|
||||
expect(() =>
|
||||
validateExtendedStableRunIdentity({
|
||||
run: pluginRun,
|
||||
kind: "plugin",
|
||||
npmDistTag: "extended-stable",
|
||||
expectedBranch: branch,
|
||||
expectedSha: sha,
|
||||
expectedOrchestratorBranch: toolingRef,
|
||||
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: toolingRef,
|
||||
expectedOrchestratorSha: workflowSha,
|
||||
}),
|
||||
).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts plugin recovery only with authenticated main tooling and the exact source identity", () => {
|
||||
const toolingSha = "b".repeat(40);
|
||||
const run = {
|
||||
|
|
|
|||
|
|
@ -383,7 +383,7 @@ describe("minimal npm extended-stable workflow", () => {
|
|||
expect(summary.run).toContain("Extended-stable guard bypass: ${BYPASS_EXTENDED_STABLE_GUARD}");
|
||||
});
|
||||
|
||||
it("lets main promote only the canonical immutable extended-stable candidate", () => {
|
||||
it("lets protected tooling promote only the canonical immutable extended-stable candidate", () => {
|
||||
const parsed = workflow();
|
||||
const releaseDocs = readFileSync("docs/reference/RELEASING.md", "utf8");
|
||||
const input = parsed.on?.workflow_dispatch?.inputs?.release_candidate_branch;
|
||||
|
|
@ -404,20 +404,97 @@ describe("minimal npm extended-stable workflow", () => {
|
|||
);
|
||||
expect(trustedRef.env?.RELEASE_CANDIDATE_BRANCH).toBe("${{ inputs.release_candidate_branch }}");
|
||||
expect(trustedRef.run).toContain('release_candidate_branch="${RELEASE_CANDIDATE_BRANCH:-}"');
|
||||
expect(trustedRef.run).toContain('"${WORKFLOW_REF}" != "refs/heads/main"');
|
||||
expect(trustedRef.run).toContain(
|
||||
'! "${WORKFLOW_REF}" =~ ^refs/tags/release-publish/[a-f0-9]{12}-[1-9][0-9]*$',
|
||||
);
|
||||
expect(trustedRef.run).toContain(
|
||||
'expected_candidate_branch="extended-stable/${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.33"',
|
||||
);
|
||||
|
||||
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('--ref "$PUBLISH_REF"');
|
||||
expect(releaseDocs).toContain(
|
||||
"The parent derives the canonical `extended-stable/YYYY.M.33` branch",
|
||||
);
|
||||
expect(releaseDocs).toContain('git tag "$PUBLISH_REF" "$TOOLING_SHA"');
|
||||
expect(releaseDocs).toContain("The helper dispatches from an immutable `release-ci/*` ref");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ label: "trusted-main recovery", workflowRef: "refs/heads/main", status: 0 },
|
||||
{
|
||||
label: "protected publisher",
|
||||
workflowRef: "refs/tags/release-publish/bbbbbbbbbbbb-123",
|
||||
status: 0,
|
||||
},
|
||||
{ label: "feature branch", workflowRef: "refs/heads/feature/recovery", status: 1 },
|
||||
{
|
||||
label: "release branch with candidate override",
|
||||
workflowRef: "refs/heads/release/2026.8.1",
|
||||
status: 1,
|
||||
},
|
||||
{ label: "ordinary tag", workflowRef: "refs/tags/v2026.8.34", status: 1 },
|
||||
{
|
||||
label: "wrong candidate month",
|
||||
workflowRef: "refs/heads/main",
|
||||
candidate: "extended-stable/2026.7.33",
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
label: "noncanonical candidate branch",
|
||||
workflowRef: "refs/heads/main",
|
||||
candidate: "extended-stable/2026.8.34",
|
||||
status: 1,
|
||||
},
|
||||
{ label: "latest selector", workflowRef: "refs/heads/main", npmDistTag: "latest", status: 1 },
|
||||
{ label: "beta selector", workflowRef: "refs/heads/main", npmDistTag: "beta", status: 1 },
|
||||
{ label: "correction suffix", workflowRef: "refs/heads/main", tag: "v2026.8.34-1", status: 1 },
|
||||
{ label: "non-tag candidate", workflowRef: "refs/heads/main", tag: "a".repeat(40), status: 1 },
|
||||
{
|
||||
label: "wrong protected SHA prefix",
|
||||
workflowRef: "refs/tags/release-publish/aaaaaaaaaaaa-123",
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
label: "moved protected tag",
|
||||
workflowRef: "refs/tags/release-publish/bbbbbbbbbbbb-123",
|
||||
remoteSha: "c".repeat(40),
|
||||
status: 1,
|
||||
},
|
||||
])(
|
||||
"checks the actual publication admission shell for $label",
|
||||
({ workflowRef, candidate, npmDistTag, tag, remoteSha, status }) => {
|
||||
const guard = step(
|
||||
workflow().jobs?.validate_publish_request,
|
||||
"Require trusted workflow ref for publish",
|
||||
);
|
||||
const result = spawnSync(
|
||||
"bash",
|
||||
[
|
||||
"--noprofile",
|
||||
"--norc",
|
||||
"-c",
|
||||
`gh() { printf '%s\\n' "$REMOTE_WORKFLOW_SHA"; }\n${guard.run}`,
|
||||
],
|
||||
{
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
GITHUB_REPOSITORY: "openclaw/openclaw",
|
||||
RELEASE_TAG: tag ?? "v2026.8.34",
|
||||
RELEASE_NPM_DIST_TAG: npmDistTag ?? "extended-stable",
|
||||
RELEASE_CANDIDATE_BRANCH: candidate ?? "extended-stable/2026.8.33",
|
||||
WORKFLOW_REF: workflowRef,
|
||||
WORKFLOW_SHA: "b".repeat(40),
|
||||
REMOTE_WORKFLOW_SHA: remoteSha ?? "b".repeat(40),
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(result.status, result.stderr).toBe(status);
|
||||
},
|
||||
);
|
||||
|
||||
it("accepts arbitrary SHA preflight targets and exercises every publishable plugin package", () => {
|
||||
const parsed = workflow(preflightWorkflowPath);
|
||||
const preflight = parsed.jobs?.check_contents_npm;
|
||||
|
|
@ -582,6 +659,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 != '' && startsWith(github.ref, 'refs/tags/release-publish/') && github.ref_name || '' }}",
|
||||
);
|
||||
expect(verify.env?.EXPECTED_ORCHESTRATOR_SHA).toBe(
|
||||
"${{ inputs.release_candidate_branch != '' && startsWith(github.ref, 'refs/tags/release-publish/') && github.workflow_sha || '' }}",
|
||||
);
|
||||
expect(verify.run).toContain(
|
||||
"node trusted-workflow/scripts/openclaw-npm-extended-stable-release.mjs verify-run",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2158,22 +2158,24 @@ process.exitCode = 1;
|
|||
|
||||
describe("prepared Windows handoff", () => {
|
||||
it.each([
|
||||
["stable", "v2026.9.2", "success", true, true, false, true],
|
||||
["absent", "v2026.9.2", "success", false, false, false, false],
|
||||
["incomplete", "v2026.9.2", "success", true, false, false, true],
|
||||
["beta", "v2026.9.2-beta.1", "success", true, true, false, false],
|
||||
["alpha", "v2026.9.2-alpha.1", "success", true, true, false, false],
|
||||
["failed activation", "v2026.9.2", "failure", true, true, false, false],
|
||||
["skipped activation", "v2026.9.2", "skipped", true, true, false, false],
|
||||
["dispatch failed", "v2026.9.2", "success", true, true, true, true],
|
||||
["stable on beta", "v2026.9.2", "beta", "success", true, true, false, true],
|
||||
["stable on latest", "v2026.9.2", "latest", "success", true, true, false, true],
|
||||
["absent", "v2026.9.2", "beta", "success", false, false, false, false],
|
||||
["incomplete", "v2026.9.2", "beta", "success", true, false, false, true],
|
||||
["beta", "v2026.9.2-beta.1", "beta", "success", true, true, false, false],
|
||||
["alpha", "v2026.9.2-alpha.1", "alpha", "success", true, true, false, false],
|
||||
["failed activation", "v2026.9.2", "beta", "failure", true, true, false, false],
|
||||
["skipped activation", "v2026.9.2", "beta", "skipped", true, true, false, false],
|
||||
["dispatch failed", "v2026.9.2", "beta", "success", true, true, true, true],
|
||||
] as const)(
|
||||
"uses the frozen optional selection after activation: %s",
|
||||
(_label, tag, activation, selected, digests, dispatchFailure, scheduled) => {
|
||||
(_label, tag, channel, activation, selected, digests, dispatchFailure, scheduled) => {
|
||||
const fixture = finalizationFixture({ windowsDispatchFailure: dispatchFailure });
|
||||
const ready = readyRelease();
|
||||
ready.inputs = {
|
||||
...ready.inputs,
|
||||
tag,
|
||||
npm_dist_tag: channel,
|
||||
windows_node_tag: selected ? "v1.2.3" : "",
|
||||
windows_node_installer_digests: digests
|
||||
? JSON.stringify({
|
||||
|
|
|
|||
|
|
@ -3973,11 +3973,13 @@ function runReleasePublishInputValidation(overrides: Record<string, string>) {
|
|||
writeFileSync(join(binDir, "gh"), '#!/bin/sh\nprintf "%s\\n" "$WORKFLOW_SHA"\n', {
|
||||
mode: 0o755,
|
||||
});
|
||||
return spawnSync("bash", ["-c", script], {
|
||||
const githubOutput = resolve(tempDirs.make("release-publish-inputs-"), "github-output");
|
||||
return spawnSync("bash", ["--noprofile", "--norc", "-c", script], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
FULL_RELEASE_VALIDATION_RUN_ATTEMPT: "1",
|
||||
FULL_RELEASE_VALIDATION_RUN_ID: "222",
|
||||
GITHUB_OUTPUT: githubOutput,
|
||||
OPENCLAW_NPM_RESUME_RUN_ID: "",
|
||||
GITHUB_REPOSITORY: "openclaw/openclaw",
|
||||
PATH: `${binDir}:${process.env.PATH}`,
|
||||
|
|
@ -4040,7 +4042,7 @@ function runOpenClawNpmTrustedRefGuard(overrides: Record<string, string>) {
|
|||
`#!/bin/sh\n[ "$1" = "--signal=TERM" ] && [ "$2" = "--kill-after=10s" ] && [ "$3" = "120s" ] || exit 2\nshift 3\nexec "$@"\n`,
|
||||
);
|
||||
chmodSync(timeoutPath, 0o755);
|
||||
return spawnSync("bash", ["-c", script], {
|
||||
return spawnSync("bash", ["--noprofile", "--norc", "-c", script], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
GITHUB_REPOSITORY: "openclaw/openclaw",
|
||||
|
|
@ -5031,7 +5033,7 @@ describe("package acceptance workflow", () => {
|
|||
expect(verifyStep.run).not.toContain("npm view openclaw@extended-stable version");
|
||||
});
|
||||
|
||||
it("accepts only exact protected SHA-pinned release publish tags", () => {
|
||||
it("accepts only main-reachable protected SHA-pinned release publish tags", () => {
|
||||
const workflowSha = "a".repeat(40);
|
||||
const binDir = tempDirs.make("release-publish-gh-");
|
||||
const ghPath = `${binDir}/gh`;
|
||||
|
|
@ -5289,6 +5291,7 @@ dispatch_workflow_at_ref "$WORKFLOW_REF" "$PARENT_WORKFLOW_SHA" plugin-clawhub-r
|
|||
RELEASE_PLUGINS: plugin.packageName,
|
||||
BASE_REF: "",
|
||||
NPM_DIST_TAG: "default",
|
||||
RELEASE_NPM_DIST_TAG: "latest",
|
||||
PREFLIGHT_ONLY: "false",
|
||||
DRY_RUN: "false",
|
||||
PREPARED_ARTIFACT: "",
|
||||
|
|
@ -5418,7 +5421,7 @@ const fs=require("node:fs");fs.writeFileSync("install-proof.json",JSON.stringify
|
|||
expect(planner["working-directory"]).toBeUndefined();
|
||||
}
|
||||
expect(identity.if).toBe(
|
||||
"github.event_name == 'workflow_dispatch' && (inputs.preflight_only || (inputs.npm_dist_tag == 'extended-stable' && github.ref == 'refs/heads/main'))",
|
||||
"github.event_name == 'workflow_dispatch' && (inputs.preflight_only || inputs.release_candidate_branch != '' || (inputs.npm_dist_tag == 'extended-stable' && github.ref == 'refs/heads/main'))",
|
||||
);
|
||||
expect(identity.env).toMatchObject({
|
||||
GH_TOKEN: "${{ github.token }}",
|
||||
|
|
@ -6565,6 +6568,7 @@ wait_for_run openclaw-npm-release.yml 404 "$EXPECTED_SHA" "$STARTED_JOB" "$APPRO
|
|||
env: {
|
||||
PATH: `${root}:${process.env.PATH}`,
|
||||
RELEASE_TAG: "v2026.9.1",
|
||||
RELEASE_NPM_DIST_TAG: "latest",
|
||||
PUBLISH_OPENCLAW_NPM: "true",
|
||||
PUBLISH_DOCKER_ONLY: "false",
|
||||
GITHUB_OUTPUT: join(root, "output"),
|
||||
|
|
@ -6602,6 +6606,11 @@ wait_for_run openclaw-npm-release.yml 404 "$EXPECTED_SHA" "$STARTED_JOB" "$APPRO
|
|||
env: {
|
||||
PATH: `${root}:${process.env.PATH}`,
|
||||
RELEASE_TAG: tag,
|
||||
RELEASE_NPM_DIST_TAG: tag.includes("-alpha.")
|
||||
? "alpha"
|
||||
: tag.includes("-beta.")
|
||||
? "beta"
|
||||
: "latest",
|
||||
PUBLISH_OPENCLAW_NPM: "true",
|
||||
PUBLISH_DOCKER_ONLY: "false",
|
||||
GITHUB_OUTPUT: join(root, "output"),
|
||||
|
|
@ -7264,6 +7273,9 @@ NODE
|
|||
expect(workflow).toContain("main_ref: ${{ steps.inputs.outputs.main_ref }}");
|
||||
expect(workflow).toContain("TRIGGER_SHA: ${{ github.sha }}");
|
||||
expect(workflow).toContain('main_ref="$TRIGGER_SHA"');
|
||||
expect(workflow).toContain('classifyReleaseTrain(parsed) === "stable"');
|
||||
expect(workflow).toContain('classifyReleaseTrain(parsed) !== "stable"');
|
||||
expect(workflow).toContain("below the extended-stable .33 boundary");
|
||||
expect(workflow).toContain("ref: ${{ needs.resolve.outputs.main_ref }}");
|
||||
expect(
|
||||
workflowStep(
|
||||
|
|
@ -13876,6 +13888,7 @@ printf '%s\\n' "$DEEPSEEK_API_KEY" "$DEEPINFRA_API_KEY"`,
|
|||
"sparse-checkout": "scripts",
|
||||
});
|
||||
expect(validateManifest.env).toMatchObject({
|
||||
EXPECTED_WORKFLOW_BRANCH: "${{ github.ref_name }}",
|
||||
PUBLICATION_CONSUMER:
|
||||
"${{ inputs.publish_docker_only && !inputs.publish_openclaw_npm && 'docker-only' || 'publisher' }}",
|
||||
PUBLISH_DOCKER_ONLY: "${{ inputs.publish_docker_only }}",
|
||||
|
|
@ -14186,13 +14199,33 @@ printf '%s\\n' "$DEEPSEEK_API_KEY" "$DEEPINFRA_API_KEY"`,
|
|||
});
|
||||
|
||||
it.each([
|
||||
["omitted", false, false, false, 0],
|
||||
["selected", true, true, false, 0],
|
||||
["dispatch failure", true, true, true, 1],
|
||||
["missing digests", true, false, false, 1],
|
||||
{ scenario: "omitted", selected: false, hasDigests: false, dispatchFailure: false, exit: 0 },
|
||||
{ scenario: "selected", selected: true, hasDigests: true, dispatchFailure: false, exit: 0 },
|
||||
{
|
||||
scenario: "dispatch failure",
|
||||
selected: true,
|
||||
hasDigests: true,
|
||||
dispatchFailure: true,
|
||||
exit: 1,
|
||||
},
|
||||
{
|
||||
scenario: "missing digests",
|
||||
selected: true,
|
||||
hasDigests: false,
|
||||
dispatchFailure: false,
|
||||
exit: 1,
|
||||
},
|
||||
{
|
||||
scenario: "extended-stable",
|
||||
selected: true,
|
||||
hasDigests: true,
|
||||
dispatchFailure: false,
|
||||
exit: 0,
|
||||
},
|
||||
])(
|
||||
"dispatches optional Windows promotion without waiting: %s",
|
||||
(_scenario, selected, hasDigests, dispatchFailure, exit) => {
|
||||
"dispatches optional Windows promotion without waiting: $scenario",
|
||||
({ scenario, selected, hasDigests, dispatchFailure, exit }) => {
|
||||
const shouldDispatch = selected && hasDigests && scenario !== "extended-stable";
|
||||
const source = readFileSync("scripts/lib/release-publish-children.sh", "utf8");
|
||||
const root = tempDirs.make("windows-detached-publish-");
|
||||
const dispatchPath = join(root, "dispatch");
|
||||
|
|
@ -14230,6 +14263,7 @@ promote_windows_release_assets
|
|||
GITHUB_STEP_SUMMARY: summaryPath,
|
||||
GITHUB_REPOSITORY: "openclaw/openclaw",
|
||||
RELEASE_TAG: "v2026.9.1",
|
||||
RELEASE_NPM_DIST_TAG: scenario === "extended-stable" ? "extended-stable" : "latest",
|
||||
CHILD_WORKFLOW_REF: workflowRef,
|
||||
PARENT_WORKFLOW_SHA: workflowSha,
|
||||
WINDOWS_NODE_TAG: selected ? "v0.6.3" : "",
|
||||
|
|
@ -14239,8 +14273,8 @@ promote_windows_release_assets
|
|||
);
|
||||
expect(result.status, result.stderr).toBe(exit);
|
||||
expect(result.stderr).not.toContain("unexpected-windows-wait");
|
||||
expect(existsSync(dispatchPath)).toBe(selected && hasDigests);
|
||||
if (selected && hasDigests) {
|
||||
expect(existsSync(dispatchPath)).toBe(shouldDispatch);
|
||||
if (shouldDispatch) {
|
||||
expect(readFileSync(dispatchPath, "utf8").trim().split("\n")).toEqual([
|
||||
workflowRef,
|
||||
workflowSha,
|
||||
|
|
@ -14254,7 +14288,7 @@ promote_windows_release_assets
|
|||
]);
|
||||
}
|
||||
expect(readFileSync(summaryPath, "utf8").includes("actions/runs/456")).toBe(
|
||||
selected && hasDigests && !dispatchFailure,
|
||||
shouldDispatch && !dispatchFailure,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { runInNewContext } from "node:vm";
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
import { PLUGIN_NPM_RELEASE_AUTHORITY_PATHS } from "../../scripts/lib/plugin-publication-candidates.ts";
|
||||
import { validateActiveExtendedStableLine } from "../../scripts/openclaw-npm-extended-stable-release.mjs";
|
||||
import { createStablePluginNpmBootstrapApproval } from "../../scripts/plugin-npm-bootstrap-approval.mjs";
|
||||
import { resolveTestNodeExecPath } from "../../src/test-utils/node-process.js";
|
||||
import { requireNodeTool } from "../helpers/node-toolchain.js";
|
||||
|
|
@ -144,7 +145,12 @@ function runStableBootstrapAdmission(
|
|||
);
|
||||
return spawnSync(
|
||||
"/bin/bash",
|
||||
["-c", step(workflow().jobs?.publish_plugins_npm, "Authorize bootstrap release").run!],
|
||||
[
|
||||
"--noprofile",
|
||||
"--norc",
|
||||
"-c",
|
||||
step(workflow().jobs?.publish_plugins_npm, "Authorize bootstrap release").run!,
|
||||
],
|
||||
{
|
||||
encoding: "utf8",
|
||||
timeout: 15_000,
|
||||
|
|
@ -420,6 +426,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 protected release tooling publishes its immutable target",
|
||||
required: false,
|
||||
type: "string",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses one override for check, plan, pack, and publish", () => {
|
||||
|
|
@ -463,10 +475,38 @@ describe("plugin npm extended-stable workflow", () => {
|
|||
{ publishTag: "beta", toolingTrusted: true, candidateMoved: false },
|
||||
{ publishTag: "extended-stable", toolingTrusted: true, candidateMoved: false },
|
||||
{ publishTag: "extended-stable", toolingTrusted: true, candidateMoved: true },
|
||||
{
|
||||
publishTag: "extended-stable",
|
||||
toolingTrusted: true,
|
||||
candidateMoved: false,
|
||||
mainVersion: "2026.9.1",
|
||||
expectedFailure: "only the trailing completed month",
|
||||
},
|
||||
{
|
||||
publishTag: "extended-stable",
|
||||
toolingTrusted: true,
|
||||
candidateMoved: false,
|
||||
mainApiUnavailable: true,
|
||||
expectedFailure: "fixture main API unavailable",
|
||||
},
|
||||
{
|
||||
publishTag: "extended-stable",
|
||||
toolingTrusted: true,
|
||||
candidateMoved: false,
|
||||
mainVersion: "invalid",
|
||||
expectedFailure: "Protected main package version",
|
||||
},
|
||||
{ publishTag: "latest", toolingTrusted: false, candidateMoved: false },
|
||||
])(
|
||||
"publishes sealed bytes only with current authority: $publishTag / trusted $toolingTrusted / moved $candidateMoved",
|
||||
({ publishTag, toolingTrusted, candidateMoved }) => {
|
||||
"publishes sealed bytes only with current authority: $publishTag / trusted $toolingTrusted / moved $candidateMoved / main $mainVersion / unavailable $mainApiUnavailable",
|
||||
({
|
||||
publishTag,
|
||||
toolingTrusted,
|
||||
candidateMoved,
|
||||
mainVersion = "2026.8.1",
|
||||
mainApiUnavailable = false,
|
||||
expectedFailure,
|
||||
}) => {
|
||||
const nodeExecutable = requireNodeTool("node");
|
||||
const npmCli = realpathSync(requireNodeTool("npm"));
|
||||
const root = mkdtempSync(join(tmpdir(), "plugin-oidc-artifact-"));
|
||||
|
|
@ -474,7 +514,12 @@ describe("plugin npm extended-stable workflow", () => {
|
|||
const bin = join(root, "bin");
|
||||
mkdirSync(bin);
|
||||
mkdirSync(join(root, "scripts/lib"), { recursive: true });
|
||||
for (const script of ["release-tooling-identity.mjs", "lib/record-shared.mjs"]) {
|
||||
for (const script of [
|
||||
"release-tooling-identity.mjs",
|
||||
"lib/record-shared.mjs",
|
||||
"openclaw-npm-extended-stable-release.mjs",
|
||||
"lib/release-version.mjs",
|
||||
]) {
|
||||
writeFileSync(join(root, "scripts", script), readFileSync(join("scripts", script)));
|
||||
}
|
||||
writeFileSync(
|
||||
|
|
@ -487,13 +532,16 @@ describe("plugin npm extended-stable workflow", () => {
|
|||
writeFileSync(tarball, "sealed preflight bytes");
|
||||
const targetSha = "a".repeat(40);
|
||||
const toolingSha = "b".repeat(40);
|
||||
// Qualification succeeded while main was August. Publication must reread
|
||||
// main even when the qualified candidate and monthly branch remain unchanged.
|
||||
expect(() => validateActiveExtendedStableLine("2026.7.33", "2026.8.1")).not.toThrow();
|
||||
const candidateRef = "refs/heads/extended-stable/2026.7.33";
|
||||
// The artifact was admitted at targetSha; the approval wait may advance the branch.
|
||||
const currentRef = {
|
||||
ref: candidateRef,
|
||||
object: { type: "commit", sha: candidateMoved ? "c".repeat(40) : targetSha },
|
||||
};
|
||||
writeFileSync(join(bin, "node"), `#!/bin/bash\nexec "${nodeExecutable}" "$@"\n`, {
|
||||
writeFileSync(join(bin, "node"), `#!/bin/sh\nexec "${nodeExecutable}" "$@"\n`, {
|
||||
mode: 0o755,
|
||||
});
|
||||
writeFileSync(
|
||||
|
|
@ -506,6 +554,12 @@ if (endpoint === "repos/openclaw/openclaw/compare/${toolingSha}...main") {
|
|||
process.stdout.write(${JSON.stringify(JSON.stringify({ status: toolingTrusted ? "ahead" : "diverged" }))});
|
||||
} else if (endpoint === "repos/openclaw/openclaw/git/ref/heads/extended-stable/2026.7.33") {
|
||||
process.stdout.write(${JSON.stringify(JSON.stringify(currentRef))});
|
||||
} else if (endpoint === "repos/openclaw/openclaw/contents/package.json?ref=refs/heads/main") {
|
||||
if (${mainApiUnavailable}) {
|
||||
process.stderr.write("fixture main API unavailable");
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write(${JSON.stringify(Buffer.from(JSON.stringify({ version: mainVersion })).toString("base64"))});
|
||||
} else {
|
||||
process.stderr.write("Unexpected GitHub request: " + endpoint);
|
||||
process.exit(90);
|
||||
|
|
@ -524,40 +578,47 @@ fs.appendFileSync(process.env.EVENTS, JSON.stringify({ command: "npm", args, byt
|
|||
`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
writeFileSync(join(bin, "timeout"), '#!/bin/bash\nshift 3\nexec "$@"\n', {
|
||||
writeFileSync(join(bin, "timeout"), '#!/bin/sh\nshift 3\nexec "$@"\n', {
|
||||
mode: 0o755,
|
||||
});
|
||||
const publish = step(
|
||||
workflow().jobs?.publish_plugins_npm,
|
||||
"Publish with trusted publisher",
|
||||
);
|
||||
const result = spawnSync("/bin/bash", ["-e", "-o", "pipefail", "-c", publish.run!], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
timeout: 15_000,
|
||||
env: {
|
||||
PATH: `${bin}:/usr/bin:/bin`,
|
||||
EVENTS: events,
|
||||
NPM_CLI: npmCli,
|
||||
RUNNER_TEMP: root,
|
||||
TARBALL_PATH: tarball,
|
||||
PUBLISH_TAG: publishTag,
|
||||
PACKAGE_VERSION: "2026.7.33",
|
||||
RELEASE_TARGET_SHA: targetSha,
|
||||
OPENCLAW_RELEASE_TOOLING_REPOSITORY: "openclaw/openclaw",
|
||||
OPENCLAW_RELEASE_TOOLING_FULL_REF: "refs/heads/main",
|
||||
OPENCLAW_RELEASE_TOOLING_REF: "main",
|
||||
OPENCLAW_RELEASE_TOOLING_SHA: toolingSha,
|
||||
OPENCLAW_RELEASE_PUBLISH_RUN_ID: "",
|
||||
OPENCLAW_RELEASE_PUBLISH_RUN_ATTEMPT: "",
|
||||
OPENCLAW_RELEASE_PUBLISH_REF: "",
|
||||
OPENCLAW_RELEASE_PUBLISH_FULL_REF: "",
|
||||
OPENCLAW_RELEASE_PUBLISH_PARENT_STATE_POLICY: "",
|
||||
NPM_TOKEN: "fixture-token-must-not-reach-npm",
|
||||
NODE_AUTH_TOKEN: "fixture-token-must-not-reach-npm",
|
||||
const result = spawnSync(
|
||||
"/bin/bash",
|
||||
["--noprofile", "--norc", "-e", "-o", "pipefail", "-c", publish.run!],
|
||||
{
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
timeout: 15_000,
|
||||
env: {
|
||||
PATH: `${bin}:/usr/bin:/bin`,
|
||||
EVENTS: events,
|
||||
NPM_CLI: npmCli,
|
||||
RUNNER_TEMP: root,
|
||||
TARBALL_PATH: tarball,
|
||||
PUBLISH_TAG: publishTag,
|
||||
PACKAGE_VERSION: "2026.7.33",
|
||||
GITHUB_REPOSITORY: "openclaw/openclaw",
|
||||
// No environment bypass may override the live pre-mutation guard.
|
||||
BYPASS_EXTENDED_STABLE_GUARD: "true",
|
||||
RELEASE_TARGET_SHA: targetSha,
|
||||
OPENCLAW_RELEASE_TOOLING_REPOSITORY: "openclaw/openclaw",
|
||||
OPENCLAW_RELEASE_TOOLING_FULL_REF: "refs/heads/main",
|
||||
OPENCLAW_RELEASE_TOOLING_REF: "main",
|
||||
OPENCLAW_RELEASE_TOOLING_SHA: toolingSha,
|
||||
OPENCLAW_RELEASE_PUBLISH_RUN_ID: "",
|
||||
OPENCLAW_RELEASE_PUBLISH_RUN_ATTEMPT: "",
|
||||
OPENCLAW_RELEASE_PUBLISH_REF: "",
|
||||
OPENCLAW_RELEASE_PUBLISH_FULL_REF: "",
|
||||
OPENCLAW_RELEASE_PUBLISH_PARENT_STATE_POLICY: "",
|
||||
NPM_TOKEN: "fixture-token-must-not-reach-npm",
|
||||
NODE_AUTH_TOKEN: "fixture-token-must-not-reach-npm",
|
||||
},
|
||||
},
|
||||
});
|
||||
const allowed = toolingTrusted && !candidateMoved;
|
||||
);
|
||||
const allowed = toolingTrusted && !candidateMoved && !expectedFailure;
|
||||
expect(result.status, result.stderr).toBe(allowed ? 0 : 1);
|
||||
expect(readdirSync(root).filter((name) => name.startsWith("plugin-npm-oidc."))).toEqual([]);
|
||||
const calls = readFileSync(events, "utf8")
|
||||
|
|
@ -567,7 +628,8 @@ fs.appendFileSync(process.env.EVENTS, JSON.stringify({ command: "npm", args, byt
|
|||
const npmCalls = calls.filter((call) => call.command === "npm");
|
||||
if (!allowed) {
|
||||
expect(result.stderr).toContain(
|
||||
candidateMoved ? "branch is missing or moved" : "not reachable from current main",
|
||||
expectedFailure ??
|
||||
(candidateMoved ? "branch is missing or moved" : "not reachable from current main"),
|
||||
);
|
||||
expect(npmCalls).toEqual([]);
|
||||
} else {
|
||||
|
|
@ -589,9 +651,11 @@ fs.appendFileSync(process.env.EVENTS, JSON.stringify({ command: "npm", args, byt
|
|||
},
|
||||
]);
|
||||
if (publishTag === "extended-stable") {
|
||||
expect(calls.at(-2)?.endpoint).toBe(
|
||||
expect(calls.slice(-3).map((call) => call.endpoint ?? call.command)).toEqual([
|
||||
"repos/openclaw/openclaw/git/ref/heads/extended-stable/2026.7.33",
|
||||
);
|
||||
"repos/openclaw/openclaw/contents/package.json?ref=refs/heads/main",
|
||||
"npm",
|
||||
]);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
|
|
@ -600,7 +664,7 @@ fs.appendFileSync(process.env.EVENTS, JSON.stringify({ command: "npm", args, byt
|
|||
},
|
||||
);
|
||||
|
||||
it("keeps main recovery bound to the canonical monthly source tip", () => {
|
||||
it("admits exact monthly tips for recovery or canonical candidates from protected tooling", () => {
|
||||
const trusted = step(
|
||||
workflow().jobs?.preview_plugins_npm,
|
||||
"Validate ref is on a trusted publish branch",
|
||||
|
|
@ -615,6 +679,19 @@ fs.appendFileSync(process.env.EVENTS, JSON.stringify({ command: "npm", args, byt
|
|||
expect(trusted.run).toContain(
|
||||
'exact_ref_match(\n "HEAD",\n f"refs/remotes/origin/{extended_branch}"',
|
||||
);
|
||||
expect(trusted.env?.RELEASE_CANDIDATE_BRANCH).toBe(
|
||||
"${{ github.event_name == 'workflow_dispatch' && inputs.release_candidate_branch || '' }}",
|
||||
);
|
||||
expect(trusted.run).toContain("candidate_branch != extended_branch");
|
||||
expect(trusted.run).toContain('r"refs/tags/release-publish/[a-f0-9]{12}-[1-9][0-9]*"');
|
||||
expect(trusted.run).toContain('is_ancestor(f"refs/remotes/origin/{extended_branch}")');
|
||||
expect(trusted.run).toContain('is_ancestor("origin/main", os.environ["WORKFLOW_SHA"])');
|
||||
expect(
|
||||
step(
|
||||
workflow().jobs?.preview_plugins_npm,
|
||||
"Verify trusted preflight or recovery tooling identity",
|
||||
).if,
|
||||
).toContain("inputs.release_candidate_branch != ''");
|
||||
});
|
||||
|
||||
it("binds preflight to an exact source SHA without release-publish approval", () => {
|
||||
|
|
@ -651,16 +728,27 @@ fs.appendFileSync(process.env.EVENTS, JSON.stringify({ command: "npm", args, byt
|
|||
expect(toolingIdentity.run).toContain('--workflow-ref "$WORKFLOW_REF"');
|
||||
expect(toolingIdentity.run).toContain('--workflow-full-ref "$WORKFLOW_FULL_REF"');
|
||||
expect(toolingIdentity.run).toContain('--workflow-sha "$WORKFLOW_SHA"');
|
||||
for (const [preflight, distTag, ref, expected] of [
|
||||
[true, "default", "refs/heads/main", true],
|
||||
[false, "extended-stable", "refs/heads/main", true],
|
||||
[false, "extended-stable", "refs/heads/extended-stable/2026.8.33", false],
|
||||
[false, "default", "refs/heads/main", false],
|
||||
for (const [preflight, distTag, ref, candidateBranch, expected] of [
|
||||
[true, "default", "refs/heads/main", "", true],
|
||||
[false, "extended-stable", "refs/heads/main", "", true],
|
||||
[false, "extended-stable", "refs/heads/extended-stable/2026.8.33", "", false],
|
||||
[false, "default", "refs/heads/main", "", false],
|
||||
[
|
||||
false,
|
||||
"extended-stable",
|
||||
`refs/tags/release-publish/${"d".repeat(12)}-12345`,
|
||||
"extended-stable/2026.8.33",
|
||||
true,
|
||||
],
|
||||
] as const) {
|
||||
expect(
|
||||
runInNewContext(toolingIdentity.if!, {
|
||||
github: { event_name: "workflow_dispatch", ref },
|
||||
inputs: { preflight_only: preflight, npm_dist_tag: distTag },
|
||||
inputs: {
|
||||
preflight_only: preflight,
|
||||
npm_dist_tag: distTag,
|
||||
release_candidate_branch: candidateBranch,
|
||||
},
|
||||
}),
|
||||
).toBe(expected);
|
||||
}
|
||||
|
|
@ -714,6 +802,7 @@ fs.appendFileSync(process.env.EVENTS, JSON.stringify({ command: "npm", args, byt
|
|||
expect(trusted.run).toContain(
|
||||
"Plugin npm preflight must not include a release publish parent run tuple.",
|
||||
);
|
||||
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/*.",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ const modes: Record<
|
|||
step: "Validate ref is on a trusted publish branch",
|
||||
},
|
||||
env: {
|
||||
RELEASE_CANDIDATE_BRANCH: "",
|
||||
NPM_DIST_TAG: "default",
|
||||
PREFLIGHT_ONLY: "false",
|
||||
PREPARED_ARTIFACT: "",
|
||||
|
|
@ -394,6 +395,95 @@ posixIt.each(["refs/heads/extended-stable/2026.8.33", "refs/heads/main"])(
|
|||
55_000,
|
||||
);
|
||||
|
||||
const candidateAdmissionCases: Array<{
|
||||
name: string;
|
||||
env: Record<string, string>;
|
||||
commands: RunOptions["commandResults"];
|
||||
code: number;
|
||||
message: string;
|
||||
}> = [
|
||||
{ name: "qualified protected tooling", env: {}, commands: {}, code: 0, message: "" },
|
||||
{
|
||||
name: "wrong candidate month",
|
||||
env: { RELEASE_CANDIDATE_BRANCH: "extended-stable/2026.7.33" },
|
||||
commands: {},
|
||||
code: 1,
|
||||
message: "release_candidate_branch must be extended-stable/2026.8.33",
|
||||
},
|
||||
{
|
||||
name: "mutable main tooling",
|
||||
env: { WORKFLOW_REF: "refs/heads/main" },
|
||||
commands: {},
|
||||
code: 1,
|
||||
message: "protected release-publish workflow tooling",
|
||||
},
|
||||
{
|
||||
name: "tooling outside main",
|
||||
env: {},
|
||||
commands: { [`merge-base --is-ancestor ${workflowSha} origin/main`]: { code: 1 } },
|
||||
code: 1,
|
||||
message: "workflow revision is not reachable from current main",
|
||||
},
|
||||
{
|
||||
name: "candidate outside monthly branch",
|
||||
env: {},
|
||||
commands: {
|
||||
"merge-base --is-ancestor HEAD refs/remotes/origin/extended-stable/2026.8.33": { code: 1 },
|
||||
},
|
||||
code: 1,
|
||||
message: "target must be reachable from extended-stable/2026.8.33",
|
||||
},
|
||||
{
|
||||
name: "tooling ancestry Git failure",
|
||||
env: {},
|
||||
commands: { [`merge-base --is-ancestor ${workflowSha} origin/main`]: { code: 23 } },
|
||||
code: 23,
|
||||
message: "",
|
||||
},
|
||||
{
|
||||
name: "preflight candidate override",
|
||||
env: { PREFLIGHT_ONLY: "true" },
|
||||
commands: {},
|
||||
code: 1,
|
||||
message: "preflight must not include release_candidate_branch",
|
||||
},
|
||||
{
|
||||
name: "non-extended candidate override",
|
||||
env: { NPM_DIST_TAG: "default" },
|
||||
commands: {},
|
||||
code: 1,
|
||||
message: "release_candidate_branch is only valid for extended-stable publication",
|
||||
},
|
||||
];
|
||||
|
||||
posixIt.each(candidateAdmissionCases)(
|
||||
"npm canonical candidate admission: $name",
|
||||
async ({ env, commands, code, message }) => {
|
||||
const report = await pluginRun("npm-trust", {
|
||||
env: {
|
||||
NPM_DIST_TAG: "extended-stable",
|
||||
PUBLISH_SCOPE: "all-publishable",
|
||||
RELEASE_CANDIDATE_BRANCH: "extended-stable/2026.8.33",
|
||||
WORKFLOW_REF: `refs/tags/release-publish/${workflowSha.slice(0, 12)}-123`,
|
||||
...env,
|
||||
},
|
||||
revisions: { [`${sha}^{commit}`]: sha },
|
||||
commandResults: commands,
|
||||
});
|
||||
expect(report.code, report.output).toBe(code);
|
||||
if (message) {
|
||||
expect(report.output).toContain(message);
|
||||
}
|
||||
if (code === 0) {
|
||||
expect(gitCommands(report).slice(-2)).toEqual([
|
||||
["merge-base", "--is-ancestor", workflowSha, "origin/main"],
|
||||
["merge-base", "--is-ancestor", "HEAD", "refs/remotes/origin/extended-stable/2026.8.33"],
|
||||
]);
|
||||
}
|
||||
},
|
||||
55_000,
|
||||
);
|
||||
|
||||
posixIt.each([
|
||||
["moved canonical tip", "refs/heads/main", "c".repeat(40)],
|
||||
["untrusted workflow branch", "refs/heads/topic", sha],
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ type WorkflowStep = {
|
|||
type WorkflowJob = {
|
||||
"continue-on-error"?: boolean | string;
|
||||
"runs-on"?: string;
|
||||
environment?: string;
|
||||
env?: Record<string, string>;
|
||||
if?: string;
|
||||
needs?: string | string[];
|
||||
|
|
@ -1965,6 +1966,18 @@ describe("release validation no-push transport", () => {
|
|||
const releasePublishPath = ".github/workflows/openclaw-release-publish.yml";
|
||||
const releasePublish = readWorkflow(releasePublishPath);
|
||||
const dockerCall = job(releasePublish, "publish_docker");
|
||||
const resolveTarget = job(releasePublish, "resolve_release_target");
|
||||
const validateInputs = step(resolveTarget, "Validate inputs");
|
||||
const validateEvidence = step(resolveTarget, "Validate full release validation manifest");
|
||||
const validateReleaseBranch = step(
|
||||
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 ?? "";
|
||||
const coreStart = step(publishJob, "Start core npm publication");
|
||||
|
||||
expect(dockerRelease.on?.push).toBeUndefined();
|
||||
expect(dockerRelease.on?.workflow_dispatch).toBeUndefined();
|
||||
|
|
@ -1990,13 +2003,34 @@ describe("release validation no-push transport", () => {
|
|||
// approval; its own guard test covers those safety properties.
|
||||
expect(callers).toEqual(["docker-image-refresh.yml", "openclaw-release-publish.yml"]);
|
||||
|
||||
expect(validateInputs.id).toBe("inputs");
|
||||
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("${{ github.ref_name }}");
|
||||
expect(validateReleaseBranch.run).toContain(
|
||||
'expected_ref="refs/remotes/origin/${EXPECTED_VALIDATION_BRANCH}"',
|
||||
);
|
||||
expect(validateReleaseBranch.run).toContain(
|
||||
"must be reachable from ${EXPECTED_VALIDATION_BRANCH}",
|
||||
);
|
||||
|
||||
expect(dockerCall.needs).toEqual([
|
||||
"resolve_release_target",
|
||||
"publish",
|
||||
"verify_core_npm_registry",
|
||||
"finalize_github_release_before_docker",
|
||||
]);
|
||||
expect(dockerCall.if).toContain("inputs.publish_openclaw_npm");
|
||||
expect(dockerCall.if).toContain("needs.publish.result == 'success'");
|
||||
expect(dockerCall.if).toContain("inputs.publish_docker_only");
|
||||
expect(dockerCall.if).toContain("needs.verify_core_npm_registry.result == 'success'");
|
||||
expect(dockerCall.with).toEqual({
|
||||
tag: "${{ inputs.tag }}",
|
||||
|
|
@ -2031,6 +2065,19 @@ describe("release validation no-push transport", () => {
|
|||
job(releasePublish, "resolve_release_target"),
|
||||
"Validate full release validation manifest",
|
||||
);
|
||||
expect(resolveClawHubPlan.run).toContain("plan_args+=(--skip-clawhub)");
|
||||
expect(dispatchRun).toContain("-f npm_dist_tag=extended-stable");
|
||||
expect(coreStart.run).toContain('-f plugin_npm_run_id="${plugin_npm_run_id}"');
|
||||
for (const nativeJob of [
|
||||
"qualify_android_native",
|
||||
"publish_android",
|
||||
"publish_linux",
|
||||
"publish_windows",
|
||||
]) {
|
||||
expect(job(releasePublish, nativeJob).if).toContain(
|
||||
"inputs.npm_dist_tag != 'extended-stable'",
|
||||
);
|
||||
}
|
||||
expect(validation.env?.EXPECTED_SHA).toBe("${{ steps.ref.outputs.sha }}");
|
||||
expect(validation.run).toContain('--consumer publisher --manifest "$manifest"');
|
||||
expect(job(releasePublish, "finalize_github_release").needs).toEqual([
|
||||
|
|
@ -2053,6 +2100,64 @@ describe("release validation no-push transport", () => {
|
|||
expect(reusablePermissionViolations(DOCKER_RELEASE, "prepare")).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["v2026.9.1", "latest", true],
|
||||
["v2026.9.1-beta.1", "beta", false],
|
||||
["v2026.8.35", "extended-stable", false],
|
||||
] as const)(
|
||||
"finalizes %s through the shared owner with explicit latest intent",
|
||||
(tag, distTag, latest) => {
|
||||
const workflow = readWorkflow(".github/workflows/openclaw-release-publish.yml");
|
||||
const script = step(
|
||||
job(workflow, "finalize_github_release"),
|
||||
"Publish the verified draft release",
|
||||
).run;
|
||||
const root = tempDirs.make("release-finalize-track-");
|
||||
const calls = join(root, "calls");
|
||||
for (const finalizerExit of [0, 1]) {
|
||||
writeFileSync(calls, "");
|
||||
const result = spawnSync(
|
||||
"bash",
|
||||
[
|
||||
"-c",
|
||||
`
|
||||
gh() { printf '%s\\n' "$SOURCE_SHA"; }
|
||||
node() {
|
||||
if [[ "$1 $2" == "scripts/linux-app-channel.mjs finalize-core" ]]; then
|
||||
printf '%s\\n' "$*" >> "$CALLS"
|
||||
return "$FINALIZER_EXIT"
|
||||
fi
|
||||
}
|
||||
${script}
|
||||
`,
|
||||
],
|
||||
{
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
CALLS: calls,
|
||||
FINALIZER_EXIT: String(finalizerExit),
|
||||
RUNNER_TEMP: root,
|
||||
GITHUB_REPOSITORY: "openclaw/openclaw",
|
||||
RELEASE_TAG: tag,
|
||||
RELEASE_NPM_DIST_TAG: distTag,
|
||||
SOURCE_SHA: "a".repeat(40),
|
||||
GITHUB_WORKFLOW_SHA: "b".repeat(40),
|
||||
GITHUB_REF_NAME: "release-publish/bbbbbbbbbbbb-123",
|
||||
GITHUB_REF: "refs/tags/release-publish/bbbbbbbbbbbb-123",
|
||||
GITHUB_RUN_ID: "456",
|
||||
GITHUB_RUN_ATTEMPT: "1",
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(readFileSync(calls, "utf8")).toContain(
|
||||
`finalize-core --tag ${tag} --source-sha ${"a".repeat(40)} --latest ${latest}`,
|
||||
);
|
||||
expect(result.status, result.stderr).toBe(finalizerExit);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps Docker required by default and activates early only after explicit approval", () => {
|
||||
const workflow = readWorkflow(".github/workflows/openclaw-release-publish.yml");
|
||||
expect(workflow.on?.workflow_dispatch?.inputs?.finalize_release_before_docker).toMatchObject({
|
||||
|
|
|
|||
|
|
@ -51,11 +51,15 @@ describe("scripts/resolve-upgrade-survivor-baselines", () => {
|
|||
)?.run;
|
||||
assert(run);
|
||||
// Mixed prereleases put the requested cutoff beyond the first 100 records.
|
||||
const releases = Array.from({ length: 130 }, (_, index) => ({
|
||||
tagName: `v2026.5.${130 - index}${index % 3 === 0 ? "-beta.1" : ""}`,
|
||||
publishedAt: new Date(Date.UTC(2026, 8, 1) - index * 86_400_000).toISOString(),
|
||||
isPrerelease: index % 3 === 0,
|
||||
}));
|
||||
const releases = Array.from({ length: 130 }, (_, index) => {
|
||||
const publishedAt = new Date(Date.UTC(2026, 8, 1) - index * 86_400_000);
|
||||
const version = `${publishedAt.getUTCFullYear()}.${publishedAt.getUTCMonth() + 1}.${publishedAt.getUTCDate()}`;
|
||||
return {
|
||||
tagName: `v${version}${index % 3 === 0 ? "-beta.1" : ""}`,
|
||||
publishedAt: publishedAt.toISOString(),
|
||||
isPrerelease: index % 3 === 0,
|
||||
};
|
||||
});
|
||||
const versions = releases
|
||||
.filter((release) => !release.isPrerelease && release.tagName !== "v2026.5.26")
|
||||
.map((release) => release.tagName.slice(1));
|
||||
|
|
@ -127,7 +131,13 @@ if (process.env.FAIL_API === "true") process.exit(75);
|
|||
expect(readFileSync(output, "utf8")).toBe("");
|
||||
} else {
|
||||
invoke();
|
||||
const expected = versions.filter((version) => Number(version.split(".")[2]) >= 24);
|
||||
const expected = releases
|
||||
.filter(
|
||||
(release) =>
|
||||
release.publishedAt >= "2026-05-24T00:00:00.000Z" &&
|
||||
versions.includes(release.tagName.slice(1)),
|
||||
)
|
||||
.map((release) => release.tagName.slice(1));
|
||||
expect(readFileSync(output, "utf8")).toBe(
|
||||
`baselines=${expected.map((version) => `openclaw@${version}`).join(" ")}\nbaseline_scope=all-scenarios\nbaseline=openclaw@2026.5.24\n`,
|
||||
);
|
||||
|
|
@ -401,9 +411,14 @@ console.log(JSON.stringify(process.argv[4] === "dist-tags"
|
|||
},
|
||||
{
|
||||
tags: { latest: "2026.9.2", "extended-stable": "2026.6.99" },
|
||||
versions: ["2026.6.34", "2026.9.2"],
|
||||
error: "npm extended-stable must name a published stable version",
|
||||
versions: ["2026.6.34", "2026.9.1", "2026.9.2"],
|
||||
error: "npm extended-stable must name a published extended-stable version",
|
||||
},
|
||||
...["2026.9.1", "2026.6.35-1", "2026.6.35-beta.1"].map((extended) => ({
|
||||
tags: { latest: "2026.9.2", "extended-stable": extended },
|
||||
versions: ["2026.6.34", "2026.9.1", "2026.9.2", extended],
|
||||
error: "npm extended-stable must name a published extended-stable version",
|
||||
})),
|
||||
{
|
||||
tags: { latest: "2026.9.2" },
|
||||
versions: ["2026.9.1", "2026.9.2"],
|
||||
|
|
@ -582,6 +597,32 @@ console.log(JSON.stringify(process.argv[4] === "dist-tags"
|
|||
});
|
||||
});
|
||||
|
||||
it("excludes extended-stable GitHub releases from regular stable baselines", () => {
|
||||
const releases = [
|
||||
{
|
||||
isPrerelease: false,
|
||||
publishedAt: "2026-08-02T00:00:00Z",
|
||||
tagName: "v2026.6.34",
|
||||
},
|
||||
{
|
||||
isPrerelease: false,
|
||||
publishedAt: "2026-08-01T00:00:00Z",
|
||||
tagName: "v2026.7.12",
|
||||
},
|
||||
];
|
||||
|
||||
withReleaseFixture(releases, (file) => {
|
||||
expect(
|
||||
resolveBaselines(
|
||||
new Map([
|
||||
["requested", "last-stable-1"],
|
||||
["releases-json", file],
|
||||
]),
|
||||
),
|
||||
).toEqual(["openclaw@2026.7.12"]);
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves the last-stable count when the unpublished candidate is newest", () => {
|
||||
const releases = ["2026.9.4", "2026.9.3", "2026.9.2", "2026.9.1", "2026.8.30"].map(
|
||||
(version, index) => ({
|
||||
|
|
|
|||
|
|
@ -548,6 +548,27 @@ describe("full release validation evidence", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("rejects direct monthly-branch evidence under a protected publisher", () => {
|
||||
const branch = "extended-stable/2026.6.33";
|
||||
expect(() =>
|
||||
validateFullReleaseValidationEvidence({
|
||||
run: releaseRun({ head_branch: branch }),
|
||||
manifest: releaseManifest({
|
||||
workflowRef: branch,
|
||||
workflowFullRef: `refs/heads/${branch}`,
|
||||
targetRef: "v2026.6.35",
|
||||
}),
|
||||
expectedRepository: "openclaw/openclaw",
|
||||
expectedRunId: "123",
|
||||
expectedTargetSha: targetSha,
|
||||
expectedWorkflowBranch: branch,
|
||||
expectedTrustedWorkflowFullRef: `refs/tags/release-publish/${workflowSha.slice(0, 12)}-123`,
|
||||
expectedTrustedWorkflowSha: workflowSha,
|
||||
isTrustedMainAncestor: () => false,
|
||||
}),
|
||||
).toThrow("must use a canonical release-ci producer branch");
|
||||
});
|
||||
|
||||
it("rejects direct main evidence outside current main", () => {
|
||||
expect(() =>
|
||||
validate(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue