mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(release): use monthly patch versions
Switch release train handling to YYYY.M.PATCH monthly patch numbering, preserve pre-transition compatibility, and pin the June 2026 stable/beta floor at 2026.6.5 after the published beta. Verification: - node scripts/run-vitest.mjs run test/appcast.test.ts test/release-check.test.ts test/scripts/package-mac-app.test.ts test/scripts/package-mac-dist.test.ts test/openclaw-npm-release-check.test.ts test/npm-publish-plan.test.ts src/infra/npm-registry-spec.test.ts src/infra/clawhub.test.ts src/plugins/clawhub.test.ts test/plugin-npm-release.test.ts test/scripts/ios-version.test.ts test/scripts/ios-pin-version.test.ts - node --import tsx scripts/plugin-npm-release-check.ts --base-ref origin/main --head-ref HEAD - node --import tsx scripts/plugin-clawhub-release-check.ts --base-ref origin/main --head-ref HEAD - git diff --check origin/main...HEAD - .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --no-web-search
This commit is contained in:
@@ -13,7 +13,7 @@ on:
|
||||
default: true
|
||||
type: boolean
|
||||
public_release_branch:
|
||||
description: Public branch that contains the release tag commit, usually main or release/YYYY.M.D
|
||||
description: Public branch that contains the release tag commit, usually main or release/YYYY.M.PATCH
|
||||
required: false
|
||||
default: main
|
||||
type: string
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "${PUBLIC_RELEASE_BRANCH}" != "main" && ! "${PUBLIC_RELEASE_BRANCH}" =~ ^release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]]; then
|
||||
echo "public_release_branch must be main or release/YYYY.M.D, got ${PUBLIC_RELEASE_BRANCH}." >&2
|
||||
echo "public_release_branch must be main or release/YYYY.M.PATCH, got ${PUBLIC_RELEASE_BRANCH}." >&2
|
||||
exit 1
|
||||
fi
|
||||
RELEASE_SHA=$(git rev-parse HEAD)
|
||||
|
||||
@@ -391,7 +391,7 @@ jobs:
|
||||
tideclaw_alpha_publish=true
|
||||
fi
|
||||
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]] && [[ "${tideclaw_alpha_publish}" != "true" ]]; then
|
||||
echo "Real publish runs must be dispatched from main, release/YYYY.M.D, or a Tideclaw alpha branch for alpha prereleases. Use preflight_only=true for other branch validation."
|
||||
echo "Real publish runs must be dispatched from main, release/YYYY.M.PATCH, or a Tideclaw alpha branch for alpha prereleases. Use preflight_only=true for other branch validation."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
fi
|
||||
fi
|
||||
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ ]] && [[ ! "${WORKFLOW_REF}" =~ ^refs/heads/release-ci/[0-9a-f]{12}-[0-9]+$ ]] && [[ "${tideclaw_alpha_check}" != "true" ]]; then
|
||||
echo "Release checks must be dispatched from main, release/YYYY.M.D, a Full Release Validation release-ci/<sha>-<timestamp> ref, or a Tideclaw alpha branch for alpha prereleases." >&2
|
||||
echo "Release checks must be dispatched from main, release/YYYY.M.PATCH, a Full Release Validation release-ci/<sha>-<timestamp> ref, or a Tideclaw alpha branch for alpha prereleases." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
tideclaw_alpha_publish=true
|
||||
fi
|
||||
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${WORKFLOW_REF}" != "refs/heads/main" && ! "${WORKFLOW_REF}" =~ ^refs/heads/release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$ && "${tideclaw_alpha_publish}" != "true" ]]; then
|
||||
echo "publish_openclaw_npm=true requires dispatching this workflow from main, release/YYYY.M.D, or a Tideclaw alpha branch for alpha prereleases." >&2
|
||||
echo "publish_openclaw_npm=true requires dispatching this workflow from main, release/YYYY.M.PATCH, or a Tideclaw alpha branch for alpha prereleases." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${PLUGIN_PUBLISH_SCOPE}" != "all-publishable" ]]; then
|
||||
|
||||
@@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Mobile: Android provider/model screens now surface expiring, unavailable, unresolved, and attention states more clearly, while iOS settings and Talk tabs keep diagnostics, gateway rows, attachment labels, and unavailable Talk controls reachable.
|
||||
- Memory: QMD search can use the new rerank toggle, and memory adapter status uses the resolved default model identity when checking plain status. (#61834)
|
||||
- Docs/tooling: add Parallel search docs, refresh weather-skill guidance toward `web_fetch`, clarify legacy `openai-codex` auth, document release/test helper scripts, and tighten changed-test routing docs for CI/debugging work. (#90028, #90250) Thanks @fuller-stack-dev.
|
||||
- Release/process: switch release trains to `YYYY.M.PATCH` monthly patch numbering, keep pre-transition tags compatible, and pin the June 2026 floor at `2026.6.5` after the published beta.
|
||||
- Platform maintenance: refresh Android, Swift/macOS, Docker, CodeQL, Buildx, Docker build/push, and Codex Action dependencies for this release train. (#74980, #81757, #86481, #86483, #90601)
|
||||
|
||||
### Fixes
|
||||
|
||||
+5
-5
@@ -116,7 +116,7 @@ Manual CI dispatches run the same job graph as normal CI but force every non-And
|
||||
Manual runs use a unique concurrency group so a release-candidate full suite is not cancelled by another push or PR run on the same ref. The optional `target_ref` input lets a trusted caller run that graph against a branch, tag, or full commit SHA while using the workflow file from the selected dispatch ref.
|
||||
|
||||
```bash
|
||||
gh workflow run ci.yml --ref release/YYYY.M.D
|
||||
gh workflow run ci.yml --ref release/YYYY.M.PATCH
|
||||
gh workflow run ci.yml --ref main -f target_ref=<branch-or-sha> -f include_android=true
|
||||
gh workflow run full-release-validation.yml --ref main -f ref=<branch-or-sha>
|
||||
```
|
||||
@@ -196,7 +196,7 @@ stage matrix, exact workflow job names, profile differences, artifacts, and
|
||||
focused rerun handles.
|
||||
|
||||
`OpenClaw Release Publish` is the manual mutating release workflow. Dispatch it
|
||||
from `release/YYYY.M.D` or `main` after the release tag exists and after the
|
||||
from `release/YYYY.M.PATCH` or `main` after the release tag exists and after the
|
||||
OpenClaw npm preflight has succeeded. It verifies `pnpm plugins:sync:check`,
|
||||
dispatches `Plugin NPM Release` for all publishable plugin packages, dispatches
|
||||
`Plugin ClawHub Release` for the same release SHA, and only then dispatches
|
||||
@@ -204,8 +204,8 @@ dispatches `Plugin NPM Release` for all publishable plugin packages, dispatches
|
||||
|
||||
```bash
|
||||
gh workflow run openclaw-release-publish.yml \
|
||||
--ref release/YYYY.M.D \
|
||||
-f tag=vYYYY.M.D-beta.N \
|
||||
--ref release/YYYY.M.PATCH \
|
||||
-f tag=vYYYY.M.PATCH-beta.N \
|
||||
-f preflight_run_id=<successful-openclaw-npm-preflight-run-id> \
|
||||
-f npm_dist_tag=beta
|
||||
```
|
||||
@@ -334,7 +334,7 @@ gh workflow run package-acceptance.yml \
|
||||
--ref main \
|
||||
-f workflow_ref=main \
|
||||
-f source=ref \
|
||||
-f package_ref=release/YYYY.M.D \
|
||||
-f package_ref=release/YYYY.M.PATCH \
|
||||
-f suite_profile=package \
|
||||
-f telegram_mode=mock-openai
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ Candidate sources:
|
||||
|
||||
Full Release Validation uses `source=artifact` by default, built from the
|
||||
resolved release SHA. For post-publish proof, pass
|
||||
`package_acceptance_package_spec=openclaw@YYYY.M.D` so the same upgrade matrix
|
||||
`package_acceptance_package_spec=openclaw@YYYY.M.PATCH` so the same upgrade matrix
|
||||
targets the shipped npm package instead.
|
||||
|
||||
Release checks call Package Acceptance with the package/update/restart/plugin set:
|
||||
|
||||
@@ -123,12 +123,12 @@ source (config, git tag, git branch, or default).
|
||||
|
||||
## Tagging best practices
|
||||
|
||||
- Tag releases you want git checkouts to land on (`vYYYY.M.D` for stable,
|
||||
`vYYYY.M.D-beta.N` for beta; named semver prerelease suffixes such as
|
||||
- Tag releases you want git checkouts to land on (`vYYYY.M.PATCH` for stable,
|
||||
`vYYYY.M.PATCH-beta.N` for beta; named semver prerelease suffixes such as
|
||||
`-alpha.N`, `-rc.N`, and `-next.N` are not stable targets).
|
||||
- Legacy numeric stable tags such as `vYYYY.M.D-1` and `v1.0.1-1` are still
|
||||
- Legacy numeric stable tags such as `vYYYY.M.PATCH-1` and `v1.0.1-1` are still
|
||||
recognized as stable git tags for compatibility.
|
||||
- `vYYYY.M.D.beta.N` is also recognized for compatibility, but prefer `-beta.N`.
|
||||
- `vYYYY.M.PATCH.beta.N` is also recognized for compatibility, but prefer `-beta.N`.
|
||||
- Keep tags immutable: never move or reuse a tag.
|
||||
- npm dist-tags remain the source of truth for npm installs:
|
||||
- `latest` -> stable
|
||||
|
||||
+48
-37
@@ -15,13 +15,24 @@ OpenClaw has three public release lanes:
|
||||
|
||||
## Version naming
|
||||
|
||||
- Stable release version: `YYYY.M.D`
|
||||
- Git tag: `vYYYY.M.D`
|
||||
- Stable correction release version: `YYYY.M.D-N`
|
||||
- Git tag: `vYYYY.M.D-N`
|
||||
- Beta prerelease version: `YYYY.M.D-beta.N`
|
||||
- Git tag: `vYYYY.M.D-beta.N`
|
||||
- Do not zero-pad month or day
|
||||
- Stable release version: `YYYY.M.PATCH`
|
||||
- Git tag: `vYYYY.M.PATCH`
|
||||
- Stable correction release version: `YYYY.M.PATCH-N`
|
||||
- Git tag: `vYYYY.M.PATCH-N`
|
||||
- Beta prerelease version: `YYYY.M.PATCH-beta.N`
|
||||
- Git tag: `vYYYY.M.PATCH-beta.N`
|
||||
- Do not zero-pad month or patch
|
||||
- Starting with the June 2026 release process update, the third component is a
|
||||
monthly patch counter, not a calendar day. Pre-update tags and npm versions
|
||||
keep their existing names and remain valid; release automation continues to
|
||||
compare them by year, month, patch, channel, and prerelease or correction
|
||||
number.
|
||||
- npm versions are immutable. If a beta tag has already been published, do not
|
||||
delete, republish, or reuse it; cut the next beta number or the next monthly
|
||||
patch instead. Because `2026.6.5-beta.1` was already published during the
|
||||
transition, June 2026 release trains must use patch `5` or higher. Do not
|
||||
publish new June 2026 stable or beta trains as `2026.6.2`, `2026.6.3`, or
|
||||
`2026.6.4`.
|
||||
- `latest` means the current promoted stable npm release
|
||||
- `beta` means the current beta install target
|
||||
- Stable and stable correction releases publish to npm `beta` by default; release operators can target `latest` explicitly, or promote a vetted beta build later
|
||||
@@ -34,7 +45,7 @@ OpenClaw has three public release lanes:
|
||||
|
||||
- Releases move beta-first
|
||||
- Stable follows only after the latest beta is validated
|
||||
- Maintainers normally cut releases from a `release/YYYY.M.D` branch created
|
||||
- Maintainers normally cut releases from a `release/YYYY.M.PATCH` branch created
|
||||
from current `main`, so release validation and fixes do not block new
|
||||
development on `main`
|
||||
- If a beta tag has been pushed or published and needs a fix, maintainers cut
|
||||
@@ -59,7 +70,7 @@ the maintainer-only release runbook.
|
||||
`src/commands/doctor/shared/deprecation-compat.ts`. Remove expired
|
||||
compatibility only when the upgrade path stays covered, or record why it is
|
||||
intentionally carried.
|
||||
4. Create `release/YYYY.M.D` from current `main`; do not do normal release work
|
||||
4. Create `release/YYYY.M.PATCH` from current `main`; do not do normal release work
|
||||
directly on `main`.
|
||||
5. Bump every required version location for the intended tag, then run
|
||||
`pnpm release:prep`. It refreshes plugin versions, plugin inventory, config
|
||||
@@ -80,8 +91,8 @@ the maintainer-only release runbook.
|
||||
file, lane, workflow job, package profile, provider, or model allowlist that
|
||||
proves the fix. Rerun the full umbrella only when the changed surface makes
|
||||
prior evidence stale.
|
||||
9. For beta, tag `vYYYY.M.D-beta.N`, then run `pnpm release:candidate -- --tag
|
||||
vYYYY.M.D-beta.N` from the matching `release/YYYY.M.D` branch. The helper runs
|
||||
9. For beta, tag `vYYYY.M.PATCH-beta.N`, then run `pnpm release:candidate -- --tag
|
||||
vYYYY.M.PATCH-beta.N` from the matching `release/YYYY.M.PATCH` branch. The helper runs
|
||||
the local generated-release checks, dispatches or verifies the full release
|
||||
validation and npm preflight evidence, runs Parallels and Telegram package
|
||||
proof, records plugin npm and ClawHub plans, and prints the exact
|
||||
@@ -109,7 +120,7 @@ vYYYY.M.D-beta.N` from the matching `release/YYYY.M.D` branch. The helper runs
|
||||
preview cell flakes, and ends with registry verification for every expected
|
||||
plugin version so partial publishes remain visible and retryable. Then run the post-publish
|
||||
package acceptance against the published
|
||||
`openclaw@YYYY.M.D-beta.N` or
|
||||
`openclaw@YYYY.M.PATCH-beta.N` or
|
||||
`openclaw@beta` package. If a pushed or published prerelease needs a fix,
|
||||
cut the next matching prerelease number; do not delete or rewrite the old
|
||||
prerelease.
|
||||
@@ -172,7 +183,7 @@ vYYYY.M.D-beta.N` from the matching `release/YYYY.M.D` branch. The helper runs
|
||||
`evidence_package_spec` when the release evidence report should prove that the
|
||||
validation matches a published npm package without forcing Telegram E2E.
|
||||
Example:
|
||||
`gh workflow run full-release-validation.yml --ref main -f ref=release/YYYY.M.D`
|
||||
`gh workflow run full-release-validation.yml --ref main -f ref=release/YYYY.M.PATCH`
|
||||
- Run the manual `Package Acceptance` workflow when you want side-channel proof
|
||||
for a package candidate while release work continues. Use `source=npm` for
|
||||
`openclaw@beta`, `openclaw@latest`, or an exact release version; `source=ref`
|
||||
@@ -204,7 +215,7 @@ vYYYY.M.D-beta.N` from the matching `release/YYYY.M.D` branch. The helper runs
|
||||
channel contract shards, Node 22 compatibility, `check-*`, `check-additional-*`,
|
||||
built-artifact smoke checks, docs checks, Python skills, Windows, macOS,
|
||||
Android, and Control UI i18n lanes.
|
||||
Example: `gh workflow run ci.yml --ref release/YYYY.M.D`
|
||||
Example: `gh workflow run ci.yml --ref release/YYYY.M.PATCH`
|
||||
- Run `pnpm qa:otel:smoke` when validating release telemetry. It exercises
|
||||
QA-lab through a local OTLP/HTTP receiver and verifies trace, metric, and log
|
||||
export plus bounded trace attributes and content/identifier redaction without
|
||||
@@ -231,7 +242,7 @@ vYYYY.M.D-beta.N` from the matching `release/YYYY.M.D` branch. The helper runs
|
||||
publish path reuses that preflight artifact, then attaches the same evidence
|
||||
to the GitHub release as `openclaw-<version>-dependency-evidence.zip`.
|
||||
- Run `OpenClaw Release Publish` for the mutating publish sequence after the
|
||||
tag exists. Dispatch it from `release/YYYY.M.D` (or `main` when publishing a
|
||||
tag exists. Dispatch it from `release/YYYY.M.PATCH` (or `main` when publishing a
|
||||
main-reachable tag), pass the release tag and successful OpenClaw npm
|
||||
`preflight_run_id`, and keep the default plugin publish scope
|
||||
`all-publishable` unless you are deliberately running a focused repair. The
|
||||
@@ -280,17 +291,17 @@ Validation` or from the `main`/release workflow ref so workflow logic and
|
||||
using both `OPENAI_API_KEY` and `ANTHROPIC_API_KEY` workflow secrets
|
||||
- npm release preflight no longer waits on the separate release checks lane
|
||||
- Before tagging a release candidate locally, run
|
||||
`RELEASE_TAG=vYYYY.M.D-beta.N pnpm release:fast-pretag-check`. The helper
|
||||
`RELEASE_TAG=vYYYY.M.PATCH-beta.N pnpm release:fast-pretag-check`. The helper
|
||||
runs the fast release guardrails, plugin npm/ClawHub release checks, build,
|
||||
UI build, and `release:openclaw:npm:check` in the order that catches common
|
||||
approval-blocking mistakes before the GitHub publish workflow starts.
|
||||
- Run `RELEASE_TAG=vYYYY.M.D node --import tsx scripts/openclaw-npm-release-check.ts`
|
||||
- Run `RELEASE_TAG=vYYYY.M.PATCH node --import tsx scripts/openclaw-npm-release-check.ts`
|
||||
(or the matching beta/correction tag) before approval
|
||||
- After npm publish, run
|
||||
`node --import tsx scripts/openclaw-npm-postpublish-verify.ts YYYY.M.D`
|
||||
`node --import tsx scripts/openclaw-npm-postpublish-verify.ts YYYY.M.PATCH`
|
||||
(or the matching beta/correction version) to verify the published registry
|
||||
install path in a fresh temp prefix
|
||||
- After a beta publish, run `OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC=openclaw@YYYY.M.D-beta.N OPENCLAW_NPM_TELEGRAM_CREDENTIAL_SOURCE=convex OPENCLAW_NPM_TELEGRAM_CREDENTIAL_ROLE=ci pnpm test:docker:npm-telegram-live`
|
||||
- After a beta publish, run `OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC=openclaw@YYYY.M.PATCH-beta.N OPENCLAW_NPM_TELEGRAM_CREDENTIAL_SOURCE=convex OPENCLAW_NPM_TELEGRAM_CREDENTIAL_ROLE=ci pnpm test:docker:npm-telegram-live`
|
||||
to verify installed-package onboarding, Telegram setup, and real Telegram E2E
|
||||
against the published npm package using the shared leased Telegram credential
|
||||
pool. Local maintainer one-offs may omit the Convex vars and pass the three
|
||||
@@ -302,7 +313,7 @@ Validation` or from the `main`/release workflow ref so workflow logic and
|
||||
- Maintainer release automation now uses preflight-then-promote:
|
||||
- real npm publish must pass a successful npm `preflight_run_id`
|
||||
- the real npm publish must be dispatched from the same `main` or
|
||||
`release/YYYY.M.D` branch as the successful preflight run
|
||||
`release/YYYY.M.PATCH` branch as the successful preflight run
|
||||
- stable npm releases default to `beta`
|
||||
- stable npm publish can target `latest` explicitly via workflow input
|
||||
- token-based npm dist-tag mutation now lives in
|
||||
@@ -311,13 +322,13 @@ Validation` or from the `main`/release workflow ref so workflow logic and
|
||||
OIDC-only publish
|
||||
- public `macOS Release` is validation-only; when a tag lives only on a
|
||||
release branch but the workflow is dispatched from `main`, set
|
||||
`public_release_branch=release/YYYY.M.D`
|
||||
`public_release_branch=release/YYYY.M.PATCH`
|
||||
- real macOS publish must pass successful macOS `preflight_run_id` and
|
||||
`validate_run_id`
|
||||
- the real publish paths promote prepared artifacts instead of rebuilding
|
||||
them again
|
||||
- For stable correction releases like `YYYY.M.D-N`, the post-publish verifier
|
||||
also checks the same temp-prefix upgrade path from `YYYY.M.D` to `YYYY.M.D-N`
|
||||
- For stable correction releases like `YYYY.M.PATCH-N`, the post-publish verifier
|
||||
also checks the same temp-prefix upgrade path from `YYYY.M.PATCH` to `YYYY.M.PATCH-N`
|
||||
so release corrections cannot silently leave older global installs on the
|
||||
base stable payload
|
||||
- npm release preflight fails closed unless the tarball includes both
|
||||
@@ -366,11 +377,11 @@ ref and pass the release branch or tag as `ref`:
|
||||
```bash
|
||||
gh workflow run full-release-validation.yml \
|
||||
--ref main \
|
||||
-f ref=release/YYYY.M.D \
|
||||
-f ref=release/YYYY.M.PATCH \
|
||||
-f provider=openai \
|
||||
-f mode=both \
|
||||
-f release_profile=stable \
|
||||
-f evidence_package_spec=openclaw@YYYY.M.D-beta.N
|
||||
-f evidence_package_spec=openclaw@YYYY.M.PATCH-beta.N
|
||||
```
|
||||
|
||||
The workflow resolves the target ref, dispatches manual `CI` with
|
||||
@@ -418,7 +429,7 @@ each baseline sharded into its own Docker runner job. `full` implies
|
||||
ref once as `release-package-under-test` and reuses that artifact in cross-OS,
|
||||
Package Acceptance, and release-path Docker checks when soak runs. This keeps
|
||||
all package-facing boxes on the same bytes and avoids repeated package builds.
|
||||
After a beta is already on npm, set `release_package_spec=openclaw@YYYY.M.D-beta.N`
|
||||
After a beta is already on npm, set `release_package_spec=openclaw@YYYY.M.PATCH-beta.N`
|
||||
so release checks download the shipped package once, extract its build source
|
||||
SHA from `dist/build-info.json`, and reuse that artifact for cross-OS,
|
||||
Package Acceptance, release-path Docker, and package Telegram lanes.
|
||||
@@ -434,7 +445,7 @@ Use these variants depending on release stage:
|
||||
# Validate an unpublished release candidate branch.
|
||||
gh workflow run full-release-validation.yml \
|
||||
--ref main \
|
||||
-f ref=release/YYYY.M.D \
|
||||
-f ref=release/YYYY.M.PATCH \
|
||||
-f provider=openai \
|
||||
-f mode=both \
|
||||
-f release_profile=stable
|
||||
@@ -449,12 +460,12 @@ gh workflow run full-release-validation.yml \
|
||||
# After publishing a beta, add published-package Telegram E2E.
|
||||
gh workflow run full-release-validation.yml \
|
||||
--ref main \
|
||||
-f ref=release/YYYY.M.D \
|
||||
-f ref=release/YYYY.M.PATCH \
|
||||
-f provider=openai \
|
||||
-f mode=both \
|
||||
-f release_profile=full \
|
||||
-f release_package_spec=openclaw@YYYY.M.D-beta.N \
|
||||
-f evidence_package_spec=openclaw@YYYY.M.D-beta.N \
|
||||
-f release_package_spec=openclaw@YYYY.M.PATCH-beta.N \
|
||||
-f evidence_package_spec=openclaw@YYYY.M.PATCH-beta.N \
|
||||
-f npm_telegram_provider_mode=mock-openai
|
||||
```
|
||||
|
||||
@@ -502,7 +513,7 @@ Run manual CI directly only when the release needs deterministic normal CI but
|
||||
not the Docker, QA Lab, live, cross-OS, or package boxes:
|
||||
|
||||
```bash
|
||||
gh workflow run ci.yml --ref main -f target_ref=release/YYYY.M.D
|
||||
gh workflow run ci.yml --ref main -f target_ref=release/YYYY.M.PATCH
|
||||
```
|
||||
|
||||
### Docker
|
||||
@@ -673,8 +684,8 @@ Beta publish example:
|
||||
|
||||
```bash
|
||||
gh workflow run openclaw-release-publish.yml \
|
||||
--ref release/YYYY.M.D \
|
||||
-f tag=vYYYY.M.D-beta.N \
|
||||
--ref release/YYYY.M.PATCH \
|
||||
-f tag=vYYYY.M.PATCH-beta.N \
|
||||
-f preflight_run_id=<successful-openclaw-npm-preflight-run-id> \
|
||||
-f npm_dist_tag=beta
|
||||
```
|
||||
@@ -683,8 +694,8 @@ Stable publish to the default beta dist-tag:
|
||||
|
||||
```bash
|
||||
gh workflow run openclaw-release-publish.yml \
|
||||
--ref release/YYYY.M.D \
|
||||
-f tag=vYYYY.M.D \
|
||||
--ref release/YYYY.M.PATCH \
|
||||
-f tag=vYYYY.M.PATCH \
|
||||
-f preflight_run_id=<successful-openclaw-npm-preflight-run-id> \
|
||||
-f npm_dist_tag=beta
|
||||
```
|
||||
@@ -693,8 +704,8 @@ Stable promotion directly to `latest` is explicit:
|
||||
|
||||
```bash
|
||||
gh workflow run openclaw-release-publish.yml \
|
||||
--ref release/YYYY.M.D \
|
||||
-f tag=vYYYY.M.D \
|
||||
--ref release/YYYY.M.PATCH \
|
||||
-f tag=vYYYY.M.PATCH \
|
||||
-f preflight_run_id=<successful-openclaw-npm-preflight-run-id> \
|
||||
-f npm_dist_tag=latest
|
||||
```
|
||||
|
||||
@@ -17,7 +17,7 @@ tag, or full commit SHA as `ref`:
|
||||
```bash
|
||||
gh workflow run full-release-validation.yml \
|
||||
--ref main \
|
||||
-f ref=release/YYYY.M.D \
|
||||
-f ref=release/YYYY.M.PATCH \
|
||||
-f provider=openai \
|
||||
-f mode=both \
|
||||
-f release_profile=stable
|
||||
@@ -34,7 +34,7 @@ the broad advisory profile never drops coverage silently.
|
||||
|
||||
Package Acceptance normally builds the candidate tarball from the resolved
|
||||
`ref`, including full-SHA runs dispatched with `pnpm ci:full-release`. After a
|
||||
beta publish, pass `release_package_spec=openclaw@YYYY.M.D-beta.N` to reuse the
|
||||
beta publish, pass `release_package_spec=openclaw@YYYY.M.PATCH-beta.N` to reuse the
|
||||
shipped npm package across release checks, Package Acceptance, cross-OS,
|
||||
release-path Docker, and package Telegram. Use `package_acceptance_package_spec`
|
||||
only when Package Acceptance should intentionally prove a different package.
|
||||
|
||||
@@ -25,7 +25,7 @@ export type PinIosVersionResult = {
|
||||
|
||||
function usage(): string {
|
||||
return [
|
||||
"Usage: node --import tsx scripts/ios-pin-version.ts (--from-gateway | --version <YYYY.M.D>) [--no-sync] [--root dir]",
|
||||
"Usage: node --import tsx scripts/ios-pin-version.ts (--from-gateway | --version <YYYY.M.PATCH>) [--no-sync] [--root dir]",
|
||||
"",
|
||||
"Examples:",
|
||||
" node --import tsx scripts/ios-pin-version.ts --from-gateway",
|
||||
@@ -76,7 +76,7 @@ export function parseArgs(argv: string[]): CliOptions {
|
||||
}
|
||||
|
||||
if (fromGateway === (explicitVersion !== null)) {
|
||||
throw new Error("Choose exactly one of --from-gateway or --version <YYYY.M.D>.");
|
||||
throw new Error("Choose exactly one of --from-gateway or --version <YYYY.M.PATCH>.");
|
||||
}
|
||||
|
||||
if (explicitVersion !== null && !explicitVersion.trim()) {
|
||||
|
||||
@@ -103,7 +103,7 @@ export function normalizeUpgradeSurvivorBaselineSpec(raw) {
|
||||
throw new Error(
|
||||
`invalid published upgrade survivor baseline: ${JSON.stringify(
|
||||
value,
|
||||
)}. Expected openclaw@latest, openclaw@beta, openclaw@alpha, or openclaw@YYYY.M.D.`,
|
||||
)}. Expected openclaw@latest, openclaw@beta, openclaw@alpha, or openclaw@YYYY.M.PATCH.`,
|
||||
);
|
||||
}
|
||||
return spec;
|
||||
@@ -163,12 +163,12 @@ function parsePublishedReleaseVersion(spec) {
|
||||
return {
|
||||
year: Number(match[1]),
|
||||
month: Number(match[2]),
|
||||
day: Number(match[3]),
|
||||
patch: Number(match[3]),
|
||||
};
|
||||
}
|
||||
|
||||
function comparePublishedReleaseVersion(a, b) {
|
||||
return a.year - b.year || a.month - b.month || a.day - b.day;
|
||||
return a.year - b.year || a.month - b.month || a.patch - b.patch;
|
||||
}
|
||||
|
||||
function supportsUpgradeSurvivorPluginDependencyCleanup(baselineSpec) {
|
||||
@@ -179,7 +179,7 @@ function supportsUpgradeSurvivorPluginDependencyCleanup(baselineSpec) {
|
||||
if (!version) {
|
||||
return true;
|
||||
}
|
||||
return comparePublishedReleaseVersion(version, { year: 2026, month: 4, day: 23 }) >= 0;
|
||||
return comparePublishedReleaseVersion(version, { year: 2026, month: 4, patch: 23 }) >= 0;
|
||||
}
|
||||
|
||||
function expandUpgradeSurvivorBaselineLanes(poolLanes, rawBaselineSpecs, rawScenarios = "") {
|
||||
|
||||
@@ -7,8 +7,8 @@ const IOS_CHANGELOG_FILE = "apps/ios/CHANGELOG.md";
|
||||
const IOS_VERSION_XCCONFIG_FILE = "apps/ios/Config/Version.xcconfig";
|
||||
const IOS_RELEASE_NOTES_FILE = "apps/ios/fastlane/metadata/en-US/release_notes.txt";
|
||||
|
||||
const PINNED_IOS_VERSION_PATTERN = /^(\d{4}\.\d{1,2}\.\d{1,2})$/u;
|
||||
const GATEWAY_VERSION_PATTERN = /^(\d{4}\.\d{1,2}\.\d{1,2})(?:-(?:alpha\.\d+|beta\.\d+|\d+))?$/u;
|
||||
const PINNED_IOS_VERSION_PATTERN = /^(\d{4}\.\d{1,2}\.[1-9]\d*)$/u;
|
||||
const GATEWAY_VERSION_PATTERN = /^(\d{4}\.\d{1,2}\.[1-9]\d*)(?:-(?:alpha\.\d+|beta\.\d+|\d+))?$/u;
|
||||
|
||||
type IosVersionManifest = {
|
||||
version: string;
|
||||
@@ -38,7 +38,9 @@ export function normalizePinnedIosVersion(rawVersion: string): string {
|
||||
|
||||
const match = PINNED_IOS_VERSION_PATTERN.exec(trimmed);
|
||||
if (!match) {
|
||||
throw new Error(`Invalid iOS version '${rawVersion}'. Expected pinned CalVer like 2026.4.6.`);
|
||||
throw new Error(
|
||||
`Invalid iOS version '${rawVersion}'. Expected pinned release version like 2026.6.5.`,
|
||||
);
|
||||
}
|
||||
|
||||
return match[1] ?? trimmed;
|
||||
@@ -53,7 +55,7 @@ export function normalizeGatewayVersionToPinnedIosVersion(rawVersion: string): s
|
||||
const match = GATEWAY_VERSION_PATTERN.exec(trimmed);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Invalid gateway version '${rawVersion}'. Expected YYYY.M.D, YYYY.M.D-alpha.N, YYYY.M.D-beta.N, or YYYY.M.D-N.`,
|
||||
`Invalid gateway version '${rawVersion}'. Expected YYYY.M.PATCH, YYYY.M.PATCH-alpha.N, YYYY.M.PATCH-beta.N, or YYYY.M.PATCH-N.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// Parses OpenClaw date-based release versions and npm dist-tag publish plans.
|
||||
const STABLE_VERSION_REGEX = /^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)$/;
|
||||
// Parses OpenClaw monthly patch release versions and npm dist-tag publish plans.
|
||||
const STABLE_VERSION_REGEX = /^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<patch>[1-9]\d*)$/;
|
||||
const ALPHA_VERSION_REGEX =
|
||||
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)-alpha\.(?<alpha>[1-9]\d*)$/;
|
||||
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<patch>[1-9]\d*)-alpha\.(?<alpha>[1-9]\d*)$/;
|
||||
const BETA_VERSION_REGEX =
|
||||
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)-beta\.(?<beta>[1-9]\d*)$/;
|
||||
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<patch>[1-9]\d*)-beta\.(?<beta>[1-9]\d*)$/;
|
||||
const CORRECTION_VERSION_REGEX =
|
||||
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)-(?<correction>[1-9]\d*)$/;
|
||||
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<patch>[1-9]\d*)-(?<correction>[1-9]\d*)$/;
|
||||
export const JUNE_2026_PATCH_FLOOR = 5;
|
||||
|
||||
/**
|
||||
* @typedef {object} ParsedReleaseVersion
|
||||
@@ -14,11 +15,10 @@ const CORRECTION_VERSION_REGEX =
|
||||
* @property {"stable" | "alpha" | "beta"} channel
|
||||
* @property {number} year
|
||||
* @property {number} month
|
||||
* @property {number} day
|
||||
* @property {number} patch
|
||||
* @property {number | undefined} [alphaNumber]
|
||||
* @property {number | undefined} [betaNumber]
|
||||
* @property {number | undefined} [correctionNumber]
|
||||
* @property {Date} date
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -44,21 +44,20 @@ const CORRECTION_VERSION_REGEX =
|
||||
* @param {"stable" | "alpha" | "beta"} channel
|
||||
* @returns {ParsedReleaseVersion | null}
|
||||
*/
|
||||
function parseDateParts(version, groups, channel) {
|
||||
function parseVersionParts(version, groups, channel) {
|
||||
const year = Number.parseInt(groups.year ?? "", 10);
|
||||
const month = Number.parseInt(groups.month ?? "", 10);
|
||||
const day = Number.parseInt(groups.day ?? "", 10);
|
||||
const patch = Number.parseInt(groups.patch ?? "", 10);
|
||||
const alphaNumber = channel === "alpha" ? Number.parseInt(groups.alpha ?? "", 10) : undefined;
|
||||
const betaNumber = channel === "beta" ? Number.parseInt(groups.beta ?? "", 10) : undefined;
|
||||
|
||||
if (
|
||||
!Number.isInteger(year) ||
|
||||
!Number.isInteger(month) ||
|
||||
!Number.isInteger(day) ||
|
||||
!Number.isInteger(patch) ||
|
||||
month < 1 ||
|
||||
month > 12 ||
|
||||
day < 1 ||
|
||||
day > 31
|
||||
patch < 1
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -69,25 +68,15 @@ function parseDateParts(version, groups, channel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const date = new Date(Date.UTC(year, month - 1, day));
|
||||
if (
|
||||
date.getUTCFullYear() !== year ||
|
||||
date.getUTCMonth() !== month - 1 ||
|
||||
date.getUTCDate() !== day
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
baseVersion: `${year}.${month}.${day}`,
|
||||
baseVersion: `${year}.${month}.${patch}`,
|
||||
channel,
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
patch,
|
||||
alphaNumber,
|
||||
betaNumber,
|
||||
date,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -103,22 +92,22 @@ export function parseReleaseVersion(version) {
|
||||
|
||||
const stableMatch = STABLE_VERSION_REGEX.exec(trimmed);
|
||||
if (stableMatch?.groups) {
|
||||
return parseDateParts(trimmed, stableMatch.groups, "stable");
|
||||
return parseVersionParts(trimmed, stableMatch.groups, "stable");
|
||||
}
|
||||
|
||||
const alphaMatch = ALPHA_VERSION_REGEX.exec(trimmed);
|
||||
if (alphaMatch?.groups) {
|
||||
return parseDateParts(trimmed, alphaMatch.groups, "alpha");
|
||||
return parseVersionParts(trimmed, alphaMatch.groups, "alpha");
|
||||
}
|
||||
|
||||
const betaMatch = BETA_VERSION_REGEX.exec(trimmed);
|
||||
if (betaMatch?.groups) {
|
||||
return parseDateParts(trimmed, betaMatch.groups, "beta");
|
||||
return parseVersionParts(trimmed, betaMatch.groups, "beta");
|
||||
}
|
||||
|
||||
const correctionMatch = CORRECTION_VERSION_REGEX.exec(trimmed);
|
||||
if (correctionMatch?.groups) {
|
||||
const parsedCorrection = parseDateParts(trimmed, correctionMatch.groups, "stable");
|
||||
const parsedCorrection = parseVersionParts(trimmed, correctionMatch.groups, "stable");
|
||||
const correctionNumber = Number.parseInt(correctionMatch.groups.correction ?? "", 10);
|
||||
if (parsedCorrection === null || !Number.isInteger(correctionNumber) || correctionNumber < 1) {
|
||||
return null;
|
||||
@@ -133,6 +122,29 @@ export function parseReleaseVersion(version) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | ParsedReleaseVersion | null} version
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function collectReleaseVersionFloorErrors(version) {
|
||||
const parsedVersion =
|
||||
typeof version === "string" ? parseReleaseVersion(version) : (version ?? null);
|
||||
if (parsedVersion === null) {
|
||||
return [];
|
||||
}
|
||||
if (
|
||||
parsedVersion.year === 2026 &&
|
||||
parsedVersion.month === 6 &&
|
||||
parsedVersion.patch < JUNE_2026_PATCH_FLOOR &&
|
||||
parsedVersion.channel !== "alpha"
|
||||
) {
|
||||
return [
|
||||
`June 2026 stable and beta release trains must use patch ${JUNE_2026_PATCH_FLOOR} or higher because 2026.6.5-beta.1 is already published; found "${parsedVersion.version}".`,
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} left
|
||||
* @param {string} right
|
||||
@@ -145,9 +157,14 @@ export function compareReleaseVersions(left, right) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dateDelta = parsedLeft.date.getTime() - parsedRight.date.getTime();
|
||||
if (dateDelta !== 0) {
|
||||
return Math.sign(dateDelta);
|
||||
if (parsedLeft.year !== parsedRight.year) {
|
||||
return Math.sign(parsedLeft.year - parsedRight.year);
|
||||
}
|
||||
if (parsedLeft.month !== parsedRight.month) {
|
||||
return Math.sign(parsedLeft.month - parsedRight.month);
|
||||
}
|
||||
if (parsedLeft.patch !== parsedRight.patch) {
|
||||
return Math.sign(parsedLeft.patch - parsedRight.patch);
|
||||
}
|
||||
|
||||
if (parsedLeft.channel !== parsedRight.channel) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
collectChangedPathsFromGitRange,
|
||||
collectChangedExtensionIdsFromPaths,
|
||||
collectPublishablePluginPackageErrors,
|
||||
assertPluginReleaseVersionFloors,
|
||||
parsePluginReleaseArgs,
|
||||
resolvePublishablePluginVersion,
|
||||
resolveGitCommitSha,
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
type PluginReleaseSelectionMode,
|
||||
} from "./plugin-npm-release.ts";
|
||||
|
||||
export { parsePluginReleaseArgs };
|
||||
export { assertPluginReleaseVersionFloors, parsePluginReleaseArgs };
|
||||
|
||||
type PluginPackageJson = {
|
||||
name?: string;
|
||||
@@ -453,6 +454,11 @@ export async function collectPluginClawHubReleasePlan(params?: {
|
||||
rootDir,
|
||||
});
|
||||
|
||||
const explicitPublishSelection = params?.selectionMode !== undefined || selection.length > 0;
|
||||
if (explicitPublishSelection) {
|
||||
assertPluginReleaseVersionFloors(selectedPublishable, "Plugin ClawHub release plan");
|
||||
}
|
||||
|
||||
const all = await Promise.all(
|
||||
selectedPublishable.map(async (plugin) =>
|
||||
Object.assign({}, plugin, {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { join, resolve } from "node:path";
|
||||
import { normalizeOptionalString } from "../../packages/normalization-core/src/string-coerce.js";
|
||||
import { validateExternalCodePluginPackageJson } from "../../packages/plugin-package-contract/src/index.ts";
|
||||
import { parseReleaseVersion } from "../openclaw-npm-release-check.ts";
|
||||
import { resolveNpmPublishPlan } from "./npm-publish-plan.mjs";
|
||||
import { collectReleaseVersionFloorErrors, resolveNpmPublishPlan } from "./npm-publish-plan.mjs";
|
||||
|
||||
export type PluginPackageJson = {
|
||||
name?: string;
|
||||
@@ -124,7 +124,7 @@ export function resolvePublishablePluginVersion(params: {
|
||||
const parsedVersion = parseReleaseVersion(version);
|
||||
if (parsedVersion === null) {
|
||||
params.validationErrors.push(
|
||||
`${params.extensionId}: package.json version must match YYYY.M.D, YYYY.M.D-N, YYYY.M.D-alpha.N, or YYYY.M.D-beta.N; found "${version}".`,
|
||||
`${params.extensionId}: package.json version must match YYYY.M.PATCH, YYYY.M.PATCH-N, YYYY.M.PATCH-alpha.N, or YYYY.M.PATCH-beta.N; found "${version}".`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
@@ -251,7 +251,7 @@ export function collectPublishablePluginPackageErrors(
|
||||
errors.push("package.json version must be non-empty.");
|
||||
} else if (parseReleaseVersion(packageVersion) === null) {
|
||||
errors.push(
|
||||
`package.json version must match YYYY.M.D, YYYY.M.D-N, YYYY.M.D-alpha.N, or YYYY.M.D-beta.N; found "${packageVersion}".`,
|
||||
`package.json version must match YYYY.M.PATCH, YYYY.M.PATCH-N, YYYY.M.PATCH-alpha.N, or YYYY.M.PATCH-beta.N; found "${packageVersion}".`,
|
||||
);
|
||||
}
|
||||
if (!Array.isArray(extensions) || extensions.length === 0) {
|
||||
@@ -465,6 +465,31 @@ export function resolveChangedPublishablePluginPackages(params: {
|
||||
return params.plugins.filter((plugin) => changed.has(plugin.extensionId));
|
||||
}
|
||||
|
||||
export function collectPluginReleaseVersionFloorErrors(
|
||||
plugins: readonly Pick<PublishablePluginPackage, "packageName" | "version">[],
|
||||
): string[] {
|
||||
return plugins.flatMap((plugin) =>
|
||||
collectReleaseVersionFloorErrors(plugin.version).map(
|
||||
(error) => `${plugin.packageName}@${plugin.version}: ${error}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function assertPluginReleaseVersionFloors(
|
||||
plugins: readonly Pick<PublishablePluginPackage, "packageName" | "version">[],
|
||||
label: string,
|
||||
): void {
|
||||
const errors = collectPluginReleaseVersionFloorErrors(plugins);
|
||||
if (errors.length === 0) {
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
`${label} rejected plugin versions below the release floor:\n${errors
|
||||
.map((error) => `- ${error}`)
|
||||
.join("\n")}`,
|
||||
);
|
||||
}
|
||||
|
||||
function isPluginVersionPublished(packageName: string, version: string): boolean {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "openclaw-plugin-npm-view-"));
|
||||
const userconfigPath = join(tempDir, "npmrc");
|
||||
@@ -521,6 +546,12 @@ export function collectPluginReleasePlan(params?: {
|
||||
})
|
||||
: allPublishable;
|
||||
|
||||
const explicitPublishSelection =
|
||||
params?.selectionMode !== undefined || (params?.selection?.length ?? 0) > 0;
|
||||
if (explicitPublishSelection) {
|
||||
assertPluginReleaseVersionFloors(selectedPublishable, "Plugin NPM release plan");
|
||||
}
|
||||
|
||||
const all = selectedPublishable.map((plugin) =>
|
||||
Object.assign({}, plugin, {
|
||||
alreadyPublished: isPluginVersionPublished(plugin.packageName, plugin.version),
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "../src/infra/package-dist-inventory.ts";
|
||||
import {
|
||||
compareReleaseVersions as compareReleaseVersionsBase,
|
||||
collectReleaseVersionFloorErrors as collectReleaseVersionFloorErrorsBase,
|
||||
resolveNpmDistTagMirrorAuth as resolveNpmDistTagMirrorAuthBase,
|
||||
parseReleaseVersion as parseReleaseVersionBase,
|
||||
} from "./lib/npm-publish-plan.mjs";
|
||||
@@ -37,11 +38,10 @@ export type ParsedReleaseVersion = {
|
||||
channel: "stable" | "alpha" | "beta";
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
patch: number;
|
||||
alphaNumber?: number;
|
||||
betaNumber?: number;
|
||||
correctionNumber?: number;
|
||||
date: Date;
|
||||
};
|
||||
|
||||
export type ParsedReleaseTag = {
|
||||
@@ -50,7 +50,6 @@ export type ParsedReleaseTag = {
|
||||
baseVersion: string;
|
||||
channel: "stable" | "alpha" | "beta";
|
||||
correctionNumber?: number;
|
||||
date: Date;
|
||||
};
|
||||
|
||||
export type NpmPublishPlan = {
|
||||
@@ -66,7 +65,6 @@ export type NpmDistTagMirrorAuth = {
|
||||
const EXPECTED_REPOSITORY_URL = "https://github.com/openclaw/openclaw";
|
||||
const OPTIONAL_LOCAL_EMBEDDING_RUNTIME_PACKAGE = "node-llama-cpp";
|
||||
const FS_SAFE_PACKAGE = "@openclaw/fs-safe";
|
||||
const MAX_CALVER_DISTANCE_DAYS = 2;
|
||||
const REQUIRED_PACKED_PATHS = [
|
||||
"npm-shrinkwrap.json",
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
@@ -286,7 +284,6 @@ export function parseReleaseTagVersion(version: string): ParsedReleaseTag | null
|
||||
packageVersion: parsedVersion.version,
|
||||
baseVersion: parsedVersion.baseVersion,
|
||||
channel: parsedVersion.channel,
|
||||
date: parsedVersion.date,
|
||||
correctionNumber: parsedVersion.correctionNumber,
|
||||
};
|
||||
}
|
||||
@@ -294,14 +291,6 @@ export function parseReleaseTagVersion(version: string): ParsedReleaseTag | null
|
||||
return null;
|
||||
}
|
||||
|
||||
function startOfUtcDay(date: Date): number {
|
||||
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
||||
}
|
||||
|
||||
export function utcCalendarDayDistance(left: Date, right: Date): number {
|
||||
return Math.round(Math.abs(startOfUtcDay(left) - startOfUtcDay(right)) / 86_400_000);
|
||||
}
|
||||
|
||||
function positiveEnvInt(name: string, env: NodeJS.ProcessEnv, fallback: number): number {
|
||||
const raw = env[name]?.trim();
|
||||
if (raw === undefined || raw === "") {
|
||||
@@ -416,18 +405,18 @@ export function collectReleaseTagErrors(params: {
|
||||
releaseTag: string;
|
||||
releaseSha?: string;
|
||||
releaseMainRef?: string;
|
||||
now?: Date;
|
||||
}): string[] {
|
||||
const errors: string[] = [];
|
||||
const releaseTag = params.releaseTag.trim();
|
||||
const packageVersion = params.packageVersion.trim();
|
||||
const now = params.now ?? new Date();
|
||||
|
||||
const parsedVersion = parseReleaseVersion(packageVersion);
|
||||
if (parsedVersion === null) {
|
||||
errors.push(
|
||||
`package.json version must match YYYY.M.D, YYYY.M.D-N, YYYY.M.D-alpha.N, or YYYY.M.D-beta.N; found "${packageVersion || "<missing>"}".`,
|
||||
`package.json version must match YYYY.M.PATCH, YYYY.M.PATCH-N, YYYY.M.PATCH-alpha.N, or YYYY.M.PATCH-beta.N; found "${packageVersion || "<missing>"}".`,
|
||||
);
|
||||
} else {
|
||||
errors.push(...collectReleaseVersionFloorErrorsBase(parsedVersion));
|
||||
}
|
||||
|
||||
if (!releaseTag.startsWith("v")) {
|
||||
@@ -438,7 +427,7 @@ export function collectReleaseTagErrors(params: {
|
||||
const parsedTag = parseReleaseTagVersion(tagVersion);
|
||||
if (parsedTag === null) {
|
||||
errors.push(
|
||||
`Release tag must match vYYYY.M.D, vYYYY.M.D-alpha.N, vYYYY.M.D-beta.N, or fallback correction tag vYYYY.M.D-N; found "${releaseTag || "<missing>"}".`,
|
||||
`Release tag must match vYYYY.M.PATCH, vYYYY.M.PATCH-alpha.N, vYYYY.M.PATCH-beta.N, or fallback correction tag vYYYY.M.PATCH-N; found "${releaseTag || "<missing>"}".`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -464,17 +453,6 @@ export function collectReleaseTagErrors(params: {
|
||||
);
|
||||
}
|
||||
|
||||
if (parsedVersion !== null) {
|
||||
const dayDistance = utcCalendarDayDistance(parsedVersion.date, now);
|
||||
if (dayDistance > MAX_CALVER_DISTANCE_DAYS) {
|
||||
const nowLabel = now.toISOString().slice(0, 10);
|
||||
const versionDate = parsedVersion.date.toISOString().slice(0, 10);
|
||||
errors.push(
|
||||
`Release version ${packageVersion} is ${dayDistance} days away from current UTC date ${nowLabel}; release CalVer date ${versionDate} must be within ${MAX_CALVER_DISTANCE_DAYS} days.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (params.releaseSha?.trim() && params.releaseMainRef?.trim()) {
|
||||
try {
|
||||
runNpmReleaseCheckCommand(
|
||||
@@ -770,7 +748,6 @@ export function collectPackedTestCargoErrors(paths: Iterable<string>): string[]
|
||||
|
||||
async function main(): Promise<number> {
|
||||
const pkg = loadPackageJson();
|
||||
const now = new Date();
|
||||
const skipPackValidation = shouldSkipPackedTarballValidation();
|
||||
const metadataErrors = collectReleasePackageMetadataErrors(pkg);
|
||||
const tagErrors = collectReleaseTagErrors({
|
||||
@@ -778,7 +755,6 @@ async function main(): Promise<number> {
|
||||
releaseTag: process.env.RELEASE_TAG ?? "",
|
||||
releaseSha: process.env.RELEASE_SHA,
|
||||
releaseMainRef: process.env.RELEASE_MAIN_REF,
|
||||
now,
|
||||
});
|
||||
if (!skipPackValidation) {
|
||||
await writePackageDistInventory(process.cwd());
|
||||
@@ -796,10 +772,8 @@ async function main(): Promise<number> {
|
||||
|
||||
const parsedVersion = parseReleaseVersion(pkg.version ?? "");
|
||||
const channel = parsedVersion?.channel ?? "unknown";
|
||||
const dayDistance =
|
||||
parsedVersion === null ? "unknown" : String(utcCalendarDayDistance(parsedVersion.date, now));
|
||||
console.log(
|
||||
`openclaw-npm-release-check: validated ${channel} release ${pkg.version} (${dayDistance} day UTC delta${skipPackValidation ? "; metadata-only" : ""}).`,
|
||||
`openclaw-npm-release-check: validated ${channel} release ${pkg.version} (monthly patch version${skipPackValidation ? "; metadata-only" : ""}).`,
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url";
|
||||
import {
|
||||
collectClawHubPublishablePluginPackages,
|
||||
collectClawHubVersionGateErrors,
|
||||
assertPluginReleaseVersionFloors,
|
||||
parsePluginReleaseArgs,
|
||||
resolveSelectedClawHubPublishablePluginPackages,
|
||||
} from "./lib/plugin-clawhub-release.ts";
|
||||
@@ -23,6 +24,10 @@ export async function runPluginClawHubReleaseCheck(argv: string[]) {
|
||||
gitRange,
|
||||
});
|
||||
|
||||
if (selectionMode !== undefined || selection.length > 0) {
|
||||
assertPluginReleaseVersionFloors(selected, "plugin-clawhub-release-check");
|
||||
}
|
||||
|
||||
if (gitRange) {
|
||||
const errors = collectClawHubVersionGateErrors({
|
||||
plugins: publishable,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url";
|
||||
import {
|
||||
collectChangedExtensionIdsFromGitRange,
|
||||
collectPublishablePluginPackages,
|
||||
assertPluginReleaseVersionFloors,
|
||||
parsePluginReleaseArgs,
|
||||
resolveChangedPublishablePluginPackages,
|
||||
resolveSelectedPublishablePluginPackages,
|
||||
@@ -40,6 +41,10 @@ export function runPluginNpmReleaseCheck(argv: string[]) {
|
||||
})
|
||||
: publishable;
|
||||
|
||||
if (selectionMode !== undefined || selection.length > 0) {
|
||||
assertPluginReleaseVersionFloors(selected, "plugin-npm-release-check");
|
||||
}
|
||||
|
||||
console.log("plugin-npm-release-check: publishable plugin metadata looks OK.");
|
||||
if (baseRef && headRef && selected.length === 0) {
|
||||
console.log(
|
||||
|
||||
@@ -17,7 +17,7 @@ const DEFAULT_TELEGRAM_PROVIDER_MODE = "mock-openai";
|
||||
const DEFAULT_GITHUB_API_TIMEOUT_MS = 30_000;
|
||||
|
||||
function usage() {
|
||||
return `Usage: pnpm release:candidate -- --tag vYYYY.M.D-beta.N [options]
|
||||
return `Usage: pnpm release:candidate -- --tag vYYYY.M.PATCH-beta.N [options]
|
||||
|
||||
Dispatches or consumes release validation runs, validates the prepared npm tarball,
|
||||
builds plugin publish plans, writes a green evidence bundle, then prints the exact
|
||||
|
||||
@@ -142,7 +142,7 @@ const forbiddenPrivateQaContentScanPrefixes = ["dist/"] as const;
|
||||
const forbiddenPluginSdkRootAliasMinifiedExportPattern = /\bmod\.[A-Za-z_$]\b/u;
|
||||
const appcastPath = resolve("appcast.xml");
|
||||
const laneBuildMin = 1_000_000_000;
|
||||
const laneFloorAdoptionDateKey = 20260227;
|
||||
const laneFloorAdoptionReleaseKey = 20260227;
|
||||
const SAFE_UNIX_SMOKE_PATH = "/usr/bin:/bin";
|
||||
const DEFAULT_RELEASE_CHECK_COMMAND_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
const DEFAULT_RELEASE_CHECK_COMMAND_MAX_BUFFER_BYTES = 100 * 1024 * 1024;
|
||||
@@ -987,18 +987,19 @@ export function collectAppcastSparkleVersionErrors(xml: string): string[] {
|
||||
calverItems.push({ title, sparkleBuild: Number(sparkleVersion), floors });
|
||||
}
|
||||
|
||||
const observedLaneAdoptionDateKey = calverItems
|
||||
const observedLaneAdoptionReleaseKey = calverItems
|
||||
.filter((item) => item.sparkleBuild >= laneBuildMin)
|
||||
.map((item) => item.floors.dateKey)
|
||||
.map((item) => item.floors.releaseKey)
|
||||
.toSorted((a, b) => a - b)[0];
|
||||
const effectiveLaneAdoptionDateKey =
|
||||
typeof observedLaneAdoptionDateKey === "number"
|
||||
? Math.min(observedLaneAdoptionDateKey, laneFloorAdoptionDateKey)
|
||||
: laneFloorAdoptionDateKey;
|
||||
const effectiveLaneAdoptionReleaseKey =
|
||||
typeof observedLaneAdoptionReleaseKey === "number"
|
||||
? Math.min(observedLaneAdoptionReleaseKey, laneFloorAdoptionReleaseKey)
|
||||
: laneFloorAdoptionReleaseKey;
|
||||
|
||||
for (const item of calverItems) {
|
||||
const expectLaneFloor =
|
||||
item.sparkleBuild >= laneBuildMin || item.floors.dateKey >= effectiveLaneAdoptionDateKey;
|
||||
item.sparkleBuild >= laneBuildMin ||
|
||||
item.floors.releaseKey >= effectiveLaneAdoptionReleaseKey;
|
||||
const floor = expectLaneFloor ? item.floors.laneFloor : item.floors.legacyFloor;
|
||||
if (item.sparkleBuild < floor) {
|
||||
const floorLabel = expectLaneFloor ? "lane floor" : "legacy floor";
|
||||
|
||||
+56
-12
@@ -4,40 +4,70 @@
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
export type SparkleBuildFloors = {
|
||||
dateKey: number;
|
||||
releaseKey: number;
|
||||
legacyFloor: number;
|
||||
laneFloor: number;
|
||||
lane: number;
|
||||
};
|
||||
|
||||
const CALVER_REGEX = /^([0-9]{4})\.([0-9]{1,2})\.([0-9]{1,2})([.-].*)?$/;
|
||||
const RELEASE_VERSION_REGEX = /^([0-9]{4})\.([0-9]{1,2})\.([1-9][0-9]*)([.-].*)?$/;
|
||||
const MONTHLY_PATCH_SPARKLE_BUILD_ADOPTION = {
|
||||
year: 2026,
|
||||
month: 6,
|
||||
patch: 5,
|
||||
};
|
||||
|
||||
function compareReleaseParts(
|
||||
left: { year: number; month: number; patch: number },
|
||||
right: { year: number; month: number; patch: number },
|
||||
): number {
|
||||
if (left.year !== right.year) {
|
||||
return Math.sign(left.year - right.year);
|
||||
}
|
||||
if (left.month !== right.month) {
|
||||
return Math.sign(left.month - right.month);
|
||||
}
|
||||
return Math.sign(left.patch - right.patch);
|
||||
}
|
||||
|
||||
function usesMonthlyPatchSparkleBuild(version: {
|
||||
year: number;
|
||||
month: number;
|
||||
patch: number;
|
||||
}): boolean {
|
||||
return compareReleaseParts(version, MONTHLY_PATCH_SPARKLE_BUILD_ADOPTION) >= 0;
|
||||
}
|
||||
|
||||
function legacyDateReleaseKey(year: number, month: number, patch: number): number {
|
||||
return Number(`${year}${String(month).padStart(2, "0")}${String(patch).padStart(2, "0")}`);
|
||||
}
|
||||
|
||||
function monthlyPatchReleaseKey(year: number, month: number, patch: number): number {
|
||||
return (year - 2000) * 100_000_000 + month * 1_000_000 + patch * 100;
|
||||
}
|
||||
|
||||
export function sparkleBuildFloorsFromShortVersion(
|
||||
shortVersion: string,
|
||||
): SparkleBuildFloors | null {
|
||||
const match = CALVER_REGEX.exec(shortVersion.trim());
|
||||
const match = RELEASE_VERSION_REGEX.exec(shortVersion.trim());
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const year = Number.parseInt(match[1], 10);
|
||||
const month = Number.parseInt(match[2], 10);
|
||||
const day = Number.parseInt(match[3], 10);
|
||||
const patch = Number.parseInt(match[3], 10);
|
||||
if (
|
||||
!Number.isInteger(year) ||
|
||||
!Number.isInteger(month) ||
|
||||
!Number.isInteger(day) ||
|
||||
!Number.isInteger(patch) ||
|
||||
month < 1 ||
|
||||
month > 12 ||
|
||||
day < 1 ||
|
||||
day > 31
|
||||
patch < 1
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dateKey = Number(`${year}${String(month).padStart(2, "0")}${String(day).padStart(2, "0")}`);
|
||||
const legacyFloor = Number(`${dateKey}0`);
|
||||
|
||||
let lane = 90;
|
||||
const suffix = match[4] ?? "";
|
||||
if (suffix.length > 0) {
|
||||
@@ -49,8 +79,22 @@ export function sparkleBuildFloorsFromShortVersion(
|
||||
}
|
||||
}
|
||||
|
||||
const laneFloor = Number(`${dateKey}${String(lane).padStart(2, "0")}`);
|
||||
return { dateKey, legacyFloor, laneFloor, lane };
|
||||
if (usesMonthlyPatchSparkleBuild({ year, month, patch })) {
|
||||
// Keep old appcast entries byte-stable, then switch to YYMMPPPPLL so
|
||||
// monthly patches beyond 31 stay monotonic without pretending to be dates.
|
||||
const releaseKey = monthlyPatchReleaseKey(year, month, patch);
|
||||
return {
|
||||
releaseKey,
|
||||
legacyFloor: releaseKey,
|
||||
laneFloor: releaseKey + lane,
|
||||
lane,
|
||||
};
|
||||
}
|
||||
|
||||
const releaseKey = legacyDateReleaseKey(year, month, patch);
|
||||
const legacyFloor = Number(`${releaseKey}0`);
|
||||
const laneFloor = Number(`${releaseKey}${String(lane).padStart(2, "0")}`);
|
||||
return { releaseKey, legacyFloor, laneFloor, lane };
|
||||
}
|
||||
|
||||
export function canonicalSparkleBuildFromVersion(shortVersion: string): number | null {
|
||||
|
||||
@@ -76,7 +76,7 @@ describe("node host version mismatch guard", () => {
|
||||
});
|
||||
|
||||
test("local node with dev/test version is allowed (not a released version)", async () => {
|
||||
// "dev" does not match YYYY.M.D, so the guard skips
|
||||
// "dev" does not match YYYY.M.PATCH, so the guard skips
|
||||
const client = await connectGatewayClient({
|
||||
url: `ws://127.0.0.1:${port}`,
|
||||
token: "secret",
|
||||
|
||||
@@ -180,7 +180,7 @@ class NodePairingRateLimitError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/** Match production release versions (YYYY.M.D or YYYY.M.D-beta.N). */
|
||||
/** Match production release versions (YYYY.M.PATCH or YYYY.M.PATCH-beta.N). */
|
||||
const RELEASED_VERSION_RE = /^\d{4}\.\d+\.\d+/;
|
||||
|
||||
function isReleasedVersion(version: string): boolean {
|
||||
|
||||
@@ -141,8 +141,9 @@ describe("clawhub helpers", () => {
|
||||
expect(satisfiesPluginApiRange("invalid", "^1.2.0")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats OpenClaw CalVer correction versions as stable plugin API hosts", () => {
|
||||
it("treats OpenClaw release correction versions as stable plugin API hosts", () => {
|
||||
expect(satisfiesPluginApiRange("2026.5.3-1", ">=2026.5.3")).toBe(true);
|
||||
expect(satisfiesPluginApiRange("2026.5.32-1", ">=2026.5.32")).toBe(true);
|
||||
expect(satisfiesPluginApiRange("2026.5.3-2", ">=2026.5.3")).toBe(true);
|
||||
expect(satisfiesPluginApiRange("2026.5.3-beta.1", ">=2026.5.3")).toBe(true);
|
||||
expect(satisfiesPluginApiRange("2026.5.3-alpha.1", ">=2026.5.3")).toBe(true);
|
||||
|
||||
@@ -592,13 +592,13 @@ function shouldPreservePluginApiPrereleaseFloor(target: string): boolean {
|
||||
}
|
||||
|
||||
function normalizePluginApiVersionForComparator(version: string, target: string): string {
|
||||
const normalizedCorrection = normalizeCalVerNumericCorrectionForPluginApi(version);
|
||||
const normalizedCorrection = normalizeOpenClawNumericCorrectionForPluginApi(version);
|
||||
if (normalizedCorrection) {
|
||||
return normalizedCorrection;
|
||||
}
|
||||
return shouldPreservePluginApiPrereleaseFloor(target)
|
||||
? version
|
||||
: normalizeCalVerCorrectionForPluginApi(version);
|
||||
: normalizeOpenClawReleaseSuffixForPluginApi(version);
|
||||
}
|
||||
|
||||
function satisfiesComparator(version: string, token: string): boolean {
|
||||
@@ -656,18 +656,18 @@ function satisfiesSemverRange(version: string, range: string): boolean {
|
||||
return tokens.every((token) => satisfiesComparator(version, token));
|
||||
}
|
||||
|
||||
const OPENCLAW_CALVER_STABLE_CORRECTION_PATTERN =
|
||||
/^[vV]?(\d{4}\.\d{1,2}\.\d{1,2})(?:-\d+|-(?:alpha|beta|rc)\.\d+)$/i;
|
||||
const OPENCLAW_CALVER_NUMERIC_CORRECTION_PATTERN = /^[vV]?(\d{4}\.\d{1,2}\.\d{1,2})-\d+$/;
|
||||
const OPENCLAW_RELEASE_SUFFIX_PATTERN =
|
||||
/^[vV]?(\d{4}\.[1-9]\d?\.[1-9]\d*)(?:-\d+|-(?:alpha|beta|rc)\.\d+)$/i;
|
||||
const OPENCLAW_NUMERIC_CORRECTION_PATTERN = /^[vV]?(\d{4}\.[1-9]\d?\.[1-9]\d*)-\d+$/;
|
||||
|
||||
function normalizeCalVerNumericCorrectionForPluginApi(
|
||||
function normalizeOpenClawNumericCorrectionForPluginApi(
|
||||
pluginApiVersion: string,
|
||||
): string | undefined {
|
||||
return OPENCLAW_CALVER_NUMERIC_CORRECTION_PATTERN.exec(pluginApiVersion.trim())?.[1];
|
||||
return OPENCLAW_NUMERIC_CORRECTION_PATTERN.exec(pluginApiVersion.trim())?.[1];
|
||||
}
|
||||
|
||||
function normalizeCalVerCorrectionForPluginApi(pluginApiVersion: string): string {
|
||||
const match = OPENCLAW_CALVER_STABLE_CORRECTION_PATTERN.exec(pluginApiVersion.trim());
|
||||
function normalizeOpenClawReleaseSuffixForPluginApi(pluginApiVersion: string): string {
|
||||
const match = OPENCLAW_RELEASE_SUFFIX_PATTERN.exec(pluginApiVersion.trim());
|
||||
return match?.[1] ?? pluginApiVersion;
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ describe("npm registry spec parsing helpers", () => {
|
||||
{ value: "1.2.3-1", expected: true },
|
||||
{ value: "2026.5.3-beta.1", expected: true },
|
||||
{ value: "2026.5.3-1", expected: false },
|
||||
{ value: "2026.2.30-1", expected: true },
|
||||
{ value: "2026.2.30-1", expected: false },
|
||||
{ value: "1.2.3", expected: false },
|
||||
])("detects prerelease semver versions for %s", ({ value, expected }) => {
|
||||
expect(isPrereleaseSemverVersion(value)).toBe(expected);
|
||||
@@ -140,7 +140,7 @@ describe("npm registry spec parsing helpers", () => {
|
||||
{ value: "2026.5.3-2", expected: true },
|
||||
{ value: "2026.5.3-beta.1", expected: false },
|
||||
{ value: "1.2.3-1", expected: false },
|
||||
{ value: "2026.2.30-1", expected: false },
|
||||
{ value: "2026.2.30-1", expected: true },
|
||||
])("detects OpenClaw stable correction versions for %s", ({ value, expected }) => {
|
||||
expect(isOpenClawStableCorrectionVersion(value)).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -4,18 +4,21 @@ import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/st
|
||||
const EXACT_SEMVER_VERSION_RE =
|
||||
/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
|
||||
const OPENCLAW_STABLE_CORRECTION_VERSION_RE =
|
||||
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)-(?<correction>[1-9]\d*)$/;
|
||||
const OPENCLAW_STABLE_VERSION_RE = /^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)$/;
|
||||
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<patch>[1-9]\d*)-(?<correction>[1-9]\d*)$/;
|
||||
const OPENCLAW_STABLE_VERSION_RE =
|
||||
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<patch>[1-9]\d*)$/;
|
||||
const OPENCLAW_ALPHA_VERSION_RE =
|
||||
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)-alpha\.(?<alpha>[1-9]\d*)$/;
|
||||
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<patch>[1-9]\d*)-alpha\.(?<alpha>[1-9]\d*)$/;
|
||||
const OPENCLAW_BETA_VERSION_RE =
|
||||
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)-beta\.(?<beta>[1-9]\d*)$/;
|
||||
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<patch>[1-9]\d*)-beta\.(?<beta>[1-9]\d*)$/;
|
||||
const DIST_TAG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
||||
|
||||
/** Parsed date-based OpenClaw release version used for channel-aware ordering. */
|
||||
/** Parsed monthly patch OpenClaw release version used for channel-aware ordering. */
|
||||
type OpenClawReleaseVersion = {
|
||||
channel: "alpha" | "beta" | "stable";
|
||||
dateTime: number;
|
||||
year: number;
|
||||
month: number;
|
||||
patch: number;
|
||||
alphaNumber?: number;
|
||||
betaNumber?: number;
|
||||
correctionNumber?: number;
|
||||
@@ -144,7 +147,7 @@ export function isExactSemverVersion(value: string): boolean {
|
||||
return EXACT_SEMVER_VERSION_RE.test(value.trim());
|
||||
}
|
||||
|
||||
/** Parses OpenClaw's date-based stable/alpha/beta/correction version format. */
|
||||
/** Parses OpenClaw's monthly patch stable/alpha/beta/correction version format. */
|
||||
function parseOpenClawReleaseVersion(value: string): OpenClawReleaseVersion | null {
|
||||
const trimmed = value.trim();
|
||||
const candidates = [
|
||||
@@ -160,15 +163,14 @@ function parseOpenClawReleaseVersion(value: string): OpenClawReleaseVersion | nu
|
||||
|
||||
const year = Number.parseInt(candidate.match.groups.year ?? "", 10);
|
||||
const month = Number.parseInt(candidate.match.groups.month ?? "", 10);
|
||||
const day = Number.parseInt(candidate.match.groups.day ?? "", 10);
|
||||
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) {
|
||||
return null;
|
||||
}
|
||||
const date = new Date(Date.UTC(year, month - 1, day));
|
||||
const patch = Number.parseInt(candidate.match.groups.patch ?? "", 10);
|
||||
if (
|
||||
date.getUTCFullYear() !== year ||
|
||||
date.getUTCMonth() !== month - 1 ||
|
||||
date.getUTCDate() !== day
|
||||
!Number.isInteger(year) ||
|
||||
!Number.isInteger(month) ||
|
||||
!Number.isInteger(patch) ||
|
||||
month < 1 ||
|
||||
month > 12 ||
|
||||
patch < 1
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -190,28 +192,36 @@ function parseOpenClawReleaseVersion(value: string): OpenClawReleaseVersion | nu
|
||||
|
||||
return {
|
||||
channel: candidate.channel,
|
||||
dateTime: date.getTime(),
|
||||
year,
|
||||
month,
|
||||
patch,
|
||||
correctionNumber,
|
||||
alphaNumber,
|
||||
betaNumber,
|
||||
};
|
||||
}
|
||||
|
||||
/** Returns whether a version is an OpenClaw date-based stable correction release. */
|
||||
/** Returns whether a version is an OpenClaw monthly patch stable correction release. */
|
||||
export function isOpenClawStableCorrectionVersion(value: string): boolean {
|
||||
const parsed = parseOpenClawReleaseVersion(value);
|
||||
return parsed?.channel === "stable" && parsed.correctionNumber !== undefined;
|
||||
}
|
||||
|
||||
/** Compares OpenClaw date-based release versions across alpha, beta, stable, and corrections. */
|
||||
/** Compares OpenClaw monthly patch release versions across alpha, beta, stable, and corrections. */
|
||||
export function compareOpenClawReleaseVersions(left: string, right: string): number | null {
|
||||
const parsedLeft = parseOpenClawReleaseVersion(left);
|
||||
const parsedRight = parseOpenClawReleaseVersion(right);
|
||||
if (!parsedLeft || !parsedRight) {
|
||||
return null;
|
||||
}
|
||||
if (parsedLeft.dateTime !== parsedRight.dateTime) {
|
||||
return parsedLeft.dateTime < parsedRight.dateTime ? -1 : 1;
|
||||
if (parsedLeft.year !== parsedRight.year) {
|
||||
return parsedLeft.year < parsedRight.year ? -1 : 1;
|
||||
}
|
||||
if (parsedLeft.month !== parsedRight.month) {
|
||||
return parsedLeft.month < parsedRight.month ? -1 : 1;
|
||||
}
|
||||
if (parsedLeft.patch !== parsedRight.patch) {
|
||||
return parsedLeft.patch < parsedRight.patch ? -1 : 1;
|
||||
}
|
||||
if (parsedLeft.channel !== parsedRight.channel) {
|
||||
const rank = { alpha: 0, beta: 1, stable: 2 };
|
||||
|
||||
@@ -859,7 +859,7 @@ describe("installPluginFromClawHub", () => {
|
||||
expect(archiveCleanupMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("installs when a CalVer correction runtime satisfies the base plugin API range", async () => {
|
||||
it("installs when a release correction runtime satisfies the base plugin API range", async () => {
|
||||
resolveCompatibilityHostVersionMock.mockReturnValueOnce("2026.5.3-1");
|
||||
fetchClawHubPackageVersionMock.mockResolvedValueOnce({
|
||||
version: {
|
||||
|
||||
@@ -11,6 +11,18 @@ type AppcastItem = {
|
||||
sparkleVersion: number | null;
|
||||
};
|
||||
|
||||
describe("canonicalSparkleBuildFromVersion", () => {
|
||||
it("keeps pre-transition appcast builds on the legacy date key", () => {
|
||||
expect(canonicalSparkleBuildFromVersion("2026.6.2")).toBe(2026060290);
|
||||
});
|
||||
|
||||
it("uses monthly patch build keys from the June 2026 floor onward", () => {
|
||||
expect(canonicalSparkleBuildFromVersion("2026.6.5-beta.2")).toBe(2606000502);
|
||||
expect(canonicalSparkleBuildFromVersion("2026.6.32-beta.1")).toBe(2606003201);
|
||||
expect(canonicalSparkleBuildFromVersion("2026.6.32")).toBe(2606003290);
|
||||
});
|
||||
});
|
||||
|
||||
function parseItems(appcast: string): AppcastItem[] {
|
||||
return [...appcast.matchAll(/<item>([\s\S]*?)<\/item>/g)].map((match) => {
|
||||
const raw = match[1] ?? "";
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
// npm publish plan tests validate package publish planning rules.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
collectReleaseVersionFloorErrors,
|
||||
resolveNpmDistTagMirrorAuth,
|
||||
resolveNpmPublishPlan,
|
||||
shouldRequireNpmDistTagMirrorAuth,
|
||||
} from "../scripts/lib/npm-publish-plan.mjs";
|
||||
|
||||
describe("collectReleaseVersionFloorErrors", () => {
|
||||
it("blocks June 2026 stable and beta release trains below the published beta floor", () => {
|
||||
expect(collectReleaseVersionFloorErrors("2026.6.4")).toEqual([
|
||||
'June 2026 stable and beta release trains must use patch 5 or higher because 2026.6.5-beta.1 is already published; found "2026.6.4".',
|
||||
]);
|
||||
expect(collectReleaseVersionFloorErrors("2026.6.4-beta.1")).toEqual([
|
||||
'June 2026 stable and beta release trains must use patch 5 or higher because 2026.6.5-beta.1 is already published; found "2026.6.4-beta.1".',
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps alpha compatibility and patch-floor release trains valid during the transition", () => {
|
||||
expect(collectReleaseVersionFloorErrors("2026.6.4-alpha.1")).toEqual([]);
|
||||
expect(collectReleaseVersionFloorErrors("2026.6.5-beta.2")).toEqual([]);
|
||||
expect(collectReleaseVersionFloorErrors("2026.7.1")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldRequireNpmDistTagMirrorAuth", () => {
|
||||
it("does not require npm auth for dry-run preview commands", () => {
|
||||
const plan = resolveNpmPublishPlan("2026.4.1");
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
resolveNpmReleaseCheckCommandTimeoutMs,
|
||||
runNpmReleaseCheckCommand,
|
||||
shouldSkipPackedTarballValidation,
|
||||
utcCalendarDayDistance,
|
||||
} from "../scripts/openclaw-npm-release-check.ts";
|
||||
import {
|
||||
LOCAL_BUILD_METADATA_DIST_PATHS,
|
||||
@@ -51,45 +50,42 @@ describe("workspace template package paths", () => {
|
||||
});
|
||||
|
||||
describe("parseReleaseVersion", () => {
|
||||
it("parses stable CalVer releases", () => {
|
||||
it("parses stable monthly patch releases", () => {
|
||||
expect(parseReleaseVersion("2026.3.10")).toStrictEqual({
|
||||
version: "2026.3.10",
|
||||
baseVersion: "2026.3.10",
|
||||
channel: "stable",
|
||||
year: 2026,
|
||||
month: 3,
|
||||
day: 10,
|
||||
patch: 10,
|
||||
alphaNumber: undefined,
|
||||
betaNumber: undefined,
|
||||
date: new Date(Date.UTC(2026, 2, 10)),
|
||||
});
|
||||
});
|
||||
|
||||
it("parses beta CalVer releases", () => {
|
||||
it("parses beta monthly patch releases", () => {
|
||||
expect(parseReleaseVersion("2026.3.10-beta.2")).toStrictEqual({
|
||||
version: "2026.3.10-beta.2",
|
||||
baseVersion: "2026.3.10",
|
||||
channel: "beta",
|
||||
year: 2026,
|
||||
month: 3,
|
||||
day: 10,
|
||||
patch: 10,
|
||||
alphaNumber: undefined,
|
||||
betaNumber: 2,
|
||||
date: new Date(Date.UTC(2026, 2, 10)),
|
||||
});
|
||||
});
|
||||
|
||||
it("parses alpha CalVer releases", () => {
|
||||
it("parses alpha monthly patch releases", () => {
|
||||
expect(parseReleaseVersion("2026.3.10-alpha.2")).toStrictEqual({
|
||||
version: "2026.3.10-alpha.2",
|
||||
baseVersion: "2026.3.10",
|
||||
channel: "alpha",
|
||||
year: 2026,
|
||||
month: 3,
|
||||
day: 10,
|
||||
patch: 10,
|
||||
alphaNumber: 2,
|
||||
betaNumber: undefined,
|
||||
date: new Date(Date.UTC(2026, 2, 10)),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,18 +96,34 @@ describe("parseReleaseVersion", () => {
|
||||
channel: "stable",
|
||||
year: 2026,
|
||||
month: 3,
|
||||
day: 10,
|
||||
patch: 10,
|
||||
alphaNumber: undefined,
|
||||
betaNumber: undefined,
|
||||
date: new Date(Date.UTC(2026, 2, 10)),
|
||||
correctionNumber: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts patch numbers that are not calendar days", () => {
|
||||
expect(parseReleaseVersion("2026.2.30")).toMatchObject({
|
||||
version: "2026.2.30",
|
||||
baseVersion: "2026.2.30",
|
||||
channel: "stable",
|
||||
patch: 30,
|
||||
});
|
||||
expect(parseReleaseVersion("2026.6.32-beta.1")).toMatchObject({
|
||||
version: "2026.6.32-beta.1",
|
||||
baseVersion: "2026.6.32",
|
||||
channel: "beta",
|
||||
patch: 32,
|
||||
betaNumber: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects legacy and malformed release formats", () => {
|
||||
expect(parseReleaseVersion("2026.03.09")).toBeNull();
|
||||
expect(parseReleaseVersion("v2026.3.10")).toBeNull();
|
||||
expect(parseReleaseVersion("2026.2.30")).toBeNull();
|
||||
expect(parseReleaseVersion("2026.13.1")).toBeNull();
|
||||
expect(parseReleaseVersion("2026.3.0")).toBeNull();
|
||||
expect(parseReleaseVersion("2026.3.10-0")).toBeNull();
|
||||
expect(parseReleaseVersion("2.0.0-beta2")).toBeNull();
|
||||
});
|
||||
@@ -124,7 +136,6 @@ describe("parseReleaseTagVersion", () => {
|
||||
packageVersion: "2026.3.10-2",
|
||||
baseVersion: "2026.3.10",
|
||||
channel: "stable",
|
||||
date: new Date(Date.UTC(2026, 2, 10)),
|
||||
correctionNumber: 2,
|
||||
});
|
||||
});
|
||||
@@ -269,15 +280,15 @@ describe("shouldSkipPackedTarballValidation", () => {
|
||||
});
|
||||
|
||||
describe("compareReleaseVersions", () => {
|
||||
it("treats stable as newer than same-day beta", () => {
|
||||
it("treats stable as newer than same-patch beta", () => {
|
||||
expect(compareReleaseVersions("2026.3.29", "2026.3.29-beta.2")).toBe(1);
|
||||
});
|
||||
|
||||
it("orders alpha before beta on the same day", () => {
|
||||
it("orders alpha before beta on the same patch", () => {
|
||||
expect(compareReleaseVersions("2026.3.29-alpha.2", "2026.3.29-beta.1")).toBe(-1);
|
||||
});
|
||||
|
||||
it("treats a newer beta day as newer than an older stable day", () => {
|
||||
it("treats a newer beta patch as newer than an older stable patch", () => {
|
||||
expect(compareReleaseVersions("2026.4.1-beta.1", "2026.3.29")).toBe(1);
|
||||
});
|
||||
|
||||
@@ -290,14 +301,6 @@ describe("compareReleaseVersions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("utcCalendarDayDistance", () => {
|
||||
it("compares UTC calendar days rather than wall-clock hours", () => {
|
||||
const left = new Date("2026-03-09T23:59:59Z");
|
||||
const right = new Date("2026-03-11T00:00:01Z");
|
||||
expect(utcCalendarDayDistance(left, right)).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveNpmCommandInvocation", () => {
|
||||
it("uses npm_execpath when it points to npm", () => {
|
||||
expect(
|
||||
@@ -698,28 +701,57 @@ describe("collectPackedTestCargoErrors", () => {
|
||||
});
|
||||
|
||||
describe("collectReleaseTagErrors", () => {
|
||||
it("accepts versions within the two-day CalVer window", () => {
|
||||
it("accepts monthly patch versions beyond calendar day ranges", () => {
|
||||
expect(
|
||||
collectReleaseTagErrors({
|
||||
packageVersion: "2026.3.10",
|
||||
releaseTag: "v2026.3.10",
|
||||
now: new Date("2026-03-11T12:00:00Z"),
|
||||
packageVersion: "2026.3.40",
|
||||
releaseTag: "v2026.3.40",
|
||||
}),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("rejects versions outside the two-day CalVer window", () => {
|
||||
it("rejects malformed monthly patch versions", () => {
|
||||
expect(
|
||||
collectReleaseTagErrors({
|
||||
packageVersion: "2026.3.10",
|
||||
releaseTag: "v2026.3.10",
|
||||
now: new Date("2026-03-13T00:00:00Z"),
|
||||
packageVersion: "2026.3.0",
|
||||
releaseTag: "v2026.3.0",
|
||||
}),
|
||||
).toStrictEqual([
|
||||
"Release version 2026.3.10 is 3 days away from current UTC date 2026-03-13; release CalVer date 2026-03-10 must be within 2 days.",
|
||||
'package.json version must match YYYY.M.PATCH, YYYY.M.PATCH-N, YYYY.M.PATCH-alpha.N, or YYYY.M.PATCH-beta.N; found "2026.3.0".',
|
||||
'Release tag must match vYYYY.M.PATCH, vYYYY.M.PATCH-alpha.N, vYYYY.M.PATCH-beta.N, or fallback correction tag vYYYY.M.PATCH-N; found "v2026.3.0".',
|
||||
"Release tag v2026.3.0 does not match package.json version 2026.3.0; expected v2026.3.0.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects new June 2026 stable and beta trains below the transition floor", () => {
|
||||
expect(
|
||||
collectReleaseTagErrors({
|
||||
packageVersion: "2026.6.4",
|
||||
releaseTag: "v2026.6.4",
|
||||
}),
|
||||
).toStrictEqual([
|
||||
'June 2026 stable and beta release trains must use patch 5 or higher because 2026.6.5-beta.1 is already published; found "2026.6.4".',
|
||||
]);
|
||||
|
||||
expect(
|
||||
collectReleaseTagErrors({
|
||||
packageVersion: "2026.6.4-beta.1",
|
||||
releaseTag: "v2026.6.4-beta.1",
|
||||
}),
|
||||
).toStrictEqual([
|
||||
'June 2026 stable and beta release trains must use patch 5 or higher because 2026.6.5-beta.1 is already published; found "2026.6.4-beta.1".',
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps pre-transition June alpha tags parseable for compatibility", () => {
|
||||
expect(
|
||||
collectReleaseTagErrors({
|
||||
packageVersion: "2026.6.4-alpha.1",
|
||||
releaseTag: "v2026.6.4-alpha.1",
|
||||
}),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("accepts fallback correction tags for stable package versions", () => {
|
||||
expect(
|
||||
collectReleaseTagErrors({
|
||||
|
||||
@@ -5,8 +5,9 @@ import { bundledPluginFile, bundledPluginRoot } from "openclaw/plugin-sdk/test-f
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { collectClawHubPublishablePluginPackages } from "../scripts/lib/plugin-clawhub-release.ts";
|
||||
import {
|
||||
collectPublishablePluginPackages,
|
||||
collectChangedExtensionIdsFromPaths,
|
||||
collectPluginReleaseVersionFloorErrors,
|
||||
collectPublishablePluginPackages,
|
||||
collectPublishablePluginPackageErrors,
|
||||
OPENCLAW_PLUGIN_NPM_REPOSITORY_URL,
|
||||
parsePluginReleaseArgs,
|
||||
@@ -150,7 +151,7 @@ describe("collectPublishablePluginPackageErrors", () => {
|
||||
'package name must start with "@openclaw/"; found "broken".',
|
||||
"package.json private must not be true.",
|
||||
`package.json repository.url must be "${OPENCLAW_PLUGIN_NPM_REPOSITORY_URL}" so npm provenance can validate GitHub trusted publishing; found "<missing>".`,
|
||||
'package.json version must match YYYY.M.D, YYYY.M.D-N, YYYY.M.D-alpha.N, or YYYY.M.D-beta.N; found "latest".',
|
||||
'package.json version must match YYYY.M.PATCH, YYYY.M.PATCH-N, YYYY.M.PATCH-alpha.N, or YYYY.M.PATCH-beta.N; found "latest".',
|
||||
"openclaw.extensions must contain only non-empty strings.",
|
||||
"openclaw.install.npmSpec must be a non-empty string for publishable plugins.",
|
||||
]);
|
||||
@@ -235,6 +236,36 @@ describe("collectPublishablePluginPackageErrors", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectPluginReleaseVersionFloorErrors", () => {
|
||||
it("blocks selected plugin stable and beta releases below the June 2026 floor", () => {
|
||||
expect(
|
||||
collectPluginReleaseVersionFloorErrors([
|
||||
{
|
||||
packageName: "@openclaw/demo",
|
||||
version: "2026.6.4-beta.1",
|
||||
},
|
||||
]),
|
||||
).toEqual([
|
||||
'@openclaw/demo@2026.6.4-beta.1: June 2026 stable and beta release trains must use patch 5 or higher because 2026.6.5-beta.1 is already published; found "2026.6.4-beta.1".',
|
||||
]);
|
||||
});
|
||||
|
||||
it("allows alpha compatibility and patch-floor plugin releases", () => {
|
||||
expect(
|
||||
collectPluginReleaseVersionFloorErrors([
|
||||
{
|
||||
packageName: "@openclaw/demo",
|
||||
version: "2026.6.4-alpha.1",
|
||||
},
|
||||
{
|
||||
packageName: "@openclaw/demo",
|
||||
version: "2026.6.5-beta.2",
|
||||
},
|
||||
]),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectPublishablePluginPackages", () => {
|
||||
it("keeps publishable plugin dist trees out of the core npm package files list", () => {
|
||||
const corePackageRuntimePluginIds = new Set(["discord"]);
|
||||
|
||||
@@ -11,10 +11,10 @@ installIosFixtureCleanup();
|
||||
describe("parseArgs", () => {
|
||||
it("requires exactly one pin source", () => {
|
||||
expect(() => parseArgs([])).toThrow(
|
||||
"Choose exactly one of --from-gateway or --version <YYYY.M.D>",
|
||||
"Choose exactly one of --from-gateway or --version <YYYY.M.PATCH>",
|
||||
);
|
||||
expect(() => parseArgs(["--from-gateway", "--version", "2026.4.7"])).toThrow(
|
||||
"Choose exactly one of --from-gateway or --version <YYYY.M.D>",
|
||||
"Choose exactly one of --from-gateway or --version <YYYY.M.PATCH>",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import { installIosFixtureCleanup, writeIosFixture } from "./ios-version.test-su
|
||||
installIosFixtureCleanup();
|
||||
|
||||
describe("resolveIosVersion", () => {
|
||||
it("parses pinned CalVer versions and derives Apple marketing fields", () => {
|
||||
it("parses pinned release versions and derives Apple marketing fields", () => {
|
||||
const rootDir = writeIosFixture({
|
||||
version: "2026.4.6",
|
||||
changelog: "# OpenClaw iOS Changelog\n\n## 2026.4.6\n\nStable notes.\n",
|
||||
@@ -38,7 +38,7 @@ describe("resolveIosVersion", () => {
|
||||
changelog: "# OpenClaw iOS Changelog\n\n## Unreleased\n\nNotes.\n",
|
||||
});
|
||||
|
||||
expect(() => resolveIosVersion(rootDir)).toThrow("Expected pinned CalVer like 2026.4.6");
|
||||
expect(() => resolveIosVersion(rootDir)).toThrow("Expected pinned release version like 2026.6.5");
|
||||
});
|
||||
|
||||
it("rejects prerelease suffixes in the pinned iOS version file", () => {
|
||||
@@ -47,12 +47,12 @@ describe("resolveIosVersion", () => {
|
||||
changelog: "# OpenClaw iOS Changelog\n\n## Unreleased\n\nNotes.\n",
|
||||
});
|
||||
|
||||
expect(() => resolveIosVersion(rootDir)).toThrow("Expected pinned CalVer like 2026.4.6");
|
||||
expect(() => resolveIosVersion(rootDir)).toThrow("Expected pinned release version like 2026.6.5");
|
||||
});
|
||||
});
|
||||
|
||||
describe("gateway version normalization", () => {
|
||||
it("keeps stable gateway CalVer values", () => {
|
||||
it("keeps stable gateway release values", () => {
|
||||
expect(normalizeGatewayVersionToPinnedIosVersion("2026.4.6")).toBe("2026.4.6");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user