mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
ci: add npm release workflow and CalVer checks (#42414) (thanks @onutc)
This commit is contained in:
79
.github/workflows/openclaw-npm-release.yml
vendored
Normal file
79
.github/workflows/openclaw-npm-release.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
name: OpenClaw NPM Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: openclaw-npm-release-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: "22.x"
|
||||||
|
PNPM_VERSION: "10.23.0"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish_openclaw_npm:
|
||||||
|
# npm trusted publishing + provenance requires a GitHub-hosted runner.
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Node environment
|
||||||
|
uses: ./.github/actions/setup-node-env
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
pnpm-version: ${{ env.PNPM_VERSION }}
|
||||||
|
install-bun: "false"
|
||||||
|
use-sticky-disk: "false"
|
||||||
|
|
||||||
|
- name: Validate release tag and package metadata
|
||||||
|
env:
|
||||||
|
RELEASE_SHA: ${{ github.sha }}
|
||||||
|
RELEASE_TAG: ${{ github.ref_name }}
|
||||||
|
RELEASE_MAIN_REF: origin/main
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# Fetch the full main ref so merge-base ancestry checks keep working
|
||||||
|
# for older tagged commits that are still contained in main.
|
||||||
|
git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
|
||||||
|
pnpm release:openclaw:npm:check
|
||||||
|
|
||||||
|
- name: Ensure version is not already published
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
||||||
|
|
||||||
|
if npm view "openclaw@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
|
||||||
|
echo "openclaw@${PACKAGE_VERSION} is already published on npm."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Publishing openclaw@${PACKAGE_VERSION}"
|
||||||
|
|
||||||
|
- name: Check
|
||||||
|
run: pnpm check
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Verify release contents
|
||||||
|
run: pnpm release:check
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
||||||
|
|
||||||
|
if [[ "$PACKAGE_VERSION" == *-beta.* ]]; then
|
||||||
|
npm publish --access public --tag beta --provenance
|
||||||
|
else
|
||||||
|
npm publish --access public --provenance
|
||||||
|
fi
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -81,6 +81,7 @@ apps/ios/*.mobileprovision
|
|||||||
# Local untracked files
|
# Local untracked files
|
||||||
.local/
|
.local/
|
||||||
docs/.local/
|
docs/.local/
|
||||||
|
tmp/
|
||||||
IDENTITY.md
|
IDENTITY.md
|
||||||
USER.md
|
USER.md
|
||||||
.tgz
|
.tgz
|
||||||
|
|||||||
@@ -19,6 +19,32 @@ When the operator says “release”, immediately do this preflight (no extra qu
|
|||||||
- Load env from `~/.profile` and confirm `SPARKLE_PRIVATE_KEY_FILE` + App Store Connect vars are set (SPARKLE_PRIVATE_KEY_FILE should live in `~/.profile`).
|
- Load env from `~/.profile` and confirm `SPARKLE_PRIVATE_KEY_FILE` + App Store Connect vars are set (SPARKLE_PRIVATE_KEY_FILE should live in `~/.profile`).
|
||||||
- Use Sparkle keys from `~/Library/CloudStorage/Dropbox/Backup/Sparkle` if needed.
|
- Use Sparkle keys from `~/Library/CloudStorage/Dropbox/Backup/Sparkle` if needed.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Current OpenClaw releases use date-based versioning.
|
||||||
|
|
||||||
|
- Stable release version: `YYYY.M.D`
|
||||||
|
- Git tag: `vYYYY.M.D`
|
||||||
|
- Examples from repo history: `v2026.2.26`, `v2026.3.8`
|
||||||
|
- Beta prerelease version: `YYYY.M.D-beta.N`
|
||||||
|
- Git tag: `vYYYY.M.D-beta.N`
|
||||||
|
- Examples from repo history: `v2026.2.15-beta.1`, `v2026.3.8-beta.1`
|
||||||
|
- Use the same version string everywhere, minus the leading `v` where Git tags are not used:
|
||||||
|
- `package.json`: `2026.3.8`
|
||||||
|
- Git tag: `v2026.3.8`
|
||||||
|
- GitHub release title: `openclaw 2026.3.8`
|
||||||
|
- Do not zero-pad month or day. Use `2026.3.8`, not `2026.03.08`.
|
||||||
|
- Stable and beta are npm dist-tags, not separate release lines:
|
||||||
|
- `latest` = stable
|
||||||
|
- `beta` = prerelease/testing
|
||||||
|
- Dev is the moving head of `main`, not a normal git-tagged release.
|
||||||
|
- The release workflow enforces the current stable/beta tag formats and rejects versions whose CalVer date is more than 2 UTC calendar days away from the release date.
|
||||||
|
|
||||||
|
Historical note:
|
||||||
|
|
||||||
|
- Older tags such as `v2026.1.11-1`, `v2026.2.6-3`, and `v2.0.0-beta2` exist in repo history.
|
||||||
|
- Treat those as legacy tag patterns. New releases should use `vYYYY.M.D` for stable and `vYYYY.M.D-beta.N` for beta.
|
||||||
|
|
||||||
1. **Version & metadata**
|
1. **Version & metadata**
|
||||||
|
|
||||||
- [ ] Bump `package.json` version (e.g., `2026.1.29`).
|
- [ ] Bump `package.json` version (e.g., `2026.1.29`).
|
||||||
@@ -67,8 +93,11 @@ When the operator says “release”, immediately do this preflight (no extra qu
|
|||||||
6. **Publish (npm)**
|
6. **Publish (npm)**
|
||||||
|
|
||||||
- [ ] Confirm git status is clean; commit and push as needed.
|
- [ ] Confirm git status is clean; commit and push as needed.
|
||||||
- [ ] `npm login` (verify 2FA) if needed.
|
- [ ] Confirm npm trusted publishing is configured for the `openclaw` package.
|
||||||
- [ ] `npm publish --access public` (use `--tag beta` for pre-releases).
|
- [ ] Push the matching git tag to trigger `.github/workflows/openclaw-npm-release.yml`.
|
||||||
|
- Stable tags publish to npm `latest`.
|
||||||
|
- Beta tags publish to npm `beta`.
|
||||||
|
- The workflow rejects tags that do not match `package.json`, are not on `main`, or whose CalVer date is more than 2 UTC calendar days away from the release date.
|
||||||
- [ ] Verify the registry: `npm view openclaw version`, `npm view openclaw dist-tags`, and `npx -y openclaw@X.Y.Z --version` (or `--help`).
|
- [ ] Verify the registry: `npm view openclaw version`, `npm view openclaw dist-tags`, and `npx -y openclaw@X.Y.Z --version` (or `--help`).
|
||||||
|
|
||||||
### Troubleshooting (notes from 2.0.0-beta2 release)
|
### Troubleshooting (notes from 2.0.0-beta2 release)
|
||||||
@@ -84,6 +113,7 @@ When the operator says “release”, immediately do this preflight (no extra qu
|
|||||||
7. **GitHub release + appcast**
|
7. **GitHub release + appcast**
|
||||||
|
|
||||||
- [ ] Tag and push: `git tag vX.Y.Z && git push origin vX.Y.Z` (or `git push --tags`).
|
- [ ] Tag and push: `git tag vX.Y.Z && git push origin vX.Y.Z` (or `git push --tags`).
|
||||||
|
- Pushing the tag also triggers the npm release workflow.
|
||||||
- [ ] Create/refresh the GitHub release for `vX.Y.Z` with **title `openclaw X.Y.Z`** (not just the tag); body should include the **full** changelog section for that version (Highlights + Changes + Fixes), inline (no bare links), and **must not repeat the title inside the body**.
|
- [ ] Create/refresh the GitHub release for `vX.Y.Z` with **title `openclaw X.Y.Z`** (not just the tag); body should include the **full** changelog section for that version (Highlights + Changes + Fixes), inline (no bare links), and **must not repeat the title inside the body**.
|
||||||
- [ ] Attach artifacts: `npm pack` tarball (optional), `OpenClaw-X.Y.Z.zip`, and `OpenClaw-X.Y.Z.dSYM.zip` (if generated).
|
- [ ] Attach artifacts: `npm pack` tarball (optional), `OpenClaw-X.Y.Z.zip`, and `OpenClaw-X.Y.Z.dSYM.zip` (if generated).
|
||||||
- [ ] Commit the updated `appcast.xml` and push it (Sparkle feeds from main).
|
- [ ] Commit the updated `appcast.xml` and push it (Sparkle feeds from main).
|
||||||
|
|||||||
@@ -295,6 +295,7 @@
|
|||||||
"protocol:gen": "node --import tsx scripts/protocol-gen.ts",
|
"protocol:gen": "node --import tsx scripts/protocol-gen.ts",
|
||||||
"protocol:gen:swift": "node --import tsx scripts/protocol-gen-swift.ts",
|
"protocol:gen:swift": "node --import tsx scripts/protocol-gen-swift.ts",
|
||||||
"release:check": "node --import tsx scripts/release-check.ts",
|
"release:check": "node --import tsx scripts/release-check.ts",
|
||||||
|
"release:openclaw:npm:check": "node --import tsx scripts/openclaw-npm-release-check.ts",
|
||||||
"start": "node scripts/run-node.mjs",
|
"start": "node scripts/run-node.mjs",
|
||||||
"test": "node scripts/test-parallel.mjs",
|
"test": "node scripts/test-parallel.mjs",
|
||||||
"test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all",
|
"test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all",
|
||||||
|
|||||||
251
scripts/openclaw-npm-release-check.ts
Normal file
251
scripts/openclaw-npm-release-check.ts
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
#!/usr/bin/env -S node --import tsx
|
||||||
|
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
|
type PackageJson = {
|
||||||
|
name?: string;
|
||||||
|
version?: string;
|
||||||
|
description?: string;
|
||||||
|
license?: string;
|
||||||
|
repository?: { url?: string } | string;
|
||||||
|
bin?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ParsedReleaseVersion = {
|
||||||
|
version: string;
|
||||||
|
channel: "stable" | "beta";
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
day: number;
|
||||||
|
betaNumber?: number;
|
||||||
|
date: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
const STABLE_VERSION_REGEX = /^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)$/;
|
||||||
|
const BETA_VERSION_REGEX =
|
||||||
|
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)-beta\.(?<beta>[1-9]\d*)$/;
|
||||||
|
const EXPECTED_REPOSITORY_URL = "https://github.com/openclaw/openclaw";
|
||||||
|
const MAX_CALVER_DISTANCE_DAYS = 2;
|
||||||
|
|
||||||
|
function normalizeRepoUrl(value: unknown): string {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
.trim()
|
||||||
|
.replace(/^git\+/, "")
|
||||||
|
.replace(/\.git$/i, "")
|
||||||
|
.replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDateParts(
|
||||||
|
version: string,
|
||||||
|
groups: Record<string, string | undefined>,
|
||||||
|
channel: "stable" | "beta",
|
||||||
|
): ParsedReleaseVersion | null {
|
||||||
|
const year = Number.parseInt(groups.year ?? "", 10);
|
||||||
|
const month = Number.parseInt(groups.month ?? "", 10);
|
||||||
|
const day = Number.parseInt(groups.day ?? "", 10);
|
||||||
|
const betaNumber = channel === "beta" ? Number.parseInt(groups.beta ?? "", 10) : undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Number.isInteger(year) ||
|
||||||
|
!Number.isInteger(month) ||
|
||||||
|
!Number.isInteger(day) ||
|
||||||
|
month < 1 ||
|
||||||
|
month > 12 ||
|
||||||
|
day < 1 ||
|
||||||
|
day > 31
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (channel === "beta" && (!Number.isInteger(betaNumber) || (betaNumber ?? 0) < 1)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
if (
|
||||||
|
date.getUTCFullYear() !== year ||
|
||||||
|
date.getUTCMonth() !== month - 1 ||
|
||||||
|
date.getUTCDate() !== day
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version,
|
||||||
|
channel,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
betaNumber,
|
||||||
|
date,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseReleaseVersion(version: string): ParsedReleaseVersion | null {
|
||||||
|
const trimmed = version.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stableMatch = STABLE_VERSION_REGEX.exec(trimmed);
|
||||||
|
if (stableMatch?.groups) {
|
||||||
|
return parseDateParts(trimmed, stableMatch.groups, "stable");
|
||||||
|
}
|
||||||
|
|
||||||
|
const betaMatch = BETA_VERSION_REGEX.exec(trimmed);
|
||||||
|
if (betaMatch?.groups) {
|
||||||
|
return parseDateParts(trimmed, betaMatch.groups, "beta");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startOfUtcDay(date: Date): number {
|
||||||
|
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function utcCalendarDayDistance(left: Date, right: Date): number {
|
||||||
|
return Math.round(Math.abs(startOfUtcDay(left) - startOfUtcDay(right)) / 86_400_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectReleasePackageMetadataErrors(pkg: PackageJson): string[] {
|
||||||
|
const actualRepositoryUrl = normalizeRepoUrl(
|
||||||
|
typeof pkg.repository === "string" ? pkg.repository : pkg.repository?.url,
|
||||||
|
);
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
if (pkg.name !== "openclaw") {
|
||||||
|
errors.push(`package.json name must be "openclaw"; found "${pkg.name ?? ""}".`);
|
||||||
|
}
|
||||||
|
if (!pkg.description?.trim()) {
|
||||||
|
errors.push("package.json description must be non-empty.");
|
||||||
|
}
|
||||||
|
if (pkg.license !== "MIT") {
|
||||||
|
errors.push(`package.json license must be "MIT"; found "${pkg.license ?? ""}".`);
|
||||||
|
}
|
||||||
|
if (actualRepositoryUrl !== EXPECTED_REPOSITORY_URL) {
|
||||||
|
errors.push(
|
||||||
|
`package.json repository.url must resolve to ${EXPECTED_REPOSITORY_URL}; found ${
|
||||||
|
actualRepositoryUrl || "<missing>"
|
||||||
|
}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (pkg.bin?.openclaw !== "openclaw.mjs") {
|
||||||
|
errors.push(
|
||||||
|
`package.json bin.openclaw must be "openclaw.mjs"; found "${pkg.bin?.openclaw ?? ""}".`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectReleaseTagErrors(params: {
|
||||||
|
packageVersion: string;
|
||||||
|
releaseTag: string;
|
||||||
|
releaseSha?: string;
|
||||||
|
releaseMainRef?: string;
|
||||||
|
now?: Date;
|
||||||
|
}): string[] {
|
||||||
|
const errors: string[] = [];
|
||||||
|
const releaseTag = params.releaseTag.trim();
|
||||||
|
const packageVersion = params.packageVersion.trim();
|
||||||
|
const now = params.now ?? new Date();
|
||||||
|
|
||||||
|
const parsedVersion = parseReleaseVersion(packageVersion);
|
||||||
|
if (parsedVersion === null) {
|
||||||
|
errors.push(
|
||||||
|
`package.json version must match YYYY.M.D or YYYY.M.D-beta.N; found "${packageVersion || "<missing>"}".`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!releaseTag.startsWith("v")) {
|
||||||
|
errors.push(`Release tag must start with "v"; found "${releaseTag || "<missing>"}".`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagVersion = releaseTag.startsWith("v") ? releaseTag.slice(1) : releaseTag;
|
||||||
|
const parsedTag = parseReleaseVersion(tagVersion);
|
||||||
|
if (parsedTag === null) {
|
||||||
|
errors.push(
|
||||||
|
`Release tag must match vYYYY.M.D or vYYYY.M.D-beta.N; found "${releaseTag || "<missing>"}".`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedTag = packageVersion ? `v${packageVersion}` : "";
|
||||||
|
if (releaseTag !== expectedTag) {
|
||||||
|
errors.push(
|
||||||
|
`Release tag ${releaseTag || "<missing>"} does not match package.json version ${
|
||||||
|
packageVersion || "<missing>"
|
||||||
|
}; expected ${expectedTag || "<missing>"}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedVersion !== null) {
|
||||||
|
const dayDistance = utcCalendarDayDistance(parsedVersion.date, now);
|
||||||
|
if (dayDistance > MAX_CALVER_DISTANCE_DAYS) {
|
||||||
|
const nowLabel = now.toISOString().slice(0, 10);
|
||||||
|
const versionDate = parsedVersion.date.toISOString().slice(0, 10);
|
||||||
|
errors.push(
|
||||||
|
`Release version ${packageVersion} is ${dayDistance} days away from current UTC date ${nowLabel}; release CalVer date ${versionDate} must be within ${MAX_CALVER_DISTANCE_DAYS} days.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.releaseSha?.trim() && params.releaseMainRef?.trim()) {
|
||||||
|
try {
|
||||||
|
execFileSync(
|
||||||
|
"git",
|
||||||
|
["merge-base", "--is-ancestor", params.releaseSha, params.releaseMainRef],
|
||||||
|
{ stdio: "ignore" },
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
errors.push(
|
||||||
|
`Tagged commit ${params.releaseSha} is not contained in ${params.releaseMainRef}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPackageJson(): PackageJson {
|
||||||
|
return JSON.parse(readFileSync("package.json", "utf8")) as PackageJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main(): number {
|
||||||
|
const pkg = loadPackageJson();
|
||||||
|
const metadataErrors = collectReleasePackageMetadataErrors(pkg);
|
||||||
|
const tagErrors = collectReleaseTagErrors({
|
||||||
|
packageVersion: pkg.version ?? "",
|
||||||
|
releaseTag: process.env.RELEASE_TAG ?? "",
|
||||||
|
releaseSha: process.env.RELEASE_SHA,
|
||||||
|
releaseMainRef: process.env.RELEASE_MAIN_REF,
|
||||||
|
});
|
||||||
|
const errors = [...metadataErrors, ...tagErrors];
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
for (const error of errors) {
|
||||||
|
console.error(`openclaw-npm-release-check: ${error}`);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedVersion = parseReleaseVersion(pkg.version ?? "");
|
||||||
|
const channel = parsedVersion?.channel ?? "unknown";
|
||||||
|
const dayDistance =
|
||||||
|
parsedVersion === null
|
||||||
|
? "unknown"
|
||||||
|
: String(utcCalendarDayDistance(parsedVersion.date, new Date()));
|
||||||
|
console.log(
|
||||||
|
`openclaw-npm-release-check: validated ${channel} release ${pkg.version} (${dayDistance} day UTC delta).`,
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||||
|
process.exit(main());
|
||||||
|
}
|
||||||
92
test/openclaw-npm-release-check.test.ts
Normal file
92
test/openclaw-npm-release-check.test.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
collectReleasePackageMetadataErrors,
|
||||||
|
collectReleaseTagErrors,
|
||||||
|
parseReleaseVersion,
|
||||||
|
utcCalendarDayDistance,
|
||||||
|
} from "../scripts/openclaw-npm-release-check.ts";
|
||||||
|
|
||||||
|
describe("parseReleaseVersion", () => {
|
||||||
|
it("parses stable CalVer releases", () => {
|
||||||
|
expect(parseReleaseVersion("2026.3.9")).toMatchObject({
|
||||||
|
version: "2026.3.9",
|
||||||
|
channel: "stable",
|
||||||
|
year: 2026,
|
||||||
|
month: 3,
|
||||||
|
day: 9,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses beta CalVer releases", () => {
|
||||||
|
expect(parseReleaseVersion("2026.3.9-beta.2")).toMatchObject({
|
||||||
|
version: "2026.3.9-beta.2",
|
||||||
|
channel: "beta",
|
||||||
|
year: 2026,
|
||||||
|
month: 3,
|
||||||
|
day: 9,
|
||||||
|
betaNumber: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects legacy and malformed release formats", () => {
|
||||||
|
expect(parseReleaseVersion("2026.3.9-1")).toBeNull();
|
||||||
|
expect(parseReleaseVersion("2026.03.09")).toBeNull();
|
||||||
|
expect(parseReleaseVersion("v2026.3.9")).toBeNull();
|
||||||
|
expect(parseReleaseVersion("2026.2.30")).toBeNull();
|
||||||
|
expect(parseReleaseVersion("2.0.0-beta2")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("utcCalendarDayDistance", () => {
|
||||||
|
it("compares UTC calendar days rather than wall-clock hours", () => {
|
||||||
|
const left = new Date("2026-03-09T23:59:59Z");
|
||||||
|
const right = new Date("2026-03-11T00:00:01Z");
|
||||||
|
expect(utcCalendarDayDistance(left, right)).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("collectReleaseTagErrors", () => {
|
||||||
|
it("accepts versions within the two-day CalVer window", () => {
|
||||||
|
expect(
|
||||||
|
collectReleaseTagErrors({
|
||||||
|
packageVersion: "2026.3.9",
|
||||||
|
releaseTag: "v2026.3.9",
|
||||||
|
now: new Date("2026-03-11T12:00:00Z"),
|
||||||
|
}),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects versions outside the two-day CalVer window", () => {
|
||||||
|
expect(
|
||||||
|
collectReleaseTagErrors({
|
||||||
|
packageVersion: "2026.3.9",
|
||||||
|
releaseTag: "v2026.3.9",
|
||||||
|
now: new Date("2026-03-12T00:00:00Z"),
|
||||||
|
}),
|
||||||
|
).toContainEqual(expect.stringContaining("must be within 2 days"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects tags that do not match the current release format", () => {
|
||||||
|
expect(
|
||||||
|
collectReleaseTagErrors({
|
||||||
|
packageVersion: "2026.3.9",
|
||||||
|
releaseTag: "v2026.3.9-1",
|
||||||
|
now: new Date("2026-03-09T00:00:00Z"),
|
||||||
|
}),
|
||||||
|
).toContainEqual(expect.stringContaining("must match vYYYY.M.D or vYYYY.M.D-beta.N"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("collectReleasePackageMetadataErrors", () => {
|
||||||
|
it("validates the expected npm package metadata", () => {
|
||||||
|
expect(
|
||||||
|
collectReleasePackageMetadataErrors({
|
||||||
|
name: "openclaw",
|
||||||
|
description: "Multi-channel AI gateway with extensible messaging integrations",
|
||||||
|
license: "MIT",
|
||||||
|
repository: { url: "git+https://github.com/openclaw/openclaw.git" },
|
||||||
|
bin: { openclaw: "openclaw.mjs" },
|
||||||
|
}),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user