Skip to content

Commit d4eeb0f

Browse files
committed
ci: allow fallback npm correction tags
1 parent 8db6fcc commit d4eeb0f

File tree

4 files changed

+131
-15
lines changed

4 files changed

+131
-15
lines changed

.github/workflows/openclaw-npm-release.yml

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
workflow_dispatch:
88
inputs:
99
tag:
10-
description: Release tag to publish (for example v2026.3.14 or v2026.3.14-beta.1)
10+
description: Release tag to publish (for example v2026.3.14, v2026.3.14-beta.1, or fallback v2026.3.14-1)
1111
required: true
1212
type: string
1313

@@ -47,9 +47,18 @@ jobs:
4747
set -euo pipefail
4848
RELEASE_SHA=$(git rev-parse HEAD)
4949
PACKAGE_VERSION=$(node -p "require('./package.json').version")
50+
if [[ "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*-[1-9][0-9]*$ ]]; then
51+
TAG_KIND="fallback correction"
52+
else
53+
TAG_KIND="standard"
54+
fi
5055
echo "Release plan for ${RELEASE_TAG}:"
5156
echo "Resolved release SHA: ${RELEASE_SHA}"
5257
echo "Resolved package version: ${PACKAGE_VERSION}"
58+
echo "Resolved tag kind: ${TAG_KIND}"
59+
if [[ "${TAG_KIND}" == "fallback correction" ]]; then
60+
echo "Correction tag note: npm version remains ${PACKAGE_VERSION}"
61+
fi
5362
echo "Would run: git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main"
5463
echo "Would run with env: RELEASE_SHA=${RELEASE_SHA} RELEASE_TAG=${RELEASE_TAG} RELEASE_MAIN_REF=origin/main pnpm release:openclaw:npm:check"
5564
echo "Would run: npm view openclaw@${PACKAGE_VERSION} version"
@@ -71,16 +80,31 @@ jobs:
7180
pnpm release:openclaw:npm:check
7281
7382
- name: Ensure version is not already published
83+
env:
84+
RELEASE_TAG: ${{ github.ref_name }}
7485
run: |
7586
set -euxo pipefail
7687
PACKAGE_VERSION=$(node -p "require('./package.json').version")
88+
IS_CORRECTION_TAG=0
89+
if [[ "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*-[1-9][0-9]*$ ]]; then
90+
IS_CORRECTION_TAG=1
91+
fi
7792
7893
if npm view "openclaw@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
94+
if [[ "${IS_CORRECTION_TAG}" == "1" ]]; then
95+
echo "openclaw@${PACKAGE_VERSION} is already published on npm."
96+
echo "Correction tag ${RELEASE_TAG} is allowed as a fallback release tag, so preview will continue without treating this as an error."
97+
exit 0
98+
fi
7999
echo "openclaw@${PACKAGE_VERSION} is already published on npm."
80100
exit 1
81101
fi
82102
83-
echo "Previewing openclaw@${PACKAGE_VERSION}"
103+
if [[ "${IS_CORRECTION_TAG}" == "1" ]]; then
104+
echo "Previewing fallback correction tag ${RELEASE_TAG} for npm version openclaw@${PACKAGE_VERSION}"
105+
else
106+
echo "Previewing openclaw@${PACKAGE_VERSION}"
107+
fi
84108
85109
- name: Check
86110
run: |
@@ -114,7 +138,7 @@ jobs:
114138
RELEASE_TAG: ${{ inputs.tag }}
115139
run: |
116140
set -euo pipefail
117-
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-beta\.[1-9][0-9]*)?$ ]]; then
141+
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
118142
echo "Invalid release tag format: ${RELEASE_TAG}"
119143
exit 1
120144
fi

docs/reference/RELEASING.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ Current OpenClaw releases use date-based versioning.
2929
- Beta prerelease version: `YYYY.M.D-beta.N`
3030
- Git tag: `vYYYY.M.D-beta.N`
3131
- Examples from repo history: `v2026.2.15-beta.1`, `v2026.3.8-beta.1`
32+
- Fallback correction tag: `vYYYY.M.D-N`
33+
- Use only as a last-resort recovery tag when a published immutable release burned the original stable tag and you cannot reuse it.
34+
- The npm package version stays `YYYY.M.D`; the `-N` suffix is only for the git tag and GitHub release.
35+
- Prefer betas for normal pre-release iteration, then cut a clean stable tag once ready.
3236
- Use the same version string everywhere, minus the leading `v` where Git tags are not used:
3337
- `package.json`: `2026.3.8`
3438
- Git tag: `v2026.3.8`
@@ -38,12 +42,12 @@ Current OpenClaw releases use date-based versioning.
3842
- `latest` = stable
3943
- `beta` = prerelease/testing
4044
- Dev is the moving head of `main`, not a normal git-tagged release.
41-
- 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.
45+
- 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.
4246

4347
Historical note:
4448

4549
- Older tags such as `v2026.1.11-1`, `v2026.2.6-3`, and `v2.0.0-beta2` exist in repo history.
46-
- Treat those as legacy tag patterns. New releases should use `vYYYY.M.D` for stable and `vYYYY.M.D-beta.N` for beta.
50+
- 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.
4751

4852
1. **Version & metadata**
4953

@@ -99,7 +103,9 @@ Historical note:
99103
- [ ] Run `OpenClaw NPM Release` manually with the same tag to publish after `npm-release` environment approval.
100104
- Stable tags publish to npm `latest`.
101105
- Beta tags publish to npm `beta`.
102-
- 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.
106+
- Fallback correction tags like `v2026.3.13-1` map to npm version `2026.3.13`.
107+
- 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.
108+
- If `[email protected]` is already published, a fallback correction tag is still useful for GitHub release and Docker recovery, but npm publish will not republish that version.
103109
- [ ] Verify the registry: `npm view openclaw version`, `npm view openclaw dist-tags`, and `npx -y [email protected] --version` (or `--help`).
104110

105111
### Troubleshooting (notes from 2.0.0-beta2 release)
@@ -109,8 +115,9 @@ Historical note:
109115
- `NPM_CONFIG_AUTH_TYPE=legacy npm dist-tag add [email protected] latest`
110116
- **`npx` verification fails with `ECOMPROMISED: Lock compromised`**: retry with a fresh cache:
111117
- `NPM_CONFIG_CACHE=/tmp/npm-cache-$(date +%s) npx -y [email protected] --version`
112-
- **Tag needs repointing after a late fix**: force-update and push the tag, then ensure the GitHub release assets still match:
113-
- `git tag -f vX.Y.Z && git push -f origin vX.Y.Z`
118+
- **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`.
119+
- Keep the npm package version at `X.Y.Z`; the correction suffix is for the git tag and GitHub release only.
120+
- Use this only as a last resort. For normal iteration, prefer beta tags and then cut a clean stable release.
114121

115122
7. **GitHub release + appcast**
116123

scripts/openclaw-npm-release-check.ts

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,18 @@ export type ParsedReleaseVersion = {
2525
date: Date;
2626
};
2727

28+
export type ParsedReleaseTag = {
29+
version: string;
30+
packageVersion: string;
31+
channel: "stable" | "beta";
32+
correctionNumber?: number;
33+
date: Date;
34+
};
35+
2836
const STABLE_VERSION_REGEX = /^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)$/;
2937
const BETA_VERSION_REGEX =
3038
/^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<day>[1-9]\d?)-beta\.(?<beta>[1-9]\d*)$/;
39+
const CORRECTION_TAG_REGEX = /^(?<base>\d{4}\.[1-9]\d?\.[1-9]\d?)-(?<correction>[1-9]\d*)$/;
3140
const EXPECTED_REPOSITORY_URL = "https://github.com/openclaw/openclaw";
3241
const MAX_CALVER_DISTANCE_DAYS = 2;
3342

@@ -107,6 +116,49 @@ export function parseReleaseVersion(version: string): ParsedReleaseVersion | nul
107116
return null;
108117
}
109118

119+
export function parseReleaseTagVersion(version: string): ParsedReleaseTag | null {
120+
const trimmed = version.trim();
121+
if (!trimmed) {
122+
return null;
123+
}
124+
125+
const parsedVersion = parseReleaseVersion(trimmed);
126+
if (parsedVersion !== null) {
127+
return {
128+
version: trimmed,
129+
packageVersion: parsedVersion.version,
130+
channel: parsedVersion.channel,
131+
date: parsedVersion.date,
132+
correctionNumber: undefined,
133+
};
134+
}
135+
136+
const correctionMatch = CORRECTION_TAG_REGEX.exec(trimmed);
137+
if (!correctionMatch?.groups) {
138+
return null;
139+
}
140+
141+
const baseVersion = correctionMatch.groups.base ?? "";
142+
const parsedBaseVersion = parseReleaseVersion(baseVersion);
143+
const correctionNumber = Number.parseInt(correctionMatch.groups.correction ?? "", 10);
144+
if (
145+
parsedBaseVersion === null ||
146+
parsedBaseVersion.channel !== "stable" ||
147+
!Number.isInteger(correctionNumber) ||
148+
correctionNumber < 1
149+
) {
150+
return null;
151+
}
152+
153+
return {
154+
version: trimmed,
155+
packageVersion: parsedBaseVersion.version,
156+
channel: "stable",
157+
correctionNumber,
158+
date: parsedBaseVersion.date,
159+
};
160+
}
161+
110162
function startOfUtcDay(date: Date): number {
111163
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
112164
}
@@ -180,19 +232,25 @@ export function collectReleaseTagErrors(params: {
180232
}
181233

182234
const tagVersion = releaseTag.startsWith("v") ? releaseTag.slice(1) : releaseTag;
183-
const parsedTag = parseReleaseVersion(tagVersion);
235+
const parsedTag = parseReleaseTagVersion(tagVersion);
184236
if (parsedTag === null) {
185237
errors.push(
186-
`Release tag must match vYYYY.M.D or vYYYY.M.D-beta.N; found "${releaseTag || "<missing>"}".`,
238+
`Release tag must match vYYYY.M.D, vYYYY.M.D-beta.N, or fallback correction tag vYYYY.M.D-N; found "${releaseTag || "<missing>"}".`,
187239
);
188240
}
189241

190-
const expectedTag = packageVersion ? `v${packageVersion}` : "";
191-
if (releaseTag !== expectedTag) {
242+
const expectedTag = packageVersion ? `v${packageVersion}` : "<missing>";
243+
const expectedCorrectionTag = parsedVersion?.channel === "stable" ? `${expectedTag}-N` : null;
244+
const matchesExpectedTag =
245+
parsedTag !== null &&
246+
parsedVersion !== null &&
247+
parsedTag.packageVersion === parsedVersion.version &&
248+
parsedTag.channel === parsedVersion.channel;
249+
if (!matchesExpectedTag) {
192250
errors.push(
193251
`Release tag ${releaseTag || "<missing>"} does not match package.json version ${
194252
packageVersion || "<missing>"
195-
}; expected ${expectedTag || "<missing>"}.`,
253+
}; expected ${expectedCorrectionTag ? `${expectedTag} or ${expectedCorrectionTag}` : expectedTag}.`,
196254
);
197255
}
198256

test/openclaw-npm-release-check.test.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
22
import {
33
collectReleasePackageMetadataErrors,
44
collectReleaseTagErrors,
5+
parseReleaseTagVersion,
56
parseReleaseVersion,
67
utcCalendarDayDistance,
78
} from "../scripts/openclaw-npm-release-check.ts";
@@ -37,6 +38,22 @@ describe("parseReleaseVersion", () => {
3738
});
3839
});
3940

41+
describe("parseReleaseTagVersion", () => {
42+
it("accepts fallback correction tags for stable releases", () => {
43+
expect(parseReleaseTagVersion("2026.3.10-2")).toMatchObject({
44+
version: "2026.3.10-2",
45+
packageVersion: "2026.3.10",
46+
channel: "stable",
47+
correctionNumber: 2,
48+
});
49+
});
50+
51+
it("rejects beta correction tags and malformed correction tags", () => {
52+
expect(parseReleaseTagVersion("2026.3.10-beta.1-1")).toBeNull();
53+
expect(parseReleaseTagVersion("2026.3.10-0")).toBeNull();
54+
});
55+
});
56+
4057
describe("utcCalendarDayDistance", () => {
4158
it("compares UTC calendar days rather than wall-clock hours", () => {
4259
const left = new Date("2026-03-09T23:59:59Z");
@@ -66,14 +83,24 @@ describe("collectReleaseTagErrors", () => {
6683
).toContainEqual(expect.stringContaining("must be within 2 days"));
6784
});
6885

69-
it("rejects tags that do not match the current release format", () => {
86+
it("accepts fallback correction tags for stable package versions", () => {
7087
expect(
7188
collectReleaseTagErrors({
7289
packageVersion: "2026.3.10",
7390
releaseTag: "v2026.3.10-1",
7491
now: new Date("2026-03-10T00:00:00Z"),
7592
}),
76-
).toContainEqual(expect.stringContaining("must match vYYYY.M.D or vYYYY.M.D-beta.N"));
93+
).toEqual([]);
94+
});
95+
96+
it("rejects beta package versions paired with fallback correction tags", () => {
97+
expect(
98+
collectReleaseTagErrors({
99+
packageVersion: "2026.3.10-beta.1",
100+
releaseTag: "v2026.3.10-1",
101+
now: new Date("2026-03-10T00:00:00Z"),
102+
}),
103+
).toContainEqual(expect.stringContaining("does not match package.json version"));
77104
});
78105
});
79106

0 commit comments

Comments
 (0)