Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion packages/cli/test/release-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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"));
Expand Down
16 changes: 11 additions & 5 deletions scripts/release-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,15 @@ export async function npmVersions(name: string): Promise<string[]> {
}

/**
* 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 `## <version>` section. */
export const hasChangelogEntry = (text: string | undefined, version: string): boolean =>
Expand All @@ -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 `## <version>`.
* 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 `## <version>`.
* Every package already released keeps being published as before.
*/
export async function releaseGroup(
Expand All @@ -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 };
Expand Down
Loading