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:
Vincent Koc
2026-06-06 12:26:32 -07:00
committed by GitHub
parent 15cb26e6cb
commit f5a7f613ee
35 changed files with 431 additions and 230 deletions
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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:
+4 -4
View File
@@ -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
View File
@@ -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
```
+2 -2
View File
@@ -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.
+2 -2
View File
@@ -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()) {
+4 -4
View File
@@ -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 = "") {
+6 -4
View File
@@ -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.`,
);
}
+48 -31
View File
@@ -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) {
+7 -1
View File
@@ -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, {
+34 -3
View File
@@ -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),
+7 -33
View File
@@ -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
View File
@@ -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
View File
@@ -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(
+1 -1
View File
@@ -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
+9 -8
View File
@@ -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
View File
@@ -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 {
+2 -1
View File
@@ -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);
+9 -9
View File
@@ -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;
}
+2 -2
View File
@@ -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);
});
+30 -20
View File
@@ -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 };
+1 -1
View File
@@ -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: {
+12
View File
@@ -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] ?? "";
+18
View File
@@ -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");
+66 -34
View File
@@ -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({
+33 -2
View File
@@ -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"]);
+2 -2
View File
@@ -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>",
);
});
});
+4 -4
View File
@@ -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");
});