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

@@ -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@<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
;;
*)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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;

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 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.`,
);
}

View File

@@ -1,4 +1,6 @@
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 =
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)-beta\.(?<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<string, string | undefined>} 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) {

View File

@@ -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",
});
}

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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

View File

@@ -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,
);
}

View File

@@ -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);

View File

@@ -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 || "<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);
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 || "<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 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 <ref|npm|url|artifact> --output-dir <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}`,
);
}
}