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:
|
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
|
||||||
|
|
|
||||||
|
|
@ -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**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue