diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index a8790214328..ffbc83fdb8e 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -38,7 +38,7 @@ jobs: RELEASE_TAG: ${{ inputs.tag }} run: | set -euo pipefail - if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-beta\.[1-9][0-9]*)?$ ]]; then + if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-(alpha|beta)\.[1-9][0-9]*)?$ ]]; then echo "Invalid release tag: ${RELEASE_TAG}" exit 1 fi diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml index edaea6bc7f1..ffea03537b7 100644 --- a/.github/workflows/macos-release.yml +++ b/.github/workflows/macos-release.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: tag: - description: Existing release tag to validate for macOS release handoff (for example v2026.3.22 or v2026.3.22-beta.1) + description: Existing release tag to validate for macOS release handoff (for example v2026.3.22, v2026.3.22-alpha.1, or v2026.3.22-beta.1) required: true type: string preflight_only: @@ -38,7 +38,7 @@ jobs: RELEASE_TAG: ${{ inputs.tag }} run: | set -euo pipefail - if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-beta\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]]; then + if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-(alpha|beta)\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]]; then echo "Invalid release tag format: ${RELEASE_TAG}" exit 1 fi diff --git a/.github/workflows/npm-telegram-beta-e2e.yml b/.github/workflows/npm-telegram-beta-e2e.yml index b8e25f5c6db..69ae56b1631 100644 --- a/.github/workflows/npm-telegram-beta-e2e.yml +++ b/.github/workflows/npm-telegram-beta-e2e.yml @@ -152,8 +152,8 @@ jobs: set -euo pipefail if [[ -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then - if [[ ! "${PACKAGE_SPEC}" =~ ^openclaw@(beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-beta\.[1-9][0-9]*)?)$ ]]; then - echo "package_spec must be openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${PACKAGE_SPEC}" >&2 + if [[ ! "${PACKAGE_SPEC}" =~ ^openclaw@(alpha|beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-(alpha|beta)\.[1-9][0-9]*)?)$ ]]; then + echo "package_spec must be openclaw@alpha, openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${PACKAGE_SPEC}" >&2 exit 1 fi fi diff --git a/.github/workflows/openclaw-npm-release.yml b/.github/workflows/openclaw-npm-release.yml index 7fdcb436e05..85fa81931c4 100644 --- a/.github/workflows/openclaw-npm-release.yml +++ b/.github/workflows/openclaw-npm-release.yml @@ -17,11 +17,12 @@ on: required: false type: string npm_dist_tag: - description: npm dist-tag to publish to for stable releases + description: npm dist-tag to publish to required: true default: beta type: choice options: + - alpha - beta - latest @@ -54,7 +55,7 @@ jobs: RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }} run: | set -euo pipefail - if [[ ! "${RELEASE_REF}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-beta\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]] && [[ ! "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then + if [[ ! "${RELEASE_REF}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-(alpha|beta)\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]] && [[ ! "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then echo "Invalid release ref format: ${RELEASE_REF}" exit 1 fi @@ -62,6 +63,10 @@ jobs: echo "Full commit SHA input is only supported for validation-only preflight runs." exit 1 fi + if [[ "${RELEASE_REF}" == *"-alpha."* && "${RELEASE_NPM_DIST_TAG}" != "alpha" ]]; then + echo "Alpha prerelease tags must publish to npm dist-tag alpha." + exit 1 + fi if [[ "${RELEASE_REF}" == *"-beta."* && "${RELEASE_NPM_DIST_TAG}" != "beta" ]]; then echo "Beta prerelease tags must publish to npm dist-tag beta." exit 1 @@ -294,10 +299,14 @@ jobs: RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }} run: | set -euo pipefail - if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-beta\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]]; then + if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-(alpha|beta)\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]]; then echo "Invalid release tag format: ${RELEASE_TAG}" exit 1 fi + if [[ "${RELEASE_TAG}" == *"-alpha."* && "${RELEASE_NPM_DIST_TAG}" != "alpha" ]]; then + echo "Alpha prerelease tags must publish to npm dist-tag alpha." + exit 1 + fi if [[ "${RELEASE_TAG}" == *"-beta."* && "${RELEASE_NPM_DIST_TAG}" != "beta" ]]; then echo "Beta prerelease tags must publish to npm dist-tag beta." exit 1 diff --git a/.github/workflows/openclaw-release-publish.yml b/.github/workflows/openclaw-release-publish.yml index c1d44144b3b..7e40ff3277b 100644 --- a/.github/workflows/openclaw-release-publish.yml +++ b/.github/workflows/openclaw-release-publish.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: tag: - description: Release tag to publish, for example v2026.5.1-beta.1 + description: Release tag to publish, for example v2026.5.1-alpha.1 or v2026.5.1-beta.1 required: true type: string preflight_run_id: @@ -17,6 +17,7 @@ on: default: beta type: choice options: + - alpha - beta - latest plugin_publish_scope: @@ -69,10 +70,14 @@ jobs: WORKFLOW_REF: ${{ github.ref }} run: | set -euo pipefail - if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-beta\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]]; then + if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-(alpha|beta)\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]]; then echo "Invalid release tag: ${RELEASE_TAG}" >&2 exit 1 fi + if [[ "${RELEASE_TAG}" == *"-alpha."* && "${RELEASE_NPM_DIST_TAG}" != "alpha" ]]; then + echo "Alpha prerelease tags must publish OpenClaw to npm dist-tag alpha." >&2 + exit 1 + fi if [[ "${RELEASE_TAG}" == *"-beta."* && "${RELEASE_NPM_DIST_TAG}" != "beta" ]]; then echo "Beta prerelease tags must publish OpenClaw to npm dist-tag beta." >&2 exit 1 diff --git a/docs/ci.md b/docs/ci.md index 02683e60b04..bc84c81c7a1 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -239,7 +239,7 @@ Use `Package Acceptance` when the question is "does this installable OpenClaw pa ### Candidate sources -- `source=npm` accepts only `openclaw@beta`, `openclaw@latest`, or an exact OpenClaw release version such as `openclaw@2026.4.27-beta.2`. Use this for published beta/stable acceptance. +- `source=npm` accepts only `openclaw@alpha`, `openclaw@beta`, `openclaw@latest`, or an exact OpenClaw release version such as `openclaw@2026.4.27-beta.2`. Use this for published prerelease/stable acceptance. - `source=ref` packs a trusted `package_ref` branch, tag, or full commit SHA. The resolver fetches OpenClaw branches/tags, verifies the selected commit is reachable from repository branch history or a release tag, installs deps in a detached worktree, and packs it with `scripts/package-openclaw-for-docker.mjs`. - `source=url` downloads an HTTPS `.tgz`; `package_sha256` is required. - `source=artifact` downloads one `.tgz` from `artifact_run_id` and `artifact_name`; `package_sha256` is optional but should be supplied for externally shared artifacts. diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index 94954a293e5..2f6a97e6f77 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -7,9 +7,10 @@ read_when: - Looking for version naming and cadence --- -OpenClaw has three public release lanes: +OpenClaw has four public release lanes: - stable: tagged releases that publish to npm `beta` by default, or to npm `latest` when explicitly requested +- alpha: prerelease tags that publish to npm `alpha` - beta: prerelease tags that publish to npm `beta` - dev: the moving head of `main` @@ -19,10 +20,13 @@ OpenClaw has three public release lanes: - Git tag: `vYYYY.M.D` - Stable correction release version: `YYYY.M.D-N` - Git tag: `vYYYY.M.D-N` +- Alpha prerelease version: `YYYY.M.D-alpha.N` + - Git tag: `vYYYY.M.D-alpha.N` - Beta prerelease version: `YYYY.M.D-beta.N` - Git tag: `vYYYY.M.D-beta.N` - Do not zero-pad month or day - `latest` means the current promoted stable npm release +- `alpha` means the current alpha install target - `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 - Every stable OpenClaw release ships the npm package and macOS app together; @@ -75,14 +79,15 @@ 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 `OpenClaw Release Publish` from +9. For alpha or beta, tag `vYYYY.M.D-alpha.N` or `vYYYY.M.D-beta.N`, then run `OpenClaw Release Publish` from the matching `release/YYYY.M.D` branch. It verifies `pnpm plugins:sync:check`, publishes all publishable plugin packages to npm first, publishes the same set to ClawHub second, and then promotes the prepared OpenClaw npm preflight - artifact with dist-tag `beta`. After publish, run post-publish package - acceptance against the published `openclaw@YYYY.M.D-beta.N` or `openclaw@beta` - package. If a pushed or published beta needs a fix, cut the next `-beta.N`; - do not delete or rewrite the old beta. + artifact with the matching dist-tag. After publish, run post-publish package + acceptance against the published `openclaw@YYYY.M.D-alpha.N`, `openclaw@alpha`, + `openclaw@YYYY.M.D-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. 10. For stable, continue only after the vetted beta or release candidate has the required validation evidence. Stable npm publish also goes through `OpenClaw Release Publish`, reusing the successful preflight artifact via @@ -124,7 +129,7 @@ the maintainer-only release runbook. `gh workflow run full-release-validation.yml --ref main -f ref=release/YYYY.M.D` - 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` + `openclaw@alpha`, `openclaw@beta`, `openclaw@latest`, or an exact release version; `source=ref` to pack a trusted `package_ref` branch/tag/SHA with the current `workflow_ref` harness; `source=url` for an HTTPS tarball with a required SHA-256; or `source=artifact` for a tarball uploaded by another GitHub @@ -548,6 +553,16 @@ gh workflow run openclaw-release-publish.yml \ -f npm_dist_tag=beta ``` +Alpha publish example: + +```bash +gh workflow run openclaw-release-publish.yml \ + --ref release/YYYY.M.D \ + -f tag=vYYYY.M.D-alpha.N \ + -f preflight_run_id= \ + -f npm_dist_tag=alpha +``` + Stable publish to the default beta dist-tag: ```bash @@ -579,7 +594,7 @@ OpenClaw package must not be published. `OpenClaw NPM Release` accepts these operator-controlled inputs: - `tag`: required release tag such as `v2026.4.2`, `v2026.4.2-1`, or - `v2026.4.2-beta.1`; when `preflight_only=true`, it may also be the current + `v2026.4.2-alpha.1` or `v2026.4.2-beta.1`; when `preflight_only=true`, it may also be the current full 40-character workflow-branch commit SHA for validation-only preflight - `preflight_only`: `true` for validation/build/package only, `false` for the real publish path @@ -609,6 +624,7 @@ OpenClaw package must not be published. Rules: - Stable and correction tags may publish to either `beta` or `latest` +- Alpha prerelease tags may publish only to `alpha` - Beta prerelease tags may publish only to `beta` - For `OpenClaw NPM Release`, full commit SHA input is allowed only when `preflight_only=true` diff --git a/scripts/e2e/lib/upgrade-survivor/run.sh b/scripts/e2e/lib/upgrade-survivor/run.sh index 8a7328641fe..250c0d91e76 100644 --- a/scripts/e2e/lib/upgrade-survivor/run.sh +++ b/scripts/e2e/lib/upgrade-survivor/run.sh @@ -70,10 +70,10 @@ rm -f "$SUMMARY_JSON" "$CONFIG_COVERAGE_JSON" validate_baseline_package_spec() { local spec="$1" - if [[ "$spec" =~ ^openclaw@(beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-beta\.[1-9][0-9]*)?)$ ]]; then + if [[ "$spec" =~ ^openclaw@(alpha|beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-(alpha|beta)\.[1-9][0-9]*)?)$ ]]; then return 0 fi - echo "OPENCLAW_UPGRADE_SURVIVOR_BASELINE must be openclaw@latest, openclaw@beta, an exact OpenClaw release version, or a bare release version; got: $spec" >&2 + echo "OPENCLAW_UPGRADE_SURVIVOR_BASELINE must be openclaw@latest, openclaw@beta, openclaw@alpha, an exact OpenClaw release version, or a bare release version; got: $spec" >&2 return 1 } @@ -98,12 +98,12 @@ normalize_baseline() { ;; esac case "$baseline_version" in - latest | beta) + latest | beta | alpha) baseline_version="" baseline_version_expected="0" ;; dev | main | "") - echo "OPENCLAW_UPGRADE_SURVIVOR_BASELINE must be openclaw@latest, openclaw@beta, openclaw@, or a bare version" >&2 + echo "OPENCLAW_UPGRADE_SURVIVOR_BASELINE must be openclaw@latest, openclaw@beta, openclaw@alpha, openclaw@, or a bare version" >&2 return 1 ;; *) diff --git a/scripts/e2e/npm-telegram-live-docker.sh b/scripts/e2e/npm-telegram-live-docker.sh index 14ffa1e81af..af373f8d9f4 100755 --- a/scripts/e2e/npm-telegram-live-docker.sh +++ b/scripts/e2e/npm-telegram-live-docker.sh @@ -41,10 +41,10 @@ resolve_credential_role() { validate_openclaw_package_spec() { local spec="$1" - if [[ "$spec" =~ ^openclaw@(beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-beta\.[1-9][0-9]*)?)$ ]]; then + if [[ "$spec" =~ ^openclaw@(alpha|beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-(alpha|beta)\.[1-9][0-9]*)?)$ ]]; then return 0 fi - echo "OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC must be openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: $spec" >&2 + echo "OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC must be openclaw@alpha, openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: $spec" >&2 exit 1 } diff --git a/scripts/e2e/npm-telegram-rtt-docker.sh b/scripts/e2e/npm-telegram-rtt-docker.sh index e730a6e7632..0370cdc3a82 100755 --- a/scripts/e2e/npm-telegram-rtt-docker.sh +++ b/scripts/e2e/npm-telegram-rtt-docker.sh @@ -13,10 +13,10 @@ OUTPUT_DIR="${OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR:-.artifacts/qa-e2e/npm-telegram-r validate_openclaw_package_spec() { local spec="$1" - if [[ "$spec" =~ ^openclaw@(main|beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-beta\.[1-9][0-9]*)?)$ ]]; then + if [[ "$spec" =~ ^openclaw@(main|alpha|beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-(alpha|beta)\.[1-9][0-9]*)?)$ ]]; then return 0 fi - echo "OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC must be openclaw@main, openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: $spec" >&2 + echo "OPENCLAW_NPM_TELEGRAM_PACKAGE_SPEC must be openclaw@main, openclaw@alpha, openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: $spec" >&2 exit 1 } diff --git a/scripts/lib/docker-e2e-plan.mjs b/scripts/lib/docker-e2e-plan.mjs index 4f0008e67b7..fbe229b7637 100644 --- a/scripts/lib/docker-e2e-plan.mjs +++ b/scripts/lib/docker-e2e-plan.mjs @@ -91,12 +91,14 @@ export function normalizeUpgradeSurvivorBaselineSpec(raw) { } const spec = value.startsWith("openclaw@") ? value : `openclaw@${value}`; if ( - !/^openclaw@(?:beta|latest|[0-9]{4}\.[0-9]+\.[0-9]+(?:-(?:[0-9]+|beta\.[0-9]+))?)$/u.test(spec) + !/^openclaw@(?:alpha|beta|latest|[0-9]{4}\.[0-9]+\.[0-9]+(?:-(?:[0-9]+|alpha\.[0-9]+|beta\.[0-9]+))?)$/u.test( + spec, + ) ) { throw new Error( `invalid published upgrade survivor baseline: ${JSON.stringify( value, - )}. Expected openclaw@latest, openclaw@beta, or openclaw@YYYY.M.D.`, + )}. Expected openclaw@latest, openclaw@beta, openclaw@alpha, or openclaw@YYYY.M.D.`, ); } return spec; diff --git a/scripts/lib/ios-version.ts b/scripts/lib/ios-version.ts index 038c4871a90..b5e3293d812 100644 --- a/scripts/lib/ios-version.ts +++ b/scripts/lib/ios-version.ts @@ -7,7 +7,7 @@ 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})(?:-(?:beta\.\d+|\d+))?$/u; +const GATEWAY_VERSION_PATTERN = /^(\d{4}\.\d{1,2}\.\d{1,2})(?:-(?:alpha\.\d+|beta\.\d+|\d+))?$/u; type IosVersionManifest = { version: string; @@ -52,7 +52,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-beta.N, or YYYY.M.D-N.`, + `Invalid gateway version '${rawVersion}'. Expected YYYY.M.D, YYYY.M.D-alpha.N, YYYY.M.D-beta.N, or YYYY.M.D-N.`, ); } diff --git a/scripts/lib/npm-publish-plan.mjs b/scripts/lib/npm-publish-plan.mjs index df9880a0dd7..9fa24e0d750 100644 --- a/scripts/lib/npm-publish-plan.mjs +++ b/scripts/lib/npm-publish-plan.mjs @@ -1,4 +1,6 @@ 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*)$/; const BETA_VERSION_REGEX = /^(?\d{4})\.(?[1-9]\d?)\.(?[1-9]\d?)-beta\.(?[1-9]\d*)$/; const CORRECTION_VERSION_REGEX = @@ -8,10 +10,11 @@ const CORRECTION_VERSION_REGEX = * @typedef {object} ParsedReleaseVersion * @property {string} version * @property {string} baseVersion - * @property {"stable" | "beta"} channel + * @property {"stable" | "alpha" | "beta"} channel * @property {number} year * @property {number} month * @property {number} day + * @property {number | undefined} [alphaNumber] * @property {number | undefined} [betaNumber] * @property {number | undefined} [correctionNumber] * @property {Date} date @@ -19,9 +22,9 @@ const CORRECTION_VERSION_REGEX = /** * @typedef {object} NpmPublishPlan - * @property {"stable" | "beta"} channel - * @property {"latest" | "beta"} publishTag - * @property {("latest" | "beta")[]} mirrorDistTags + * @property {"stable" | "alpha" | "beta"} channel + * @property {"latest" | "alpha" | "beta"} publishTag + * @property {("latest" | "alpha" | "beta")[]} mirrorDistTags */ /** @@ -37,13 +40,14 @@ const CORRECTION_VERSION_REGEX = /** * @param {string} version * @param {Record} groups - * @param {"stable" | "beta"} channel + * @param {"stable" | "alpha" | "beta"} channel * @returns {ParsedReleaseVersion | null} */ function parseDateParts(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 alphaNumber = channel === "alpha" ? Number.parseInt(groups.alpha ?? "", 10) : undefined; const betaNumber = channel === "beta" ? Number.parseInt(groups.beta ?? "", 10) : undefined; if ( @@ -60,6 +64,9 @@ function parseDateParts(version, groups, channel) { if (channel === "beta" && (!Number.isInteger(betaNumber) || (betaNumber ?? 0) < 1)) { return null; } + if (channel === "alpha" && (!Number.isInteger(alphaNumber) || (alphaNumber ?? 0) < 1)) { + return null; + } const date = new Date(Date.UTC(year, month - 1, day)); if ( @@ -77,6 +84,7 @@ function parseDateParts(version, groups, channel) { year, month, day, + alphaNumber, betaNumber, date, }; @@ -97,6 +105,11 @@ export function parseReleaseVersion(version) { return parseDateParts(trimmed, stableMatch.groups, "stable"); } + const alphaMatch = ALPHA_VERSION_REGEX.exec(trimmed); + if (alphaMatch?.groups) { + return parseDateParts(trimmed, alphaMatch.groups, "alpha"); + } + const betaMatch = BETA_VERSION_REGEX.exec(trimmed); if (betaMatch?.groups) { return parseDateParts(trimmed, betaMatch.groups, "beta"); @@ -137,7 +150,12 @@ export function compareReleaseVersions(left, right) { } if (parsedLeft.channel !== parsedRight.channel) { - return parsedLeft.channel === "stable" ? 1 : -1; + const rank = { alpha: 0, beta: 1, stable: 2 }; + return Math.sign(rank[parsedLeft.channel] - rank[parsedRight.channel]); + } + + if (parsedLeft.channel === "alpha" && parsedRight.channel === "alpha") { + return Math.sign((parsedLeft.alphaNumber ?? 0) - (parsedRight.alphaNumber ?? 0)); } if (parsedLeft.channel === "beta" && parsedRight.channel === "beta") { @@ -165,6 +183,13 @@ export function resolveNpmPublishPlan(version, currentBetaVersion) { mirrorDistTags: [], }; } + if (parsedVersion.channel === "alpha") { + return { + channel: "alpha", + publishTag: "alpha", + mirrorDistTags: [], + }; + } const normalizedCurrentBeta = currentBetaVersion?.trim(); if (normalizedCurrentBeta) { diff --git a/scripts/lib/plugin-clawhub-release.ts b/scripts/lib/plugin-clawhub-release.ts index ce58fde7dd8..04fc5fa88c8 100644 --- a/scripts/lib/plugin-clawhub-release.ts +++ b/scripts/lib/plugin-clawhub-release.ts @@ -46,8 +46,8 @@ export type PublishablePluginPackage = { packageDir: string; packageName: string; version: string; - channel: "stable" | "beta"; - publishTag: "latest" | "beta"; + channel: "stable" | "alpha" | "beta"; + publishTag: "latest" | "alpha" | "beta"; }; type PluginReleasePlanItem = PublishablePluginPackage & { @@ -154,7 +154,12 @@ export function collectClawHubPublishablePluginPackages( packageName, version, channel: parsedVersion.channel, - publishTag: parsedVersion.channel === "beta" ? "beta" : "latest", + publishTag: + parsedVersion.channel === "alpha" + ? "alpha" + : parsedVersion.channel === "beta" + ? "beta" + : "latest", }); } diff --git a/scripts/lib/plugin-npm-release.ts b/scripts/lib/plugin-npm-release.ts index cec24caa965..ca5d57a439c 100644 --- a/scripts/lib/plugin-npm-release.ts +++ b/scripts/lib/plugin-npm-release.ts @@ -34,8 +34,8 @@ export type PublishablePluginPackage = { packageDir: string; packageName: string; version: string; - channel: "stable" | "beta"; - publishTag: "latest" | "beta"; + channel: "stable" | "alpha" | "beta"; + publishTag: "latest" | "alpha" | "beta"; installNpmSpec?: string; }; @@ -117,7 +117,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, or YYYY.M.D-beta.N; found "${version}".`, + `${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}".`, ); return null; } @@ -244,7 +244,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, or YYYY.M.D-beta.N; found "${packageVersion}".`, + `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}".`, ); } if (!Array.isArray(extensions) || extensions.length === 0) { diff --git a/scripts/lib/rtt-harness.ts b/scripts/lib/rtt-harness.ts index f061a4cee8e..32543e11acf 100644 --- a/scripts/lib/rtt-harness.ts +++ b/scripts/lib/rtt-harness.ts @@ -64,7 +64,7 @@ type TelegramQaSummary = { }; const OPENCLAW_PACKAGE_SPEC_RE = - /^openclaw@(main|beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-beta\.[1-9][0-9]*)?)$/u; + /^openclaw@(main|alpha|beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-(alpha|beta)\.[1-9][0-9]*)?)$/u; const REQUIRED_TELEGRAM_ENV = [ "OPENCLAW_QA_TELEGRAM_GROUP_ID", @@ -75,7 +75,7 @@ const REQUIRED_TELEGRAM_ENV = [ export function validateOpenClawPackageSpec(spec: string) { if (!OPENCLAW_PACKAGE_SPEC_RE.test(spec)) { throw new Error( - `Package spec must be openclaw@main, openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${spec}`, + `Package spec must be openclaw@main, openclaw@alpha, openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${spec}`, ); } return spec; diff --git a/scripts/make_appcast.sh b/scripts/make_appcast.sh index 2c999eac820..bab60fa88c3 100755 --- a/scripts/make_appcast.sh +++ b/scripts/make_appcast.sh @@ -29,7 +29,7 @@ ZIP_NAME=$(basename "$ZIP") ZIP_BASE="${ZIP_NAME%.zip}" VERSION=${SPARKLE_RELEASE_VERSION:-} if [[ -z "$VERSION" ]]; then - # Accept legacy calver suffixes like -1 and prerelease forms like -beta.1 / .beta.1. + # Accept legacy calver suffixes like -1 and prerelease forms like -alpha.1 / -beta.1 / .beta.1. if [[ "$ZIP_NAME" =~ ^OpenClaw-([0-9]+(\.[0-9]+){1,2}([-.][0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?)\.zip$ ]]; then VERSION="${BASH_REMATCH[1]}" else diff --git a/scripts/openclaw-cross-os-release-checks.ts b/scripts/openclaw-cross-os-release-checks.ts index 12f10003e44..a78aeb41c23 100644 --- a/scripts/openclaw-cross-os-release-checks.ts +++ b/scripts/openclaw-cross-os-release-checks.ts @@ -184,7 +184,7 @@ function parseBooleanEnv(name, fallback) { export function looksLikeReleaseVersionRef(ref) { const trimmed = normalizeRequestedRef(ref); - return /^v?[0-9]{4}\.[0-9]+\.[0-9]+(?:-(?:[1-9][0-9]*)|[-.](?:beta|rc)[-.]?[0-9]+)?$/iu.test( + return /^v?[0-9]{4}\.[0-9]+\.[0-9]+(?:-(?:[1-9][0-9]*)|[-.](?:alpha|beta|rc)[-.]?[0-9]+)?$/iu.test( trimmed, ); } diff --git a/scripts/openclaw-npm-publish.sh b/scripts/openclaw-npm-publish.sh index f6d4cff5ead..cad512195ae 100644 --- a/scripts/openclaw-npm-publish.sh +++ b/scripts/openclaw-npm-publish.sh @@ -24,7 +24,11 @@ mapfile -t publish_plan < <( import { resolveNpmPublishPlan } from "./scripts/openclaw-npm-release-check.ts"; const requestedPublishTag = - process.env.REQUESTED_PUBLISH_TAG === "latest" ? "latest" : "beta"; + process.env.REQUESTED_PUBLISH_TAG === "latest" + ? "latest" + : process.env.REQUESTED_PUBLISH_TAG === "alpha" + ? "alpha" + : "beta"; const plan = resolveNpmPublishPlan(process.env.PACKAGE_VERSION ?? "", undefined, requestedPublishTag); console.log(plan.channel); console.log(plan.publishTag); diff --git a/scripts/openclaw-npm-release-check.ts b/scripts/openclaw-npm-release-check.ts index a9cf59493d2..b32574be439 100644 --- a/scripts/openclaw-npm-release-check.ts +++ b/scripts/openclaw-npm-release-check.ts @@ -32,10 +32,11 @@ type PackageJson = { export type ParsedReleaseVersion = { version: string; baseVersion: string; - channel: "stable" | "beta"; + channel: "stable" | "alpha" | "beta"; year: number; month: number; day: number; + alphaNumber?: number; betaNumber?: number; correctionNumber?: number; date: Date; @@ -45,15 +46,15 @@ export type ParsedReleaseTag = { version: string; packageVersion: string; baseVersion: string; - channel: "stable" | "beta"; + channel: "stable" | "alpha" | "beta"; correctionNumber?: number; date: Date; }; export type NpmPublishPlan = { - channel: "stable" | "beta"; - publishTag: "latest" | "beta"; - mirrorDistTags: ("latest" | "beta")[]; + channel: "stable" | "alpha" | "beta"; + publishTag: "latest" | "alpha" | "beta"; + mirrorDistTags: ("latest" | "alpha" | "beta")[]; }; export type NpmDistTagMirrorAuth = { @@ -193,14 +194,30 @@ export function compareReleaseVersions(left: string, right: string): number | nu export function resolveNpmPublishPlan( version: string, _currentBetaVersion?: string | null, - requestedPublishTag?: "latest" | "beta" | null, + requestedPublishTag?: "latest" | "alpha" | "beta" | null, ): NpmPublishPlan { const parsedVersion = parseReleaseVersion(version); if (parsedVersion === null) { throw new Error(`Unsupported release version "${version}".`); } - const publishTag = requestedPublishTag?.trim() === "latest" ? "latest" : "beta"; + const publishTag = + requestedPublishTag?.trim() === "latest" + ? "latest" + : requestedPublishTag?.trim() === "alpha" + ? "alpha" + : "beta"; + + if (parsedVersion.channel === "alpha") { + if (publishTag !== "alpha") { + throw new Error("Alpha prereleases must publish to the alpha dist-tag."); + } + return { + channel: "alpha", + publishTag: "alpha", + mirrorDistTags: [], + }; + } if (parsedVersion.channel === "beta") { if (publishTag !== "beta") { @@ -336,7 +353,7 @@ export function collectReleaseTagErrors(params: { const parsedVersion = parseReleaseVersion(packageVersion); if (parsedVersion === null) { errors.push( - `package.json version must match YYYY.M.D, YYYY.M.D-N, or YYYY.M.D-beta.N; found "${packageVersion || ""}".`, + `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 || ""}".`, ); } @@ -348,7 +365,7 @@ export function collectReleaseTagErrors(params: { const parsedTag = parseReleaseTagVersion(tagVersion); if (parsedTag === null) { errors.push( - `Release tag must match vYYYY.M.D, vYYYY.M.D-beta.N, or fallback correction tag vYYYY.M.D-N; found "${releaseTag || ""}".`, + `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 || ""}".`, ); } diff --git a/scripts/resolve-openclaw-package-candidate.mjs b/scripts/resolve-openclaw-package-candidate.mjs index d5d4331ad13..62ad7008c9f 100644 --- a/scripts/resolve-openclaw-package-candidate.mjs +++ b/scripts/resolve-openclaw-package-candidate.mjs @@ -12,7 +12,7 @@ import { fileURLToPath } from "node:url"; const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const DEFAULT_OUTPUT_NAME = "openclaw-current.tgz"; export const OPENCLAW_PACKAGE_SPEC_RE = - /^openclaw@(beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-beta\.[1-9][0-9]*)?)$/u; + /^openclaw@(alpha|beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-(alpha|beta)\.[1-9][0-9]*)?)$/u; function usage() { return `Usage: node scripts/resolve-openclaw-package-candidate.mjs --source --output-dir [options] @@ -82,7 +82,7 @@ export function parseArgs(argv) { export function validateOpenClawPackageSpec(spec) { if (!OPENCLAW_PACKAGE_SPEC_RE.test(spec)) { throw new Error( - `package_spec must be openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${spec}`, + `package_spec must be openclaw@alpha, openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${spec}`, ); } } diff --git a/test/npm-publish-plan.test.ts b/test/npm-publish-plan.test.ts index 6a9a331b112..05e6b1f8c81 100644 --- a/test/npm-publish-plan.test.ts +++ b/test/npm-publish-plan.test.ts @@ -45,6 +45,16 @@ describe("shouldRequireNpmDistTagMirrorAuth", () => { ).toBe(false); }); + it("publishes alpha prereleases without dist-tag mirroring", () => { + const plan = resolveNpmPublishPlan("2026.4.1-alpha.1"); + + expect(plan).toEqual({ + channel: "alpha", + publishTag: "alpha", + mirrorDistTags: [], + }); + }); + it("does not require auth when a publish already has npm auth", () => { const plan = resolveNpmPublishPlan("2026.4.1"); const auth = resolveNpmDistTagMirrorAuth({ npmToken: "token" }); diff --git a/test/openclaw-npm-release-check.test.ts b/test/openclaw-npm-release-check.test.ts index 4b86971345e..1e15f3aa997 100644 --- a/test/openclaw-npm-release-check.test.ts +++ b/test/openclaw-npm-release-check.test.ts @@ -54,6 +54,18 @@ describe("parseReleaseVersion", () => { }); }); + it("parses alpha CalVer releases", () => { + expect(parseReleaseVersion("2026.3.10-alpha.2")).toMatchObject({ + version: "2026.3.10-alpha.2", + baseVersion: "2026.3.10", + channel: "alpha", + year: 2026, + month: 3, + day: 10, + alphaNumber: 2, + }); + }); + it("parses stable correction releases", () => { expect(parseReleaseVersion("2026.3.10-1")).toMatchObject({ version: "2026.3.10-1", @@ -101,6 +113,14 @@ describe("resolveNpmPublishPlan", () => { }); }); + it("publishes alpha prereleases to alpha only", () => { + expect(resolveNpmPublishPlan("2026.3.29-alpha.2", undefined, "alpha")).toEqual({ + channel: "alpha", + publishTag: "alpha", + mirrorDistTags: [], + }); + }); + it("publishes stable releases to beta first", () => { expect(resolveNpmPublishPlan("2026.3.29")).toEqual({ channel: "stable", @@ -138,6 +158,15 @@ describe("resolveNpmPublishPlan", () => { "Beta prereleases must publish to the beta dist-tag.", ); }); + + it("rejects publishing alpha prereleases to beta or latest", () => { + expect(() => resolveNpmPublishPlan("2026.3.29-alpha.2")).toThrow( + "Alpha prereleases must publish to the alpha dist-tag.", + ); + expect(() => resolveNpmPublishPlan("2026.3.29-alpha.2", undefined, "latest")).toThrow( + "Alpha prereleases must publish to the alpha dist-tag.", + ); + }); }); describe("resolveNpmDistTagMirrorAuth", () => { @@ -205,6 +234,10 @@ describe("compareReleaseVersions", () => { expect(compareReleaseVersions("2026.3.29", "2026.3.29-beta.2")).toBe(1); }); + it("orders alpha before beta on the same day", () => { + 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", () => { expect(compareReleaseVersions("2026.4.1-beta.1", "2026.3.29")).toBe(1); }); diff --git a/test/plugin-npm-release.test.ts b/test/plugin-npm-release.test.ts index ac839ed0163..90fe482d3bb 100644 --- a/test/plugin-npm-release.test.ts +++ b/test/plugin-npm-release.test.ts @@ -134,7 +134,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, or YYYY.M.D-beta.N; found "latest".', + '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".', "openclaw.extensions must contain only non-empty strings.", "openclaw.install.npmSpec must be a non-empty string for publishable plugins.", ]); @@ -314,6 +314,37 @@ describe("collectPublishablePluginPackages", () => { }), ).toEqual([]); }); + + it("publishes alpha plugin packages to the alpha dist-tag", () => { + const repoDir = makeTempRepoRoot(tempDirs, "openclaw-plugin-npm-release-"); + mkdirSync(join(repoDir, "extensions", "demo-plugin"), { recursive: true }); + writeJsonFile(join(repoDir, "extensions", "demo-plugin", "package.json"), { + name: "@openclaw/demo-plugin", + version: "2026.4.10-alpha.1", + repository: { + type: "git", + url: OPENCLAW_PLUGIN_NPM_REPOSITORY_URL, + }, + openclaw: { + extensions: ["./index.ts"], + install: { + npmSpec: "@openclaw/demo-plugin", + }, + release: { + publishToNpm: true, + }, + }, + }); + + expect(collectPublishablePluginPackages(repoDir)).toEqual([ + expect.objectContaining({ + channel: "alpha", + packageName: "@openclaw/demo-plugin", + publishTag: "alpha", + version: "2026.4.10-alpha.1", + }), + ]); + }); }); describe("resolveSelectedPublishablePluginPackages", () => { diff --git a/test/scripts/ios-version.test.ts b/test/scripts/ios-version.test.ts index 417fd31d258..7bd2a214259 100644 --- a/test/scripts/ios-version.test.ts +++ b/test/scripts/ios-version.test.ts @@ -55,6 +55,10 @@ describe("gateway version normalization", () => { expect(normalizeGatewayVersionToPinnedIosVersion("2026.4.6-beta.2")).toBe("2026.4.6"); }); + it("strips alpha suffixes when pinning from gateway version", () => { + expect(normalizeGatewayVersionToPinnedIosVersion("2026.4.6-alpha.2")).toBe("2026.4.6"); + }); + it("strips fallback correction suffixes when pinning from gateway version", () => { expect(normalizeGatewayVersionToPinnedIosVersion("2026.4.6-3")).toBe("2026.4.6"); }); diff --git a/test/scripts/openclaw-cross-os-release-checks.test.ts b/test/scripts/openclaw-cross-os-release-checks.test.ts index 8160b76feca..6df4bef54db 100644 --- a/test/scripts/openclaw-cross-os-release-checks.test.ts +++ b/test/scripts/openclaw-cross-os-release-checks.test.ts @@ -233,6 +233,8 @@ describe("scripts/openclaw-cross-os-release-checks", () => { expect(looksLikeReleaseVersionRef("2026.4.5")).toBe(true); expect(looksLikeReleaseVersionRef("refs/tags/v2026.4.5-beta.1")).toBe(true); expect(looksLikeReleaseVersionRef("v2026.4.5-beta.1")).toBe(true); + expect(looksLikeReleaseVersionRef("refs/tags/v2026.4.5-alpha.1")).toBe(true); + expect(looksLikeReleaseVersionRef("v2026.4.5-alpha.1")).toBe(true); expect(looksLikeReleaseVersionRef("v2026.4.7-1")).toBe(true); expect(looksLikeReleaseVersionRef("main")).toBe(false); expect(looksLikeReleaseVersionRef("codex/cross-os-release-checks")).toBe(false); diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index b961ddcd9a5..a618058b25f 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -228,7 +228,7 @@ describe("package artifact reuse", () => { expect(scheduler).toContain('["OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS",'); expect(packageJson).toContain("OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1"); expect(publishedUpgradeSurvivor).toContain("validate_baseline_package_spec"); - expect(publishedUpgradeSurvivor).toContain("openclaw@(beta|latest|"); + expect(publishedUpgradeSurvivor).toContain("openclaw@(alpha|beta|latest|"); expect(publishedUpgradeSurvivor).toContain("plugin_deps_cleanup_plugin_dirs"); expect(publishedUpgradeSurvivor).toContain('"$(package_root)/extensions/$plugin"'); expect(publishedUpgradeSurvivor).toContain("probe_gateway_endpoint"); @@ -623,7 +623,7 @@ describe("package artifact reuse", () => { }); expectTextToIncludeAll(validateStep.run, [ 'if [[ -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then', - "package_spec must be openclaw@beta", + "package_spec must be openclaw@alpha", ]); expectTextToIncludeAll(runStep.run, [ 'export OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ="${package_tgzs[0]}"', diff --git a/test/scripts/resolve-openclaw-package-candidate.test.ts b/test/scripts/resolve-openclaw-package-candidate.test.ts index ad573f8d221..5da991e5d91 100644 --- a/test/scripts/resolve-openclaw-package-candidate.test.ts +++ b/test/scripts/resolve-openclaw-package-candidate.test.ts @@ -11,25 +11,27 @@ import { describe("resolve-openclaw-package-candidate", () => { it("accepts only OpenClaw release package specs for npm candidates", () => { expect(() => validateOpenClawPackageSpec("openclaw@beta")).not.toThrow(); + expect(() => validateOpenClawPackageSpec("openclaw@alpha")).not.toThrow(); expect(() => validateOpenClawPackageSpec("openclaw@latest")).not.toThrow(); expect(() => validateOpenClawPackageSpec("openclaw@2026.4.27")).not.toThrow(); expect(() => validateOpenClawPackageSpec("openclaw@2026.4.27-1")).not.toThrow(); expect(() => validateOpenClawPackageSpec("openclaw@2026.4.27-beta.2")).not.toThrow(); + expect(() => validateOpenClawPackageSpec("openclaw@2026.4.27-alpha.2")).not.toThrow(); expect(() => validateOpenClawPackageSpec("@evil/openclaw@1.0.0")).toThrow( - "package_spec must be openclaw@beta", + "package_spec must be openclaw@alpha", ); expect(() => validateOpenClawPackageSpec("openclaw@canary")).toThrow( - "package_spec must be openclaw@beta", + "package_spec must be openclaw@alpha", ); expect(() => validateOpenClawPackageSpec("openclaw@2026.04.27")).toThrow( - "package_spec must be openclaw@beta", + "package_spec must be openclaw@alpha", ); expect(() => validateOpenClawPackageSpec("openclaw@npm:other-package")).toThrow( - "package_spec must be openclaw@beta", + "package_spec must be openclaw@alpha", ); expect(() => validateOpenClawPackageSpec("openclaw@file:../other-package.tgz")).toThrow( - "package_spec must be openclaw@beta", + "package_spec must be openclaw@alpha", ); }); diff --git a/test/scripts/rtt-harness.test.ts b/test/scripts/rtt-harness.test.ts index 7ff01565eff..1b7db1f66e9 100644 --- a/test/scripts/rtt-harness.test.ts +++ b/test/scripts/rtt-harness.test.ts @@ -21,12 +21,16 @@ const FIXTURE_PATH = path.resolve(TEST_DIR, "../fixtures/telegram-qa-summary-rtt describe("RTT harness", () => { it("validates OpenClaw package specs", () => { expect(validateOpenClawPackageSpec("openclaw@main")).toBe("openclaw@main"); + expect(validateOpenClawPackageSpec("openclaw@alpha")).toBe("openclaw@alpha"); expect(validateOpenClawPackageSpec("openclaw@beta")).toBe("openclaw@beta"); expect(validateOpenClawPackageSpec("openclaw@latest")).toBe("openclaw@latest"); expect(validateOpenClawPackageSpec("openclaw@2026.4.30")).toBe("openclaw@2026.4.30"); expect(validateOpenClawPackageSpec("openclaw@2026.4.30-beta.2")).toBe( "openclaw@2026.4.30-beta.2", ); + expect(validateOpenClawPackageSpec("openclaw@2026.4.30-alpha.2")).toBe( + "openclaw@2026.4.30-alpha.2", + ); expect(() => validateOpenClawPackageSpec("@openclaw/openclaw@beta")).toThrow( /Package spec must be/,