ci: allow fallback npm correction tags (#46486)

This commit is contained in:
Onur 2026-03-14 19:38:14 +01:00 committed by GitHub
parent 8db6fcca77
commit d33f3f843a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 131 additions and 15 deletions

View File

@ -7,7 +7,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
tag: 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 required: true
type: string type: string
@ -47,9 +47,18 @@ jobs:
set -euo pipefail set -euo pipefail
RELEASE_SHA=$(git rev-parse HEAD) RELEASE_SHA=$(git rev-parse HEAD)
PACKAGE_VERSION=$(node -p "require('./package.json').version") 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 "Release plan for ${RELEASE_TAG}:"
echo "Resolved release SHA: ${RELEASE_SHA}" echo "Resolved release SHA: ${RELEASE_SHA}"
echo "Resolved package version: ${PACKAGE_VERSION}" 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: 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 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" echo "Would run: npm view openclaw@${PACKAGE_VERSION} version"
@ -71,16 +80,31 @@ jobs:
pnpm release:openclaw:npm:check pnpm release:openclaw:npm:check
- name: Ensure version is not already published - name: Ensure version is not already published
env:
RELEASE_TAG: ${{ github.ref_name }}
run: | run: |
set -euxo pipefail set -euxo pipefail
PACKAGE_VERSION=$(node -p "require('./package.json').version") 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 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." echo "openclaw@${PACKAGE_VERSION} is already published on npm."
exit 1 exit 1
fi 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}" echo "Previewing openclaw@${PACKAGE_VERSION}"
fi
- name: Check - name: Check
run: | run: |
@ -114,7 +138,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]*((-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

@ -29,6 +29,10 @@ Current OpenClaw releases use date-based versioning.
- 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`
- Examples from repo history: `v2026.2.15-beta.1`, `v2026.3.8-beta.1` - 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: - Use the same version string everywhere, minus the leading `v` where Git tags are not used:
- `package.json`: `2026.3.8` - `package.json`: `2026.3.8`
- Git tag: `v2026.3.8` - Git tag: `v2026.3.8`
@ -38,12 +42,12 @@ Current OpenClaw releases use date-based versioning.
- `latest` = stable - `latest` = stable
- `beta` = prerelease/testing - `beta` = prerelease/testing
- Dev is the moving head of `main`, not a normal git-tagged release. - 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: Historical note:
- Older tags such as `v2026.1.11-1`, `v2026.2.6-3`, and `v2.0.0-beta2` exist in repo history. - 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** 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. - [ ] Run `OpenClaw NPM Release` manually with the same tag to publish after `npm-release` environment approval.
- Stable tags publish to npm `latest`. - Stable tags publish to npm `latest`.
- Beta tags publish to npm `beta`. - 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`). - [ ] 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)
@ -109,8 +115,9 @@ Historical note:
- `NPM_CONFIG_AUTH_TYPE=legacy npm dist-tag add openclaw@X.Y.Z latest` - `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: - **`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` - `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: - **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`.
- `git tag -f vX.Y.Z && git push -f origin 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** 7. **GitHub release + appcast**

View File

@ -25,9 +25,18 @@ export type ParsedReleaseVersion = {
date: Date; 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 STABLE_VERSION_REGEX = /^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[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_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 EXPECTED_REPOSITORY_URL = "https://github.com/openclaw/openclaw";
const MAX_CALVER_DISTANCE_DAYS = 2; const MAX_CALVER_DISTANCE_DAYS = 2;
@ -107,6 +116,49 @@ export function parseReleaseVersion(version: string): ParsedReleaseVersion | nul
return null; 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 { function startOfUtcDay(date: Date): number {
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()); 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 tagVersion = releaseTag.startsWith("v") ? releaseTag.slice(1) : releaseTag;
const parsedTag = parseReleaseVersion(tagVersion); const parsedTag = parseReleaseTagVersion(tagVersion);
if (parsedTag === null) { if (parsedTag === null) {
errors.push( 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}` : ""; const expectedTag = packageVersion ? `v${packageVersion}` : "<missing>";
if (releaseTag !== expectedTag) { 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( errors.push(
`Release tag ${releaseTag || "<missing>"} does not match package.json version ${ `Release tag ${releaseTag || "<missing>"} does not match package.json version ${
packageVersion || "<missing>" packageVersion || "<missing>"
}; expected ${expectedTag || "<missing>"}.`, }; expected ${expectedCorrectionTag ? `${expectedTag} or ${expectedCorrectionTag}` : expectedTag}.`,
); );
} }

View File

@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { import {
collectReleasePackageMetadataErrors, collectReleasePackageMetadataErrors,
collectReleaseTagErrors, collectReleaseTagErrors,
parseReleaseTagVersion,
parseReleaseVersion, parseReleaseVersion,
utcCalendarDayDistance, utcCalendarDayDistance,
} from "../scripts/openclaw-npm-release-check.ts"; } 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", () => { describe("utcCalendarDayDistance", () => {
it("compares UTC calendar days rather than wall-clock hours", () => { it("compares UTC calendar days rather than wall-clock hours", () => {
const left = new Date("2026-03-09T23:59:59Z"); const left = new Date("2026-03-09T23:59:59Z");
@ -66,14 +83,24 @@ describe("collectReleaseTagErrors", () => {
).toContainEqual(expect.stringContaining("must be within 2 days")); ).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( expect(
collectReleaseTagErrors({ collectReleaseTagErrors({
packageVersion: "2026.3.10", packageVersion: "2026.3.10",
releaseTag: "v2026.3.10-1", releaseTag: "v2026.3.10-1",
now: new Date("2026-03-10T00:00:00Z"), 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"));
}); });
}); });