From 32fbb3c6f3ca82fc889add0a49e993243ca00310 Mon Sep 17 00:00:00 2001 From: RoboClaw Date: Mon, 21 Sep 2026 04:41:53 -0700 Subject: [PATCH] fix(release): keep two extended-stable months eligible (#154777) Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com> --- .../references/extended-stable-publish.md | 6 +++--- .../references/preparation.md | 12 +++++------ docs/reference/RELEASING.md | 21 ++++++++++--------- .../openclaw-npm-extended-stable-release.mjs | 17 ++++++++++----- .../extended-stable-active-line.test.ts | 12 ++++++----- ...enclaw-npm-extended-stable-release.test.ts | 18 ++++++++++------ ...lugin-npm-extended-stable-workflow.test.ts | 8 ++++++- 7 files changed, 58 insertions(+), 36 deletions(-) diff --git a/.agents/skills/release-openclaw-maintainer/references/extended-stable-publish.md b/.agents/skills/release-openclaw-maintainer/references/extended-stable-publish.md index fe8e8ae16378..214d9396508b 100644 --- a/.agents/skills/release-openclaw-maintainer/references/extended-stable-publish.md +++ b/.agents/skills/release-openclaw-maintainer/references/extended-stable-publish.md @@ -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. diff --git a/.agents/skills/release-openclaw-maintainer/references/preparation.md b/.agents/skills/release-openclaw-maintainer/references/preparation.md index 368f0370b3df..a7878ff7db05 100644 --- a/.agents/skills/release-openclaw-maintainer/references/preparation.md +++ b/.agents/skills/release-openclaw-maintainer/references/preparation.md @@ -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: diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index bd79ae5b09e2..a9e07a90a96d 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -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. diff --git a/scripts/openclaw-npm-extended-stable-release.mjs b/scripts/openclaw-npm-extended-stable-release.mjs index 5f8fb37d7cd1..9c3c771f0afe 100644 --- a/scripts/openclaw-npm-extended-stable-release.mjs +++ b/scripts/openclaw-npm-extended-stable-release.mjs @@ -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") { diff --git a/test/scripts/extended-stable-active-line.test.ts b/test/scripts/extended-stable-active-line.test.ts index 68bc2fc42933..9e27304e8999 100644 --- a/test/scripts/extended-stable-active-line.test.ts +++ b/test/scripts/extended-stable-active-line.test.ts @@ -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", diff --git a/test/scripts/openclaw-npm-extended-stable-release.test.ts b/test/scripts/openclaw-npm-extended-stable-release.test.ts index 51e36d31c8e5..6fbafa624ce5 100644 --- a/test/scripts/openclaw-npm-extended-stable-release.test.ts +++ b/test/scripts/openclaw-npm-extended-stable-release.test.ts @@ -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.`, ); }); diff --git a/test/scripts/plugin-npm-extended-stable-workflow.test.ts b/test/scripts/plugin-npm-extended-stable-workflow.test.ts index c4f07012d90e..a807524ae3ed 100644 --- a/test/scripts/plugin-npm-extended-stable-workflow.test.ts +++ b/test/scripts/plugin-npm-extended-stable-workflow.test.ts @@ -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",