From f29c642c13d127ddf5ce585ef2e1f873e1608c58 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Sat, 28 Feb 2026 10:18:10 +0530 Subject: [PATCH] fix(release): enforce lane floor for calver appcast entries --- scripts/release-check.ts | 27 ++++++++++++++++++++------- test/release-check.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 test/release-check.test.ts diff --git a/scripts/release-check.ts b/scripts/release-check.ts index 3ed05b4c85f..9016382aa09 100755 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -3,6 +3,7 @@ import { execSync } from "node:child_process"; import { readdirSync, readFileSync } from "node:fs"; import { join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; import { sparkleBuildFloorsFromShortVersion, type SparkleBuildFloors } from "./sparkle-build.ts"; type PackFile = { path: string }; @@ -17,6 +18,8 @@ const requiredPathGroups = [ ]; const forbiddenPrefixes = ["dist/OpenClaw.app/"]; const appcastPath = resolve("appcast.xml"); +const laneBuildMin = 1_000_000_000; +const laneFloorAdoptionDateKey = 20260227; type PackageJson = { name?: string; @@ -95,8 +98,7 @@ function extractTag(item: string, tag: string): string | null { return regex.exec(item)?.[1]?.trim() ?? null; } -function checkAppcastSparkleVersions() { - const xml = readFileSync(appcastPath, "utf8"); +export function collectAppcastSparkleVersionErrors(xml: string): string[] { const itemMatches = [...xml.matchAll(/([\s\S]*?)<\/item>/g)]; const errors: string[] = []; const calverItems: Array<{ title: string; sparkleBuild: number; floors: SparkleBuildFloors }> = @@ -131,15 +133,18 @@ function checkAppcastSparkleVersions() { calverItems.push({ title, sparkleBuild: Number(sparkleVersion), floors }); } - const adoptionDateKey = calverItems - .filter((item) => item.sparkleBuild >= 1_000_000_000) + const observedLaneAdoptionDateKey = calverItems + .filter((item) => item.sparkleBuild >= laneBuildMin) .map((item) => item.floors.dateKey) .toSorted((a, b) => a - b)[0]; + const effectiveLaneAdoptionDateKey = + typeof observedLaneAdoptionDateKey === "number" + ? Math.min(observedLaneAdoptionDateKey, laneFloorAdoptionDateKey) + : laneFloorAdoptionDateKey; for (const item of calverItems) { const expectLaneFloor = - item.sparkleBuild >= 1_000_000_000 || - (typeof adoptionDateKey === "number" && item.floors.dateKey >= adoptionDateKey); + item.sparkleBuild >= laneBuildMin || item.floors.dateKey >= effectiveLaneAdoptionDateKey; const floor = expectLaneFloor ? item.floors.laneFloor : item.floors.legacyFloor; if (item.sparkleBuild < floor) { const floorLabel = expectLaneFloor ? "lane floor" : "legacy floor"; @@ -149,6 +154,12 @@ function checkAppcastSparkleVersions() { } } + return errors; +} + +function checkAppcastSparkleVersions() { + const xml = readFileSync(appcastPath, "utf8"); + const errors = collectAppcastSparkleVersionErrors(xml); if (errors.length > 0) { console.error("release-check: appcast sparkle version validation failed:"); for (const error of errors) { @@ -197,4 +208,6 @@ function main() { console.log("release-check: npm pack contents look OK."); } -main(); +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { + main(); +} diff --git a/test/release-check.test.ts b/test/release-check.test.ts new file mode 100644 index 00000000000..f1fd5c60622 --- /dev/null +++ b/test/release-check.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { collectAppcastSparkleVersionErrors } from "../scripts/release-check.ts"; + +function makeItem(shortVersion: string, sparkleVersion: string): string { + return `${shortVersion}${shortVersion}${sparkleVersion}`; +} + +describe("collectAppcastSparkleVersionErrors", () => { + it("accepts legacy 9-digit calver builds before lane-floor cutover", () => { + const xml = `${makeItem("2026.2.26", "202602260")}`; + + expect(collectAppcastSparkleVersionErrors(xml)).toEqual([]); + }); + + it("requires lane-floor builds on and after lane-floor cutover", () => { + const xml = `${makeItem("2026.2.27", "202602270")}`; + + expect(collectAppcastSparkleVersionErrors(xml)).toEqual([ + "appcast item '2026.2.27' has sparkle:version 202602270 below lane floor 2026022790.", + ]); + }); + + it("accepts canonical stable lane builds on and after lane-floor cutover", () => { + const xml = `${makeItem("2026.2.27", "2026022790")}`; + + expect(collectAppcastSparkleVersionErrors(xml)).toEqual([]); + }); +});