diff --git a/packages/cli/test/release-plan.test.ts b/packages/cli/test/release-plan.test.ts index 3fecdb71..fde2ae64 100644 --- a/packages/cli/test/release-plan.test.ts +++ b/packages/cli/test/release-plan.test.ts @@ -2,6 +2,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { + isNotRealVersion, isSecurityRelease, type Pkg, readPackages, @@ -122,7 +123,8 @@ describe("a package that joined the fixed group before its first version PR (#23 name === NEW ? forNew : ["0.8.0", released]; it("is left out of the release: nothing publishes it at the group's current version", async () => { - for (const onNpmNew of [[], ["0.0.0-placeholder.0"]]) { + // Nothing; the owner's placeholder; and npm's own `0.0.0-stage` next to it (2026-11-15). + for (const onNpmNew of [[], ["0.0.0-placeholder.0"], ["0.0.0-placeholder.0", "0.0.0-stage"]]) { const pkgs = at("0.9.0"); expect(pkgs.map((p) => p.name)).toContain(NEW); const r = await releaseGroup( @@ -168,6 +170,25 @@ describe("a package that joined the fixed group before its first version PR (#23 expect(missing.map((p) => p.name)).toContain(NEW); }); + it("counts no 0.0.0-* prerelease as a release, and every other version as one", () => { + for (const v of ["0.0.0-placeholder.0", "0.0.0-stage", "0.0.0-anything.1"]) + expect(isNotRealVersion(v)).toBe(true); + for (const v of ["0.0.0", "0.9.0", "0.10.0-next.1", "1.0.0-0.0.0-x"]) + expect(isNotRealVersion(v)).toBe(false); + }); + + it("is released by its first version PR after npm's 0.0.0-stage too", async () => { + const pkgs = at("0.10.0"); + const r = await releaseGroup( + pkgs, + "0.10.0", + changelogs("0.10.0", true), + npm(["0.0.0-placeholder.0", "0.0.0-stage"], "0.9.0"), + ); + expect(r.notYetReleased).toEqual([]); + expect((await unpublished(r.group, async () => false)).map((p) => p.name)).toContain(NEW); + }); + it("never leaves out a package npm has released, even without a changelog entry", async () => { const pkgs = at("0.9.1"); const r = await releaseGroup(pkgs, "0.9.1", () => undefined, npm(["0.9.0"], "0.9.0")); diff --git a/scripts/release-plan.ts b/scripts/release-plan.ts index 5aebde6d..7a1caf99 100644 --- a/scripts/release-plan.ts +++ b/scripts/release-plan.ts @@ -60,10 +60,15 @@ export async function npmVersions(name: string): Promise { } /** - * The deprecated placeholder an owner publishes by hand so that npm trusted publishing can be - * set up for a new package (RFC-0046 §11a). It is never a release of the package. + * A `0.0.0-*` prerelease, which is never a release of a package. Two kinds exist on npm today: + * - the deprecated placeholder an owner publishes by hand so that npm trusted publishing can be + * set up for a new package (`0.0.0-placeholder.0`, RFC-0046 §11a); + * - versions npm creates by itself around that first publish, such as `0.0.0-stage` from staged + * publishing (seen on `@cloudhal/release-verify` on 2026-11-15). + * No Cloudhal release is ever `0.0.0-*`: the fixed group started at 0.1.0. A plain `0.0.0` is not + * matched. */ -export const isPlaceholderVersion = (v: string): boolean => v.startsWith("0.0.0-placeholder."); +export const isNotRealVersion = (v: string): boolean => v.startsWith("0.0.0-"); /** Whether a changelog has a `## ` section. */ export const hasChangelogEntry = (text: string | undefined, version: string): boolean => @@ -81,7 +86,8 @@ export interface ReleaseGroup { * group in one PR and gets its first version in the next version PR. In between, `main` holds it * at the group's current version with no changelog entry for it. That version was never released * for this package and must not be published from `main` (#232). Such a package is one npm holds - * no real version of (nothing, or only the placeholder) and whose changelog has no `## `. + * no real version of (nothing, or only `0.0.0-*` prereleases such as the placeholder and npm's + * `0.0.0-stage`) and whose changelog has no `## `. * Every package already released keeps being published as before. */ export async function releaseGroup( @@ -93,7 +99,7 @@ export async function releaseGroup( const notYetReleased: Pkg[] = []; for (const p of pkgs) { if (hasChangelogEntry(changelogOf(p), version)) continue; - const real = (await versionsOf(p.name)).filter((v) => !isPlaceholderVersion(v)); + const real = (await versionsOf(p.name)).filter((v) => !isNotRealVersion(v)); if (real.length === 0) notYetReleased.push(p); } return { group: pkgs.filter((p) => !notYetReleased.includes(p)), notYetReleased };