fix(release): keep two extended-stable months eligible (#154777)

Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com>
This commit is contained in:
RoboClaw 2026-09-21 04:41:53 -07:00 • committed by GitHub
parent 18d49c068e
commit 32fbb3c6f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 58 additions and 36 deletions

View file

@ -20,9 +20,9 @@ Read `extended-stable-backports.md`; a clean cherry-pick, green
release checks, or a regenerated baseline does not by itself explain the
maintenance risk.
Use this path only for the trailing completed month's `.33+` Gateway
distribution: the `openclaw` npm package, official npm plugins, and matching
Docker Gateway images. Treat
Use this path only for a `.33+` Gateway distribution from either of the two
trailing completed months: the `openclaw` npm package, official npm plugins,
and matching Docker Gateway images. Treat
`docs/reference/RELEASING.md`,
`scripts/openclaw-npm-extended-stable-release.mjs`, and the release workflows
on pinned current `main` as the exact command and validation contract.

View file

@ -20,12 +20,12 @@ train. Continue an existing beta train with its next `beta.N` when appropriate,
otherwise increment the highest stable/beta patch and start at `beta.1`.
Prefer `-beta.N`, never new numeric-only beta suffixes.
| Track | Branch/version | Registry selector |
| --------------- | -------------------------------------------------------------- | ---------------------------------------------------------------- |
| Regular beta | `release/YYYY.M.PATCH`, `YYYY.M.PATCH-beta.N` | `beta` |
| Regular stable | `release/YYYY.M.PATCH`, `YYYY.M.PATCH` | `beta` by default; intentional publication/promotion to `latest` |
| Extended stable | `extended-stable/YYYY.M.33`, trailing completed month's `.33+` | `extended-stable` |
| Development | moving main | not a release |
| Track | Branch/version | Registry selector |
| --------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------- |
| Regular beta | `release/YYYY.M.PATCH`, `YYYY.M.PATCH-beta.N` | `beta` |
| Regular stable | `release/YYYY.M.PATCH`, `YYYY.M.PATCH` | `beta` by default; intentional publication/promotion to `latest` |
| Extended stable | `extended-stable/YYYY.M.33`, either trailing completed month's `.33+` | `extended-stable` |
| Development | moving main | not a release |
Use the release preparation controller before manual version edits:

View file

@ -11,15 +11,15 @@ read_when:
OpenClaw exposes four user-facing update channels:
- stable: the promoted regular release on npm `latest`
- extended-stable: the trailing completed month's `.33+` maintenance line on
npm `extended-stable`
- extended-stable: a `.33+` maintenance line from either of the two trailing
completed months on npm `extended-stable`
- beta: prerelease tags on npm `beta`
- dev: the moving head of `main`
Extended-stable ships the trailing month's Gateway, official npm plugins, and
Docker images without moving regular `latest` or `main` selectors. Each release
also has a GitHub Release with shared validation evidence that is never marked
Latest.
Extended-stable ships a Gateway from either of the two trailing completed
months, along with official npm plugins and Docker images, without moving
regular `latest` or `main` selectors. Each release also has a GitHub Release
with shared validation evidence that is never marked Latest.
Tideclaw alpha builds are a separate internal prerelease track (npm dist-tag `alpha`), covered under [NPM workflow inputs](#npm-workflow-inputs) and [Release test boxes](#release-test-boxes).
@ -35,7 +35,7 @@ Tideclaw alpha builds are a separate internal prerelease track (npm dist-tag `al
- Alpha/nightly builds use the next unreleased patch train and increment only `alpha.N` for repeated builds. Once that patch has a beta, new alpha builds move to the following patch.
- npm versions are immutable: never delete, republish, or reuse a published tag. Cut the next prerelease number or the next monthly patch instead.
- `latest` continues to follow the current regular/daily npm line. For core and every published official plugin, `beta` must always resolve to a version greater than or equal to `latest` under semver ordering; a same-train prerelease is older than its final release.
- `extended-stable` means the supported trailing-month Gateway distribution, beginning at patch `33`; patch `34` and later are maintenance releases on that monthly line
- `extended-stable` means a supported Gateway distribution from either trailing completed month, beginning at patch `33`; patch `34` and later are maintenance releases on that monthly line
- Regular final and regular correction releases publish to npm `beta` by default; release operators can target `latest` explicitly, or promote a vetted beta build later
- Gateway extended-stable publishes core, every npm-publishable official plugin,
and its Docker images at one exact version; see the dedicated workflow below.
@ -164,9 +164,10 @@ bytes still require their own qualification.
For completed month `YYYY.M`, create `extended-stable/YYYY.M.33` and publish
`.33+` from that branch. Tag, branch, checkout, package version, preflight, and
validation must identify one commit. Before `.33`, protected `main` must contain
a final version below patch `33` exactly one calendar month later, making the
release the trailing completed month. Maintenance patches remain eligible only
while that holds; the older line retires when `main` advances another month.
a final version below patch `33` one or two calendar months later, making the
release one of the two trailing completed months. Maintenance patches remain
eligible only while that holds; the older line retires when `main` advances a
third month.
The shared publisher checks live `main` before dispatching publication children;
each plugin checks it again immediately before npm publication, including
trusted-main recovery. Saved qualification does not authorize a retired line.

View file

@ -203,12 +203,19 @@ export function validateActiveExtendedStableLine(releaseVersion, mainPackageVers
}
const mainCalendarMonth = mainVersion.year * 12 + mainVersion.month;
const releaseCalendarMonth = releaseVersionParsed.year * 12 + releaseVersionParsed.month;
// Keep one active trailing-month line; advancing main another month retires the older line.
if (mainCalendarMonth - releaseCalendarMonth !== 1) {
const expectedYear = mainVersion.month === 1 ? mainVersion.year - 1 : mainVersion.year;
const expectedMonth = mainVersion.month === 1 ? 12 : mainVersion.month - 1;
// Keep both trailing completed months eligible so maintenance can finish shortly after
// main enters a new month. Advancing main a third month retires the older line.
const monthDifference = mainCalendarMonth - releaseCalendarMonth;
if (monthDifference < 1 || monthDifference > 2) {
const allowedMonths = [mainCalendarMonth - 1, mainCalendarMonth - 2]
.map((calendarMonth) => {
const year = Math.floor((calendarMonth - 1) / 12);
const month = ((calendarMonth - 1) % 12) + 1;
return `${year}.${month}`;
})
.join(" or ");
throw new Error(
`Extended-stable publishes only the trailing completed month: protected main ${mainPackageVersion} allows ${expectedYear}.${expectedMonth}.PATCH, not ${releaseVersion}. Retire the older line; publishing a retired line requires an explicit maintainer decision.`,
`Extended-stable publishes only the two trailing completed months: protected main ${mainPackageVersion} allows ${allowedMonths}.PATCH, not ${releaseVersion}. Retire the older line; publishing a retired line requires an explicit maintainer decision.`,
);
}
if (classifyReleaseTrain(mainVersion) !== "stable") {

View file

@ -11,19 +11,21 @@ const nodeExecutable = resolveTestNodeExecPath();
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
describe("extended-stable live publication eligibility", () => {
it("retires the same qualified candidate when main advances, including the year boundary", () => {
it("keeps both trailing months eligible across the year boundary", () => {
expect(() => validateActiveExtendedStableLine("2026.12.34", "2027.1.1")).not.toThrow();
expect(() => validateActiveExtendedStableLine("2026.12.34", "2027.2.1")).toThrow(
"only the trailing completed month",
expect(() => validateActiveExtendedStableLine("2026.12.34", "2027.2.1")).not.toThrow();
expect(() => validateActiveExtendedStableLine("2026.12.34", "2027.3.1")).toThrow(
"only the two trailing completed months",
);
});
it.skipIf(process.platform === "win32").each([
{ mainVersion: "2026.8.1", expectedStatus: 42, expectedError: "" },
{ mainVersion: "2026.9.1", expectedStatus: 42, expectedError: "" },
{
mainVersion: "2026.9.1",
mainVersion: "2026.10.1",
expectedStatus: 1,
expectedError: "only the trailing completed month",
expectedError: "only the two trailing completed months",
},
{
mainVersion: "unavailable",

View file

@ -224,7 +224,7 @@ describe("extended-stable npm release request", () => {
mainPackageVersion: "2026.7.2",
};
it("accepts .33 and later patches only in the trailing completed month", () => {
it("accepts .33 and later patches in either trailing completed month", () => {
expect(validateExtendedStableNpmReleaseRequest(valid)).toEqual({
extendedStable: true,
releaseVersion: "2026.6.33",
@ -237,6 +237,12 @@ describe("extended-stable npm release request", () => {
packageVersion: "2026.6.34",
}),
).toMatchObject({ extendedStable: true, releaseVersion: "2026.6.34" });
expect(
validateExtendedStableNpmReleaseRequest({
...valid,
mainPackageVersion: "2026.8.1",
}),
).toMatchObject({ extendedStable: true, releaseVersion: "2026.6.33" });
expect(
validateExtendedStableNpmReleaseRequest({
...valid,
@ -252,12 +258,12 @@ describe("extended-stable npm release request", () => {
});
it.each([
["main two months ahead", "2026.8.1", "2026.7"],
["main many months ahead", "2027.1.1", "2026.12"],
["main a year-plus ahead", "2028.12.32", "2028.11"],
])("rejects %s", (_label, mainPackageVersion, expectedMonth) => {
["main three months ahead", "2026.9.1", "2026.8 or 2026.7"],
["main many months ahead", "2027.1.1", "2026.12 or 2026.11"],
["main a year-plus ahead", "2028.12.32", "2028.11 or 2028.10"],
])("rejects %s", (_label, mainPackageVersion, expectedMonths) => {
expect(() => validateExtendedStableNpmReleaseRequest({ ...valid, mainPackageVersion })).toThrow(
`Extended-stable publishes only the trailing completed month: protected main ${mainPackageVersion} allows ${expectedMonth}.PATCH, not 2026.6.33. Retire the older line; publishing a retired line requires an explicit maintainer decision.`,
`Extended-stable publishes only the two trailing completed months: protected main ${mainPackageVersion} allows ${expectedMonths}.PATCH, not 2026.6.33. Retire the older line; publishing a retired line requires an explicit maintainer decision.`,
);
});

View file

@ -480,7 +480,13 @@ describe("plugin npm extended-stable workflow", () => {
toolingTrusted: true,
candidateMoved: false,
mainVersion: "2026.9.1",
expectedFailure: "only the trailing completed month",
},
{
publishTag: "extended-stable",
toolingTrusted: true,
candidateMoved: false,
mainVersion: "2026.10.1",
expectedFailure: "only the two trailing completed months",
},
{
publishTag: "extended-stable",