mirror of https://github.com/openclaw/openclaw.git
ci: allow fallback npm correction tags (#46486)
This commit is contained in:
parent
8db6fcca77
commit
d33f3f843a
|
|
@ -7,7 +7,7 @@ on:
|
|||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Release tag to publish (for example v2026.3.14 or v2026.3.14-beta.1)
|
||||
description: Release tag to publish (for example v2026.3.14, v2026.3.14-beta.1, or fallback v2026.3.14-1)
|
||||
required: true
|
||||
type: string
|
||||
|
||||
|
|
@ -47,9 +47,18 @@ jobs:
|
|||
set -euo pipefail
|
||||
RELEASE_SHA=$(git rev-parse HEAD)
|
||||
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
||||
if [[ "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*-[1-9][0-9]*$ ]]; then
|
||||
TAG_KIND="fallback correction"
|
||||
else
|
||||
TAG_KIND="standard"
|
||||
fi
|
||||
echo "Release plan for ${RELEASE_TAG}:"
|
||||
echo "Resolved release SHA: ${RELEASE_SHA}"
|
||||
echo "Resolved package version: ${PACKAGE_VERSION}"
|
||||
echo "Resolved tag kind: ${TAG_KIND}"
|
||||
if [[ "${TAG_KIND}" == "fallback correction" ]]; then
|
||||
echo "Correction tag note: npm version remains ${PACKAGE_VERSION}"
|
||||
fi
|
||||
echo "Would run: git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main"
|
||||
echo "Would run with env: RELEASE_SHA=${RELEASE_SHA} RELEASE_TAG=${RELEASE_TAG} RELEASE_MAIN_REF=origin/main pnpm release:openclaw:npm:check"
|
||||
echo "Would run: npm view openclaw@${PACKAGE_VERSION} version"
|
||||
|
|
@ -71,16 +80,31 @@ jobs:
|
|||
pnpm release:openclaw:npm:check
|
||||
|
||||
- name: Ensure version is not already published
|
||||
env:
|
||||
RELEASE_TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
||||
IS_CORRECTION_TAG=0
|
||||
if [[ "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*-[1-9][0-9]*$ ]]; then
|
||||
IS_CORRECTION_TAG=1
|
||||
fi
|
||||
|
||||
if npm view "openclaw@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
|
||||
if [[ "${IS_CORRECTION_TAG}" == "1" ]]; then
|
||||
echo "openclaw@${PACKAGE_VERSION} is already published on npm."
|
||||
echo "Correction tag ${RELEASE_TAG} is allowed as a fallback release tag, so preview will continue without treating this as an error."
|
||||
exit 0
|
||||
fi
|
||||
echo "openclaw@${PACKAGE_VERSION} is already published on npm."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${IS_CORRECTION_TAG}" == "1" ]]; then
|
||||
echo "Previewing fallback correction tag ${RELEASE_TAG} for npm version openclaw@${PACKAGE_VERSION}"
|
||||
else
|
||||
echo "Previewing openclaw@${PACKAGE_VERSION}"
|
||||
fi
|
||||
|
||||
- name: Check
|
||||
run: |
|
||||
|
|
@ -114,7 +138,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]*((-beta\.[1-9][0-9]*)|(-[1-9][0-9]*))?$ ]]; then
|
||||
echo "Invalid release tag format: ${RELEASE_TAG}"
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ Current OpenClaw releases use date-based versioning.
|
|||
- 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`
|
||||
- Fallback correction tag: `vYYYY.M.D-N`
|
||||
- Use only as a last-resort recovery tag when a published immutable release burned the original stable tag and you cannot reuse it.
|
||||
- The npm package version stays `YYYY.M.D`; the `-N` suffix is only for the git tag and GitHub release.
|
||||
- Prefer betas for normal pre-release iteration, then cut a clean stable tag once ready.
|
||||
- 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`
|
||||
|
|
@ -38,12 +42,12 @@ Current OpenClaw releases use date-based versioning.
|
|||
- `latest` = stable
|
||||
- `beta` = prerelease/testing
|
||||
- Dev is the moving head of `main`, not a normal git-tagged release.
|
||||
- The tag-triggered preview run 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.
|
||||
- The tag-triggered preview run accepts stable, beta, and fallback correction tags, 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.
|
||||
- Treat correction tags as a fallback-only escape hatch. New releases should still use `vYYYY.M.D` for stable and `vYYYY.M.D-beta.N` for beta.
|
||||
|
||||
1. **Version & metadata**
|
||||
|
||||
|
|
@ -99,7 +103,9 @@ Historical note:
|
|||
- [ ] Run `OpenClaw NPM Release` manually with the same tag to publish after `npm-release` environment approval.
|
||||
- Stable tags publish to npm `latest`.
|
||||
- Beta tags publish to npm `beta`.
|
||||
- Both the preview run and the manual publish run reject 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.
|
||||
- Fallback correction tags like `v2026.3.13-1` map to npm version `2026.3.13`.
|
||||
- Both the preview run and the manual publish run reject tags that do not map back to `package.json`, are not on `main`, or whose CalVer date is more than 2 UTC calendar days away from the release date.
|
||||
- If `openclaw@YYYY.M.D` is already published, a fallback correction tag is still useful for GitHub release and Docker recovery, but npm publish will not republish that version.
|
||||
- [ ] 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)
|
||||
|
|
@ -109,8 +115,9 @@ Historical note:
|
|||
- `NPM_CONFIG_AUTH_TYPE=legacy npm dist-tag add openclaw@X.Y.Z latest`
|
||||
- **`npx` verification fails with `ECOMPROMISED: Lock compromised`**: retry with a fresh cache:
|
||||
- `NPM_CONFIG_CACHE=/tmp/npm-cache-$(date +%s) npx -y openclaw@X.Y.Z --version`
|
||||
- **Tag needs repointing after a late fix**: force-update and push the tag, then ensure the GitHub release assets still match:
|
||||
- `git tag -f vX.Y.Z && git push -f origin vX.Y.Z`
|
||||
- **Tag needs recovery after a late fix**: if the original stable tag is tied to an immutable GitHub release, mint a fallback correction tag like `vX.Y.Z-1` instead of trying to force-update `vX.Y.Z`.
|
||||
- Keep the npm package version at `X.Y.Z`; the correction suffix is for the git tag and GitHub release only.
|
||||
- Use this only as a last resort. For normal iteration, prefer beta tags and then cut a clean stable release.
|
||||
|
||||
7. **GitHub release + appcast**
|
||||
|
||||
|
|
|
|||
|
|
@ -25,9 +25,18 @@ export type ParsedReleaseVersion = {
|
|||
date: Date;
|
||||
};
|
||||
|
||||
export type ParsedReleaseTag = {
|
||||
version: string;
|
||||
packageVersion: string;
|
||||
channel: "stable" | "beta";
|
||||
correctionNumber?: 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 CORRECTION_TAG_REGEX = /^(?<base>\d{4}\.[1-9]\d?\.[1-9]\d?)-(?<correction>[1-9]\d*)$/;
|
||||
const EXPECTED_REPOSITORY_URL = "https://github.com/openclaw/openclaw";
|
||||
const MAX_CALVER_DISTANCE_DAYS = 2;
|
||||
|
||||
|
|
@ -107,6 +116,49 @@ export function parseReleaseVersion(version: string): ParsedReleaseVersion | nul
|
|||
return null;
|
||||
}
|
||||
|
||||
export function parseReleaseTagVersion(version: string): ParsedReleaseTag | null {
|
||||
const trimmed = version.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedVersion = parseReleaseVersion(trimmed);
|
||||
if (parsedVersion !== null) {
|
||||
return {
|
||||
version: trimmed,
|
||||
packageVersion: parsedVersion.version,
|
||||
channel: parsedVersion.channel,
|
||||
date: parsedVersion.date,
|
||||
correctionNumber: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const correctionMatch = CORRECTION_TAG_REGEX.exec(trimmed);
|
||||
if (!correctionMatch?.groups) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseVersion = correctionMatch.groups.base ?? "";
|
||||
const parsedBaseVersion = parseReleaseVersion(baseVersion);
|
||||
const correctionNumber = Number.parseInt(correctionMatch.groups.correction ?? "", 10);
|
||||
if (
|
||||
parsedBaseVersion === null ||
|
||||
parsedBaseVersion.channel !== "stable" ||
|
||||
!Number.isInteger(correctionNumber) ||
|
||||
correctionNumber < 1
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
version: trimmed,
|
||||
packageVersion: parsedBaseVersion.version,
|
||||
channel: "stable",
|
||||
correctionNumber,
|
||||
date: parsedBaseVersion.date,
|
||||
};
|
||||
}
|
||||
|
||||
function startOfUtcDay(date: Date): number {
|
||||
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
||||
}
|
||||
|
|
@ -180,19 +232,25 @@ export function collectReleaseTagErrors(params: {
|
|||
}
|
||||
|
||||
const tagVersion = releaseTag.startsWith("v") ? releaseTag.slice(1) : releaseTag;
|
||||
const parsedTag = parseReleaseVersion(tagVersion);
|
||||
const parsedTag = parseReleaseTagVersion(tagVersion);
|
||||
if (parsedTag === null) {
|
||||
errors.push(
|
||||
`Release tag must match vYYYY.M.D or vYYYY.M.D-beta.N; found "${releaseTag || "<missing>"}".`,
|
||||
`Release tag must match vYYYY.M.D, vYYYY.M.D-beta.N, or fallback correction tag vYYYY.M.D-N; found "${releaseTag || "<missing>"}".`,
|
||||
);
|
||||
}
|
||||
|
||||
const expectedTag = packageVersion ? `v${packageVersion}` : "";
|
||||
if (releaseTag !== expectedTag) {
|
||||
const expectedTag = packageVersion ? `v${packageVersion}` : "<missing>";
|
||||
const expectedCorrectionTag = parsedVersion?.channel === "stable" ? `${expectedTag}-N` : null;
|
||||
const matchesExpectedTag =
|
||||
parsedTag !== null &&
|
||||
parsedVersion !== null &&
|
||||
parsedTag.packageVersion === parsedVersion.version &&
|
||||
parsedTag.channel === parsedVersion.channel;
|
||||
if (!matchesExpectedTag) {
|
||||
errors.push(
|
||||
`Release tag ${releaseTag || "<missing>"} does not match package.json version ${
|
||||
packageVersion || "<missing>"
|
||||
}; expected ${expectedTag || "<missing>"}.`,
|
||||
}; expected ${expectedCorrectionTag ? `${expectedTag} or ${expectedCorrectionTag}` : expectedTag}.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||
import {
|
||||
collectReleasePackageMetadataErrors,
|
||||
collectReleaseTagErrors,
|
||||
parseReleaseTagVersion,
|
||||
parseReleaseVersion,
|
||||
utcCalendarDayDistance,
|
||||
} from "../scripts/openclaw-npm-release-check.ts";
|
||||
|
|
@ -37,6 +38,22 @@ describe("parseReleaseVersion", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("parseReleaseTagVersion", () => {
|
||||
it("accepts fallback correction tags for stable releases", () => {
|
||||
expect(parseReleaseTagVersion("2026.3.10-2")).toMatchObject({
|
||||
version: "2026.3.10-2",
|
||||
packageVersion: "2026.3.10",
|
||||
channel: "stable",
|
||||
correctionNumber: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects beta correction tags and malformed correction tags", () => {
|
||||
expect(parseReleaseTagVersion("2026.3.10-beta.1-1")).toBeNull();
|
||||
expect(parseReleaseTagVersion("2026.3.10-0")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("utcCalendarDayDistance", () => {
|
||||
it("compares UTC calendar days rather than wall-clock hours", () => {
|
||||
const left = new Date("2026-03-09T23:59:59Z");
|
||||
|
|
@ -66,14 +83,24 @@ describe("collectReleaseTagErrors", () => {
|
|||
).toContainEqual(expect.stringContaining("must be within 2 days"));
|
||||
});
|
||||
|
||||
it("rejects tags that do not match the current release format", () => {
|
||||
it("accepts fallback correction tags for stable package versions", () => {
|
||||
expect(
|
||||
collectReleaseTagErrors({
|
||||
packageVersion: "2026.3.10",
|
||||
releaseTag: "v2026.3.10-1",
|
||||
now: new Date("2026-03-10T00:00:00Z"),
|
||||
}),
|
||||
).toContainEqual(expect.stringContaining("must match vYYYY.M.D or vYYYY.M.D-beta.N"));
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("rejects beta package versions paired with fallback correction tags", () => {
|
||||
expect(
|
||||
collectReleaseTagErrors({
|
||||
packageVersion: "2026.3.10-beta.1",
|
||||
releaseTag: "v2026.3.10-1",
|
||||
now: new Date("2026-03-10T00:00:00Z"),
|
||||
}),
|
||||
).toContainEqual(expect.stringContaining("does not match package.json version"));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue