feat: support alpha releases

This commit is contained in:
Peter Steinberger
2026-05-02 18:29:01 +01:00
parent 831958c5d4
commit bb294bcd20
29 changed files with 237 additions and 68 deletions

View File

@@ -38,7 +38,7 @@ jobs:
RELEASE_TAG: ${{ inputs.tag }} RELEASE_TAG: ${{ inputs.tag }}
run: | run: |
set -euo pipefail 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}" echo "Invalid release tag: ${RELEASE_TAG}"
exit 1 exit 1
fi fi

View File

@@ -4,7 +4,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
tag: 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 required: true
type: string type: string
preflight_only: preflight_only:
@@ -38,7 +38,7 @@ jobs:
RELEASE_TAG: ${{ inputs.tag }} RELEASE_TAG: ${{ inputs.tag }}
run: | run: |
set -euo pipefail 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}" echo "Invalid release tag format: ${RELEASE_TAG}"
exit 1 exit 1
fi fi

View File

@@ -152,8 +152,8 @@ jobs:
set -euo pipefail set -euo pipefail
if [[ -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then 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 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@beta, openclaw@latest, or an exact OpenClaw release version; got: ${PACKAGE_SPEC}" >&2 echo "package_spec must be openclaw@alpha, openclaw@beta, openclaw@latest, or an exact OpenClaw release version; got: ${PACKAGE_SPEC}" >&2
exit 1 exit 1
fi fi
fi fi

View File

@@ -17,11 +17,12 @@ on:
required: false required: false
type: string type: string
npm_dist_tag: npm_dist_tag:
description: npm dist-tag to publish to for stable releases description: npm dist-tag to publish to
required: true required: true
default: beta default: beta
type: choice type: choice
options: options:
- alpha
- beta - beta
- latest - latest
@@ -54,7 +55,7 @@ jobs:
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }} RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
run: | run: |
set -euo pipefail 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}" echo "Invalid release ref format: ${RELEASE_REF}"
exit 1 exit 1
fi fi
@@ -62,6 +63,10 @@ jobs:
echo "Full commit SHA input is only supported for validation-only preflight runs." echo "Full commit SHA input is only supported for validation-only preflight runs."
exit 1 exit 1
fi 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 if [[ "${RELEASE_REF}" == *"-beta."* && "${RELEASE_NPM_DIST_TAG}" != "beta" ]]; then
echo "Beta prerelease tags must publish to npm dist-tag beta." echo "Beta prerelease tags must publish to npm dist-tag beta."
exit 1 exit 1
@@ -294,10 +299,14 @@ jobs:
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }} RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
run: | run: |
set -euo pipefail 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}" echo "Invalid release tag format: ${RELEASE_TAG}"
exit 1 exit 1
fi 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 if [[ "${RELEASE_TAG}" == *"-beta."* && "${RELEASE_NPM_DIST_TAG}" != "beta" ]]; then
echo "Beta prerelease tags must publish to npm dist-tag beta." echo "Beta prerelease tags must publish to npm dist-tag beta."
exit 1 exit 1

View File

@@ -4,7 +4,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
tag: 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 required: true
type: string type: string
preflight_run_id: preflight_run_id:
@@ -17,6 +17,7 @@ on:
default: beta default: beta
type: choice type: choice
options: options:
- alpha
- beta - beta
- latest - latest
plugin_publish_scope: plugin_publish_scope:
@@ -69,10 +70,14 @@ jobs:
WORKFLOW_REF: ${{ github.ref }} WORKFLOW_REF: ${{ github.ref }}
run: | run: |
set -euo pipefail 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 echo "Invalid release tag: ${RELEASE_TAG}" >&2
exit 1 exit 1
fi 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 if [[ "${RELEASE_TAG}" == *"-beta."* && "${RELEASE_NPM_DIST_TAG}" != "beta" ]]; then
echo "Beta prerelease tags must publish OpenClaw to npm dist-tag beta." >&2 echo "Beta prerelease tags must publish OpenClaw to npm dist-tag beta." >&2
exit 1 exit 1

View File

@@ -239,7 +239,7 @@ Use `Package Acceptance` when the question is "does this installable OpenClaw pa
### Candidate sources ### 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=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=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. - `source=artifact` downloads one `.tgz` from `artifact_run_id` and `artifact_name`; `package_sha256` is optional but should be supplied for externally shared artifacts.

View File

@@ -7,9 +7,10 @@ read_when:
- Looking for version naming and cadence - 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 - 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` - beta: prerelease tags that publish to npm `beta`
- dev: the moving head of `main` - dev: the moving head of `main`
@@ -19,10 +20,13 @@ OpenClaw has three public release lanes:
- Git tag: `vYYYY.M.D` - Git tag: `vYYYY.M.D`
- Stable correction release version: `YYYY.M.D-N` - Stable correction release version: `YYYY.M.D-N`
- Git tag: `vYYYY.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` - Beta prerelease version: `YYYY.M.D-beta.N`
- Git tag: `vYYYY.M.D-beta.N` - Git tag: `vYYYY.M.D-beta.N`
- Do not zero-pad month or day - Do not zero-pad month or day
- `latest` means the current promoted stable npm release - `latest` means the current promoted stable npm release
- `alpha` means the current alpha install target
- `beta` means the current beta 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 - 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; - 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 file, lane, workflow job, package profile, provider, or model allowlist that
proves the fix. Rerun the full umbrella only when the changed surface makes proves the fix. Rerun the full umbrella only when the changed surface makes
prior evidence stale. 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`, the matching `release/YYYY.M.D` branch. It verifies `pnpm plugins:sync:check`,
publishes all publishable plugin packages to npm first, publishes the same publishes all publishable plugin packages to npm first, publishes the same
set to ClawHub second, and then promotes the prepared OpenClaw npm preflight set to ClawHub second, and then promotes the prepared OpenClaw npm preflight
artifact with dist-tag `beta`. After publish, run post-publish package artifact with the matching dist-tag. After publish, run post-publish package
acceptance against the published `openclaw@YYYY.M.D-beta.N` or `openclaw@beta` acceptance against the published `openclaw@YYYY.M.D-alpha.N`, `openclaw@alpha`,
package. If a pushed or published beta needs a fix, cut the next `-beta.N`; `openclaw@YYYY.M.D-beta.N`, or `openclaw@beta` package. If a pushed or
do not delete or rewrite the old beta. 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 10. For stable, continue only after the vetted beta or release candidate has the
required validation evidence. Stable npm publish also goes through required validation evidence. Stable npm publish also goes through
`OpenClaw Release Publish`, reusing the successful preflight artifact via `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` `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 - Run the manual `Package Acceptance` workflow when you want side-channel proof
for a package candidate while release work continues. Use `source=npm` for 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 to pack a trusted `package_ref` branch/tag/SHA with the current
`workflow_ref` harness; `source=url` for an HTTPS tarball with a required `workflow_ref` harness; `source=url` for an HTTPS tarball with a required
SHA-256; or `source=artifact` for a tarball uploaded by another GitHub 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 -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=<successful-openclaw-npm-preflight-run-id> \
-f npm_dist_tag=alpha
```
Stable publish to the default beta dist-tag: Stable publish to the default beta dist-tag:
```bash ```bash
@@ -579,7 +594,7 @@ OpenClaw package must not be published.
`OpenClaw NPM Release` accepts these operator-controlled inputs: `OpenClaw NPM Release` accepts these operator-controlled inputs:
- `tag`: required release tag such as `v2026.4.2`, `v2026.4.2-1`, or - `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 full 40-character workflow-branch commit SHA for validation-only preflight
- `preflight_only`: `true` for validation/build/package only, `false` for the - `preflight_only`: `true` for validation/build/package only, `false` for the
real publish path real publish path
@@ -609,6 +624,7 @@ OpenClaw package must not be published.
Rules: Rules:
- Stable and correction tags may publish to either `beta` or `latest` - 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` - Beta prerelease tags may publish only to `beta`
- For `OpenClaw NPM Release`, full commit SHA input is allowed only when - For `OpenClaw NPM Release`, full commit SHA input is allowed only when
`preflight_only=true` `preflight_only=true`

View File

@@ -70,10 +70,10 @@ rm -f "$SUMMARY_JSON" "$CONFIG_COVERAGE_JSON"
validate_baseline_package_spec() { validate_baseline_package_spec() {
local spec="$1" 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 return 0
fi 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 return 1
} }
@@ -98,12 +98,12 @@ normalize_baseline() {
;; ;;
esac esac
case "$baseline_version" in case "$baseline_version" in
latest | beta) latest | beta | alpha)
baseline_version="" baseline_version=""
baseline_version_expected="0" baseline_version_expected="0"
;; ;;
dev | main | "") dev | main | "")
echo "OPENCLAW_UPGRADE_SURVIVOR_BASELINE must be openclaw@latest, openclaw@beta, openclaw@<version>, or a bare version" >&2 echo "OPENCLAW_UPGRADE_SURVIVOR_BASELINE must be openclaw@latest, openclaw@beta, openclaw@alpha, openclaw@<version>, or a bare version" >&2
return 1 return 1
;; ;;
*) *)

View File

@@ -41,10 +41,10 @@ resolve_credential_role() {
validate_openclaw_package_spec() { validate_openclaw_package_spec() {
local spec="$1" 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 return 0
fi 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 exit 1
} }

View File

@@ -13,10 +13,10 @@ OUTPUT_DIR="${OPENCLAW_NPM_TELEGRAM_OUTPUT_DIR:-.artifacts/qa-e2e/npm-telegram-r
validate_openclaw_package_spec() { validate_openclaw_package_spec() {
local spec="$1" 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 return 0
fi 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 exit 1
} }

View File

@@ -91,12 +91,14 @@ export function normalizeUpgradeSurvivorBaselineSpec(raw) {
} }
const spec = value.startsWith("openclaw@") ? value : `openclaw@${value}`; const spec = value.startsWith("openclaw@") ? value : `openclaw@${value}`;
if ( 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( throw new Error(
`invalid published upgrade survivor baseline: ${JSON.stringify( `invalid published upgrade survivor baseline: ${JSON.stringify(
value, 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; return spec;

View File

@@ -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 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 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 = { type IosVersionManifest = {
version: string; version: string;
@@ -52,7 +52,7 @@ export function normalizeGatewayVersionToPinnedIosVersion(rawVersion: string): s
const match = GATEWAY_VERSION_PATTERN.exec(trimmed); const match = GATEWAY_VERSION_PATTERN.exec(trimmed);
if (!match) { if (!match) {
throw new Error( 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.`,
); );
} }

View File

@@ -1,4 +1,6 @@
const STABLE_VERSION_REGEX = /^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)$/; const STABLE_VERSION_REGEX = /^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)$/;
const ALPHA_VERSION_REGEX =
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)-alpha\.(?<alpha>[1-9]\d*)$/;
const BETA_VERSION_REGEX = const BETA_VERSION_REGEX =
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)-beta\.(?<beta>[1-9]\d*)$/; /^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)-beta\.(?<beta>[1-9]\d*)$/;
const CORRECTION_VERSION_REGEX = const CORRECTION_VERSION_REGEX =
@@ -8,10 +10,11 @@ const CORRECTION_VERSION_REGEX =
* @typedef {object} ParsedReleaseVersion * @typedef {object} ParsedReleaseVersion
* @property {string} version * @property {string} version
* @property {string} baseVersion * @property {string} baseVersion
* @property {"stable" | "beta"} channel * @property {"stable" | "alpha" | "beta"} channel
* @property {number} year * @property {number} year
* @property {number} month * @property {number} month
* @property {number} day * @property {number} day
* @property {number | undefined} [alphaNumber]
* @property {number | undefined} [betaNumber] * @property {number | undefined} [betaNumber]
* @property {number | undefined} [correctionNumber] * @property {number | undefined} [correctionNumber]
* @property {Date} date * @property {Date} date
@@ -19,9 +22,9 @@ const CORRECTION_VERSION_REGEX =
/** /**
* @typedef {object} NpmPublishPlan * @typedef {object} NpmPublishPlan
* @property {"stable" | "beta"} channel * @property {"stable" | "alpha" | "beta"} channel
* @property {"latest" | "beta"} publishTag * @property {"latest" | "alpha" | "beta"} publishTag
* @property {("latest" | "beta")[]} mirrorDistTags * @property {("latest" | "alpha" | "beta")[]} mirrorDistTags
*/ */
/** /**
@@ -37,13 +40,14 @@ const CORRECTION_VERSION_REGEX =
/** /**
* @param {string} version * @param {string} version
* @param {Record<string, string | undefined>} groups * @param {Record<string, string | undefined>} groups
* @param {"stable" | "beta"} channel * @param {"stable" | "alpha" | "beta"} channel
* @returns {ParsedReleaseVersion | null} * @returns {ParsedReleaseVersion | null}
*/ */
function parseDateParts(version, groups, channel) { function parseDateParts(version, groups, channel) {
const year = Number.parseInt(groups.year ?? "", 10); const year = Number.parseInt(groups.year ?? "", 10);
const month = Number.parseInt(groups.month ?? "", 10); const month = Number.parseInt(groups.month ?? "", 10);
const day = Number.parseInt(groups.day ?? "", 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; const betaNumber = channel === "beta" ? Number.parseInt(groups.beta ?? "", 10) : undefined;
if ( if (
@@ -60,6 +64,9 @@ function parseDateParts(version, groups, channel) {
if (channel === "beta" && (!Number.isInteger(betaNumber) || (betaNumber ?? 0) < 1)) { if (channel === "beta" && (!Number.isInteger(betaNumber) || (betaNumber ?? 0) < 1)) {
return null; return null;
} }
if (channel === "alpha" && (!Number.isInteger(alphaNumber) || (alphaNumber ?? 0) < 1)) {
return null;
}
const date = new Date(Date.UTC(year, month - 1, day)); const date = new Date(Date.UTC(year, month - 1, day));
if ( if (
@@ -77,6 +84,7 @@ function parseDateParts(version, groups, channel) {
year, year,
month, month,
day, day,
alphaNumber,
betaNumber, betaNumber,
date, date,
}; };
@@ -97,6 +105,11 @@ export function parseReleaseVersion(version) {
return parseDateParts(trimmed, stableMatch.groups, "stable"); 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); const betaMatch = BETA_VERSION_REGEX.exec(trimmed);
if (betaMatch?.groups) { if (betaMatch?.groups) {
return parseDateParts(trimmed, betaMatch.groups, "beta"); return parseDateParts(trimmed, betaMatch.groups, "beta");
@@ -137,7 +150,12 @@ export function compareReleaseVersions(left, right) {
} }
if (parsedLeft.channel !== parsedRight.channel) { 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") { if (parsedLeft.channel === "beta" && parsedRight.channel === "beta") {
@@ -165,6 +183,13 @@ export function resolveNpmPublishPlan(version, currentBetaVersion) {
mirrorDistTags: [], mirrorDistTags: [],
}; };
} }
if (parsedVersion.channel === "alpha") {
return {
channel: "alpha",
publishTag: "alpha",
mirrorDistTags: [],
};
}
const normalizedCurrentBeta = currentBetaVersion?.trim(); const normalizedCurrentBeta = currentBetaVersion?.trim();
if (normalizedCurrentBeta) { if (normalizedCurrentBeta) {

View File

@@ -46,8 +46,8 @@ export type PublishablePluginPackage = {
packageDir: string; packageDir: string;
packageName: string; packageName: string;
version: string; version: string;
channel: "stable" | "beta"; channel: "stable" | "alpha" | "beta";
publishTag: "latest" | "beta"; publishTag: "latest" | "alpha" | "beta";
}; };
type PluginReleasePlanItem = PublishablePluginPackage & { type PluginReleasePlanItem = PublishablePluginPackage & {
@@ -154,7 +154,12 @@ export function collectClawHubPublishablePluginPackages(
packageName, packageName,
version, version,
channel: parsedVersion.channel, channel: parsedVersion.channel,
publishTag: parsedVersion.channel === "beta" ? "beta" : "latest", publishTag:
parsedVersion.channel === "alpha"
? "alpha"
: parsedVersion.channel === "beta"
? "beta"
: "latest",
}); });
} }

View File

@@ -34,8 +34,8 @@ export type PublishablePluginPackage = {
packageDir: string; packageDir: string;
packageName: string; packageName: string;
version: string; version: string;
channel: "stable" | "beta"; channel: "stable" | "alpha" | "beta";
publishTag: "latest" | "beta"; publishTag: "latest" | "alpha" | "beta";
installNpmSpec?: string; installNpmSpec?: string;
}; };
@@ -117,7 +117,7 @@ export function resolvePublishablePluginVersion(params: {
const parsedVersion = parseReleaseVersion(version); const parsedVersion = parseReleaseVersion(version);
if (parsedVersion === null) { if (parsedVersion === null) {
params.validationErrors.push( 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; return null;
} }
@@ -244,7 +244,7 @@ export function collectPublishablePluginPackageErrors(
errors.push("package.json version must be non-empty."); errors.push("package.json version must be non-empty.");
} else if (parseReleaseVersion(packageVersion) === null) { } else if (parseReleaseVersion(packageVersion) === null) {
errors.push( 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) { if (!Array.isArray(extensions) || extensions.length === 0) {

View File

@@ -64,7 +64,7 @@ type TelegramQaSummary = {
}; };
const OPENCLAW_PACKAGE_SPEC_RE = 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 = [ const REQUIRED_TELEGRAM_ENV = [
"OPENCLAW_QA_TELEGRAM_GROUP_ID", "OPENCLAW_QA_TELEGRAM_GROUP_ID",
@@ -75,7 +75,7 @@ const REQUIRED_TELEGRAM_ENV = [
export function validateOpenClawPackageSpec(spec: string) { export function validateOpenClawPackageSpec(spec: string) {
if (!OPENCLAW_PACKAGE_SPEC_RE.test(spec)) { if (!OPENCLAW_PACKAGE_SPEC_RE.test(spec)) {
throw new Error( 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; return spec;

View File

@@ -29,7 +29,7 @@ ZIP_NAME=$(basename "$ZIP")
ZIP_BASE="${ZIP_NAME%.zip}" ZIP_BASE="${ZIP_NAME%.zip}"
VERSION=${SPARKLE_RELEASE_VERSION:-} VERSION=${SPARKLE_RELEASE_VERSION:-}
if [[ -z "$VERSION" ]]; then 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 if [[ "$ZIP_NAME" =~ ^OpenClaw-([0-9]+(\.[0-9]+){1,2}([-.][0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?)\.zip$ ]]; then
VERSION="${BASH_REMATCH[1]}" VERSION="${BASH_REMATCH[1]}"
else else

View File

@@ -184,7 +184,7 @@ function parseBooleanEnv(name, fallback) {
export function looksLikeReleaseVersionRef(ref) { export function looksLikeReleaseVersionRef(ref) {
const trimmed = normalizeRequestedRef(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, trimmed,
); );
} }

View File

@@ -24,7 +24,11 @@ mapfile -t publish_plan < <(
import { resolveNpmPublishPlan } from "./scripts/openclaw-npm-release-check.ts"; import { resolveNpmPublishPlan } from "./scripts/openclaw-npm-release-check.ts";
const requestedPublishTag = 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); const plan = resolveNpmPublishPlan(process.env.PACKAGE_VERSION ?? "", undefined, requestedPublishTag);
console.log(plan.channel); console.log(plan.channel);
console.log(plan.publishTag); console.log(plan.publishTag);

View File

@@ -32,10 +32,11 @@ type PackageJson = {
export type ParsedReleaseVersion = { export type ParsedReleaseVersion = {
version: string; version: string;
baseVersion: string; baseVersion: string;
channel: "stable" | "beta"; channel: "stable" | "alpha" | "beta";
year: number; year: number;
month: number; month: number;
day: number; day: number;
alphaNumber?: number;
betaNumber?: number; betaNumber?: number;
correctionNumber?: number; correctionNumber?: number;
date: Date; date: Date;
@@ -45,15 +46,15 @@ export type ParsedReleaseTag = {
version: string; version: string;
packageVersion: string; packageVersion: string;
baseVersion: string; baseVersion: string;
channel: "stable" | "beta"; channel: "stable" | "alpha" | "beta";
correctionNumber?: number; correctionNumber?: number;
date: Date; date: Date;
}; };
export type NpmPublishPlan = { export type NpmPublishPlan = {
channel: "stable" | "beta"; channel: "stable" | "alpha" | "beta";
publishTag: "latest" | "beta"; publishTag: "latest" | "alpha" | "beta";
mirrorDistTags: ("latest" | "beta")[]; mirrorDistTags: ("latest" | "alpha" | "beta")[];
}; };
export type NpmDistTagMirrorAuth = { export type NpmDistTagMirrorAuth = {
@@ -193,14 +194,30 @@ export function compareReleaseVersions(left: string, right: string): number | nu
export function resolveNpmPublishPlan( export function resolveNpmPublishPlan(
version: string, version: string,
_currentBetaVersion?: string | null, _currentBetaVersion?: string | null,
requestedPublishTag?: "latest" | "beta" | null, requestedPublishTag?: "latest" | "alpha" | "beta" | null,
): NpmPublishPlan { ): NpmPublishPlan {
const parsedVersion = parseReleaseVersion(version); const parsedVersion = parseReleaseVersion(version);
if (parsedVersion === null) { if (parsedVersion === null) {
throw new Error(`Unsupported release version "${version}".`); 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 (parsedVersion.channel === "beta") {
if (publishTag !== "beta") { if (publishTag !== "beta") {
@@ -336,7 +353,7 @@ export function collectReleaseTagErrors(params: {
const parsedVersion = parseReleaseVersion(packageVersion); const parsedVersion = parseReleaseVersion(packageVersion);
if (parsedVersion === null) { if (parsedVersion === null) {
errors.push( errors.push(
`package.json version must match YYYY.M.D, YYYY.M.D-N, or YYYY.M.D-beta.N; found "${packageVersion || "<missing>"}".`, `package.json version must match YYYY.M.D, YYYY.M.D-N, YYYY.M.D-alpha.N, or YYYY.M.D-beta.N; found "${packageVersion || "<missing>"}".`,
); );
} }
@@ -348,7 +365,7 @@ export function collectReleaseTagErrors(params: {
const parsedTag = parseReleaseTagVersion(tagVersion); const parsedTag = parseReleaseTagVersion(tagVersion);
if (parsedTag === null) { if (parsedTag === null) {
errors.push( errors.push(
`Release tag must match vYYYY.M.D, vYYYY.M.D-beta.N, or fallback correction tag vYYYY.M.D-N; found "${releaseTag || "<missing>"}".`, `Release tag must match vYYYY.M.D, vYYYY.M.D-alpha.N, vYYYY.M.D-beta.N, or fallback correction tag vYYYY.M.D-N; found "${releaseTag || "<missing>"}".`,
); );
} }

View File

@@ -12,7 +12,7 @@ import { fileURLToPath } from "node:url";
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const DEFAULT_OUTPUT_NAME = "openclaw-current.tgz"; const DEFAULT_OUTPUT_NAME = "openclaw-current.tgz";
export const OPENCLAW_PACKAGE_SPEC_RE = 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() { function usage() {
return `Usage: node scripts/resolve-openclaw-package-candidate.mjs --source <ref|npm|url|artifact> --output-dir <dir> [options] return `Usage: node scripts/resolve-openclaw-package-candidate.mjs --source <ref|npm|url|artifact> --output-dir <dir> [options]
@@ -82,7 +82,7 @@ export function parseArgs(argv) {
export function validateOpenClawPackageSpec(spec) { export function validateOpenClawPackageSpec(spec) {
if (!OPENCLAW_PACKAGE_SPEC_RE.test(spec)) { if (!OPENCLAW_PACKAGE_SPEC_RE.test(spec)) {
throw new Error( 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}`,
); );
} }
} }

View File

@@ -45,6 +45,16 @@ describe("shouldRequireNpmDistTagMirrorAuth", () => {
).toBe(false); ).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", () => { it("does not require auth when a publish already has npm auth", () => {
const plan = resolveNpmPublishPlan("2026.4.1"); const plan = resolveNpmPublishPlan("2026.4.1");
const auth = resolveNpmDistTagMirrorAuth({ npmToken: "token" }); const auth = resolveNpmDistTagMirrorAuth({ npmToken: "token" });

View File

@@ -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", () => { it("parses stable correction releases", () => {
expect(parseReleaseVersion("2026.3.10-1")).toMatchObject({ expect(parseReleaseVersion("2026.3.10-1")).toMatchObject({
version: "2026.3.10-1", 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", () => { it("publishes stable releases to beta first", () => {
expect(resolveNpmPublishPlan("2026.3.29")).toEqual({ expect(resolveNpmPublishPlan("2026.3.29")).toEqual({
channel: "stable", channel: "stable",
@@ -138,6 +158,15 @@ describe("resolveNpmPublishPlan", () => {
"Beta prereleases must publish to the beta dist-tag.", "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", () => { describe("resolveNpmDistTagMirrorAuth", () => {
@@ -205,6 +234,10 @@ describe("compareReleaseVersions", () => {
expect(compareReleaseVersions("2026.3.29", "2026.3.29-beta.2")).toBe(1); 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", () => { 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); expect(compareReleaseVersions("2026.4.1-beta.1", "2026.3.29")).toBe(1);
}); });

View File

@@ -134,7 +134,7 @@ describe("collectPublishablePluginPackageErrors", () => {
'package name must start with "@openclaw/"; found "broken".', 'package name must start with "@openclaw/"; found "broken".',
"package.json private must not be true.", "package.json private must not be true.",
`package.json repository.url must be "${OPENCLAW_PLUGIN_NPM_REPOSITORY_URL}" so npm provenance can validate GitHub trusted publishing; found "<missing>".`, `package.json repository.url must be "${OPENCLAW_PLUGIN_NPM_REPOSITORY_URL}" so npm provenance can validate GitHub trusted publishing; found "<missing>".`,
'package.json version must match YYYY.M.D, YYYY.M.D-N, 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.extensions must contain only non-empty strings.",
"openclaw.install.npmSpec must be a non-empty string for publishable plugins.", "openclaw.install.npmSpec must be a non-empty string for publishable plugins.",
]); ]);
@@ -314,6 +314,37 @@ describe("collectPublishablePluginPackages", () => {
}), }),
).toEqual([]); ).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", () => { describe("resolveSelectedPublishablePluginPackages", () => {

View File

@@ -55,6 +55,10 @@ describe("gateway version normalization", () => {
expect(normalizeGatewayVersionToPinnedIosVersion("2026.4.6-beta.2")).toBe("2026.4.6"); 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", () => { it("strips fallback correction suffixes when pinning from gateway version", () => {
expect(normalizeGatewayVersionToPinnedIosVersion("2026.4.6-3")).toBe("2026.4.6"); expect(normalizeGatewayVersionToPinnedIosVersion("2026.4.6-3")).toBe("2026.4.6");
}); });

View File

@@ -233,6 +233,8 @@ describe("scripts/openclaw-cross-os-release-checks", () => {
expect(looksLikeReleaseVersionRef("2026.4.5")).toBe(true); expect(looksLikeReleaseVersionRef("2026.4.5")).toBe(true);
expect(looksLikeReleaseVersionRef("refs/tags/v2026.4.5-beta.1")).toBe(true); expect(looksLikeReleaseVersionRef("refs/tags/v2026.4.5-beta.1")).toBe(true);
expect(looksLikeReleaseVersionRef("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("v2026.4.7-1")).toBe(true);
expect(looksLikeReleaseVersionRef("main")).toBe(false); expect(looksLikeReleaseVersionRef("main")).toBe(false);
expect(looksLikeReleaseVersionRef("codex/cross-os-release-checks")).toBe(false); expect(looksLikeReleaseVersionRef("codex/cross-os-release-checks")).toBe(false);

View File

@@ -228,7 +228,7 @@ describe("package artifact reuse", () => {
expect(scheduler).toContain('["OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS",'); expect(scheduler).toContain('["OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS",');
expect(packageJson).toContain("OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1"); expect(packageJson).toContain("OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1");
expect(publishedUpgradeSurvivor).toContain("validate_baseline_package_spec"); 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("plugin_deps_cleanup_plugin_dirs");
expect(publishedUpgradeSurvivor).toContain('"$(package_root)/extensions/$plugin"'); expect(publishedUpgradeSurvivor).toContain('"$(package_root)/extensions/$plugin"');
expect(publishedUpgradeSurvivor).toContain("probe_gateway_endpoint"); expect(publishedUpgradeSurvivor).toContain("probe_gateway_endpoint");
@@ -623,7 +623,7 @@ describe("package artifact reuse", () => {
}); });
expectTextToIncludeAll(validateStep.run, [ expectTextToIncludeAll(validateStep.run, [
'if [[ -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then', 'if [[ -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then',
"package_spec must be openclaw@beta", "package_spec must be openclaw@alpha",
]); ]);
expectTextToIncludeAll(runStep.run, [ expectTextToIncludeAll(runStep.run, [
'export OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ="${package_tgzs[0]}"', 'export OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ="${package_tgzs[0]}"',

View File

@@ -11,25 +11,27 @@ import {
describe("resolve-openclaw-package-candidate", () => { describe("resolve-openclaw-package-candidate", () => {
it("accepts only OpenClaw release package specs for npm candidates", () => { it("accepts only OpenClaw release package specs for npm candidates", () => {
expect(() => validateOpenClawPackageSpec("openclaw@beta")).not.toThrow(); expect(() => validateOpenClawPackageSpec("openclaw@beta")).not.toThrow();
expect(() => validateOpenClawPackageSpec("openclaw@alpha")).not.toThrow();
expect(() => validateOpenClawPackageSpec("openclaw@latest")).not.toThrow(); expect(() => validateOpenClawPackageSpec("openclaw@latest")).not.toThrow();
expect(() => validateOpenClawPackageSpec("openclaw@2026.4.27")).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-1")).not.toThrow();
expect(() => validateOpenClawPackageSpec("openclaw@2026.4.27-beta.2")).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( 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( expect(() => validateOpenClawPackageSpec("openclaw@canary")).toThrow(
"package_spec must be openclaw@beta", "package_spec must be openclaw@alpha",
); );
expect(() => validateOpenClawPackageSpec("openclaw@2026.04.27")).toThrow( 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( 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( expect(() => validateOpenClawPackageSpec("openclaw@file:../other-package.tgz")).toThrow(
"package_spec must be openclaw@beta", "package_spec must be openclaw@alpha",
); );
}); });

View File

@@ -21,12 +21,16 @@ const FIXTURE_PATH = path.resolve(TEST_DIR, "../fixtures/telegram-qa-summary-rtt
describe("RTT harness", () => { describe("RTT harness", () => {
it("validates OpenClaw package specs", () => { it("validates OpenClaw package specs", () => {
expect(validateOpenClawPackageSpec("openclaw@main")).toBe("openclaw@main"); expect(validateOpenClawPackageSpec("openclaw@main")).toBe("openclaw@main");
expect(validateOpenClawPackageSpec("openclaw@alpha")).toBe("openclaw@alpha");
expect(validateOpenClawPackageSpec("openclaw@beta")).toBe("openclaw@beta"); expect(validateOpenClawPackageSpec("openclaw@beta")).toBe("openclaw@beta");
expect(validateOpenClawPackageSpec("openclaw@latest")).toBe("openclaw@latest"); expect(validateOpenClawPackageSpec("openclaw@latest")).toBe("openclaw@latest");
expect(validateOpenClawPackageSpec("openclaw@2026.4.30")).toBe("openclaw@2026.4.30"); expect(validateOpenClawPackageSpec("openclaw@2026.4.30")).toBe("openclaw@2026.4.30");
expect(validateOpenClawPackageSpec("openclaw@2026.4.30-beta.2")).toBe( expect(validateOpenClawPackageSpec("openclaw@2026.4.30-beta.2")).toBe(
"openclaw@2026.4.30-beta.2", "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( expect(() => validateOpenClawPackageSpec("@openclaw/openclaw@beta")).toThrow(
/Package spec must be/, /Package spec must be/,