diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml index fef082a9ba94..c0f0bbc22b81 100644 --- a/.github/workflows/macos-release.yml +++ b/.github/workflows/macos-release.yml @@ -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) diff --git a/.github/workflows/openclaw-npm-release.yml b/.github/workflows/openclaw-npm-release.yml index bbfb6bb3e0f0..865b73cecdf4 100644 --- a/.github/workflows/openclaw-npm-release.yml +++ b/.github/workflows/openclaw-npm-release.yml @@ -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 diff --git a/.github/workflows/openclaw-release-checks.yml b/.github/workflows/openclaw-release-checks.yml index f17f665ac6c9..0ae8a99930d9 100644 --- a/.github/workflows/openclaw-release-checks.yml +++ b/.github/workflows/openclaw-release-checks.yml @@ -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/- 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/- ref, or a Tideclaw alpha branch for alpha prereleases." >&2 exit 1 fi diff --git a/.github/workflows/openclaw-release-publish.yml b/.github/workflows/openclaw-release-publish.yml index b8fb1ec55c99..70e53b103cbd 100644 --- a/.github/workflows/openclaw-release-publish.yml +++ b/.github/workflows/openclaw-release-publish.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d0f84941622..6a924e830813 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/ci.md b/docs/ci.md index 151ce71a0628..ca13534166d5 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -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= -f include_android=true gh workflow run full-release-validation.yml --ref main -f ref= ``` @@ -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= \ -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 diff --git a/docs/help/testing-updates-plugins.md b/docs/help/testing-updates-plugins.md index 7d3139555ec6..f75566e85d19 100644 --- a/docs/help/testing-updates-plugins.md +++ b/docs/help/testing-updates-plugins.md @@ -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: diff --git a/docs/install/development-channels.md b/docs/install/development-channels.md index 7899c13fd451..0208d548531d 100644 --- a/docs/install/development-channels.md +++ b/docs/install/development-channels.md @@ -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 diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index fefd7da9cadb..153cca4f99d5 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -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--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= \ -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= \ -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= \ -f npm_dist_tag=latest ``` diff --git a/docs/reference/full-release-validation.md b/docs/reference/full-release-validation.md index ae88430be594..693c1e9d846c 100644 --- a/docs/reference/full-release-validation.md +++ b/docs/reference/full-release-validation.md @@ -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. diff --git a/scripts/ios-pin-version.ts b/scripts/ios-pin-version.ts index 350d9f69433c..4da9af5b6978 100644 --- a/scripts/ios-pin-version.ts +++ b/scripts/ios-pin-version.ts @@ -25,7 +25,7 @@ export type PinIosVersionResult = { function usage(): string { return [ - "Usage: node --import tsx scripts/ios-pin-version.ts (--from-gateway | --version ) [--no-sync] [--root dir]", + "Usage: node --import tsx scripts/ios-pin-version.ts (--from-gateway | --version ) [--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 ."); + throw new Error("Choose exactly one of --from-gateway or --version ."); } if (explicitVersion !== null && !explicitVersion.trim()) { diff --git a/scripts/lib/docker-e2e-plan.mjs b/scripts/lib/docker-e2e-plan.mjs index 2d929d5231a8..55cbc9c08612 100644 --- a/scripts/lib/docker-e2e-plan.mjs +++ b/scripts/lib/docker-e2e-plan.mjs @@ -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 = "") { diff --git a/scripts/lib/ios-version.ts b/scripts/lib/ios-version.ts index 07ba0bc084bb..d575edac716b 100644 --- a/scripts/lib/ios-version.ts +++ b/scripts/lib/ios-version.ts @@ -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.`, ); } diff --git a/scripts/lib/npm-publish-plan.mjs b/scripts/lib/npm-publish-plan.mjs index 02a605022ce0..a7d5b08c133b 100644 --- a/scripts/lib/npm-publish-plan.mjs +++ b/scripts/lib/npm-publish-plan.mjs @@ -1,11 +1,12 @@ -// Parses OpenClaw date-based release versions and npm dist-tag publish plans. -const STABLE_VERSION_REGEX = /^(?\d{4})\.(?[1-9]\d?)\.(?[1-9]\d?)$/; +// Parses OpenClaw monthly patch release versions and npm dist-tag publish plans. +const STABLE_VERSION_REGEX = /^(?\d{4})\.(?[1-9]\d?)\.(?[1-9]\d*)$/; const ALPHA_VERSION_REGEX = - /^(?\d{4})\.(?[1-9]\d?)\.(?[1-9]\d?)-alpha\.(?[1-9]\d*)$/; + /^(?\d{4})\.(?[1-9]\d?)\.(?[1-9]\d*)-alpha\.(?[1-9]\d*)$/; const BETA_VERSION_REGEX = - /^(?\d{4})\.(?[1-9]\d?)\.(?[1-9]\d?)-beta\.(?[1-9]\d*)$/; + /^(?\d{4})\.(?[1-9]\d?)\.(?[1-9]\d*)-beta\.(?[1-9]\d*)$/; const CORRECTION_VERSION_REGEX = - /^(?\d{4})\.(?[1-9]\d?)\.(?[1-9]\d?)-(?[1-9]\d*)$/; + /^(?\d{4})\.(?[1-9]\d?)\.(?[1-9]\d*)-(?[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) { diff --git a/scripts/lib/plugin-clawhub-release.ts b/scripts/lib/plugin-clawhub-release.ts index 0f6647bf4a00..3cd227ff7f78 100644 --- a/scripts/lib/plugin-clawhub-release.ts +++ b/scripts/lib/plugin-clawhub-release.ts @@ -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, { diff --git a/scripts/lib/plugin-npm-release.ts b/scripts/lib/plugin-npm-release.ts index 6479bfc62ee6..dc09856549e7 100644 --- a/scripts/lib/plugin-npm-release.ts +++ b/scripts/lib/plugin-npm-release.ts @@ -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[], +): string[] { + return plugins.flatMap((plugin) => + collectReleaseVersionFloorErrors(plugin.version).map( + (error) => `${plugin.packageName}@${plugin.version}: ${error}`, + ), + ); +} + +export function assertPluginReleaseVersionFloors( + plugins: readonly Pick[], + 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), diff --git a/scripts/openclaw-npm-release-check.ts b/scripts/openclaw-npm-release-check.ts index 8851627b2048..d1aff83091be 100644 --- a/scripts/openclaw-npm-release-check.ts +++ b/scripts/openclaw-npm-release-check.ts @@ -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 || ""}".`, + `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 || ""}".`, ); + } 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 || ""}".`, + `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 || ""}".`, ); } @@ -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[] async function main(): Promise { 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 { 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 { 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; } diff --git a/scripts/plugin-clawhub-release-check.ts b/scripts/plugin-clawhub-release-check.ts index 1806e01fa687..bf1d91d387bf 100644 --- a/scripts/plugin-clawhub-release-check.ts +++ b/scripts/plugin-clawhub-release-check.ts @@ -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, diff --git a/scripts/plugin-npm-release-check.ts b/scripts/plugin-npm-release-check.ts index 8f76297eaba4..14258fd40402 100644 --- a/scripts/plugin-npm-release-check.ts +++ b/scripts/plugin-npm-release-check.ts @@ -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( diff --git a/scripts/release-candidate-checklist.mjs b/scripts/release-candidate-checklist.mjs index 6f0903a28ddb..55ab953aa825 100644 --- a/scripts/release-candidate-checklist.mjs +++ b/scripts/release-candidate-checklist.mjs @@ -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 diff --git a/scripts/release-check.ts b/scripts/release-check.ts index facd540e60e2..b6945f9c04f8 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -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"; diff --git a/scripts/sparkle-build.ts b/scripts/sparkle-build.ts index e48ba10c712e..f10cb5ac3291 100644 --- a/scripts/sparkle-build.ts +++ b/scripts/sparkle-build.ts @@ -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 { diff --git a/src/gateway/server.node-version-mismatch.test.ts b/src/gateway/server.node-version-mismatch.test.ts index a783df63b42a..ec8fd09f68b3 100644 --- a/src/gateway/server.node-version-mismatch.test.ts +++ b/src/gateway/server.node-version-mismatch.test.ts @@ -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", diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index 1c49d0c38706..9d25b3a93485 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -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 { diff --git a/src/infra/clawhub.test.ts b/src/infra/clawhub.test.ts index e0a8df62d75e..58a8f1360779 100644 --- a/src/infra/clawhub.test.ts +++ b/src/infra/clawhub.test.ts @@ -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); diff --git a/src/infra/clawhub.ts b/src/infra/clawhub.ts index cdcfb1f4514d..55b6479ca646 100644 --- a/src/infra/clawhub.ts +++ b/src/infra/clawhub.ts @@ -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; } diff --git a/src/infra/npm-registry-spec.test.ts b/src/infra/npm-registry-spec.test.ts index 87ce5c7807af..aef9f89267d6 100644 --- a/src/infra/npm-registry-spec.test.ts +++ b/src/infra/npm-registry-spec.test.ts @@ -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); }); diff --git a/src/infra/npm-registry-spec.ts b/src/infra/npm-registry-spec.ts index 8cfcc8ce0cfc..fed01fa3ca67 100644 --- a/src/infra/npm-registry-spec.ts +++ b/src/infra/npm-registry-spec.ts @@ -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 = - /^(?\d{4})\.(?[1-9]\d?)\.(?[1-9]\d?)-(?[1-9]\d*)$/; -const OPENCLAW_STABLE_VERSION_RE = /^(?\d{4})\.(?[1-9]\d?)\.(?[1-9]\d?)$/; + /^(?\d{4})\.(?[1-9]\d?)\.(?[1-9]\d*)-(?[1-9]\d*)$/; +const OPENCLAW_STABLE_VERSION_RE = + /^(?\d{4})\.(?[1-9]\d?)\.(?[1-9]\d*)$/; const OPENCLAW_ALPHA_VERSION_RE = - /^(?\d{4})\.(?[1-9]\d?)\.(?[1-9]\d?)-alpha\.(?[1-9]\d*)$/; + /^(?\d{4})\.(?[1-9]\d?)\.(?[1-9]\d*)-alpha\.(?[1-9]\d*)$/; const OPENCLAW_BETA_VERSION_RE = - /^(?\d{4})\.(?[1-9]\d?)\.(?[1-9]\d?)-beta\.(?[1-9]\d*)$/; + /^(?\d{4})\.(?[1-9]\d?)\.(?[1-9]\d*)-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 }; diff --git a/src/plugins/clawhub.test.ts b/src/plugins/clawhub.test.ts index 62defa8a7892..f4e899074428 100644 --- a/src/plugins/clawhub.test.ts +++ b/src/plugins/clawhub.test.ts @@ -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: { diff --git a/test/appcast.test.ts b/test/appcast.test.ts index 40bc17178d75..bf6e85784578 100644 --- a/test/appcast.test.ts +++ b/test/appcast.test.ts @@ -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(/([\s\S]*?)<\/item>/g)].map((match) => { const raw = match[1] ?? ""; diff --git a/test/npm-publish-plan.test.ts b/test/npm-publish-plan.test.ts index 07c648e3fcfe..e7445bcf23c7 100644 --- a/test/npm-publish-plan.test.ts +++ b/test/npm-publish-plan.test.ts @@ -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"); diff --git a/test/openclaw-npm-release-check.test.ts b/test/openclaw-npm-release-check.test.ts index f18c8a75a016..52a20fc51ced 100644 --- a/test/openclaw-npm-release-check.test.ts +++ b/test/openclaw-npm-release-check.test.ts @@ -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({ diff --git a/test/plugin-npm-release.test.ts b/test/plugin-npm-release.test.ts index bc0d128bd263..7a1b53759362 100644 --- a/test/plugin-npm-release.test.ts +++ b/test/plugin-npm-release.test.ts @@ -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 "".`, - '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"]); diff --git a/test/scripts/ios-pin-version.test.ts b/test/scripts/ios-pin-version.test.ts index d882a5927650..e3342f6ca43f 100644 --- a/test/scripts/ios-pin-version.test.ts +++ b/test/scripts/ios-pin-version.test.ts @@ -11,10 +11,10 @@ installIosFixtureCleanup(); describe("parseArgs", () => { it("requires exactly one pin source", () => { expect(() => parseArgs([])).toThrow( - "Choose exactly one of --from-gateway or --version ", + "Choose exactly one of --from-gateway or --version ", ); expect(() => parseArgs(["--from-gateway", "--version", "2026.4.7"])).toThrow( - "Choose exactly one of --from-gateway or --version ", + "Choose exactly one of --from-gateway or --version ", ); }); }); diff --git a/test/scripts/ios-version.test.ts b/test/scripts/ios-version.test.ts index c887af375363..050353d93004 100644 --- a/test/scripts/ios-version.test.ts +++ b/test/scripts/ios-version.test.ts @@ -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"); });