diff --git a/.github/scripts/release-pipeline.ts b/.github/scripts/release-pipeline.ts index 327d8370..de41b7a3 100644 --- a/.github/scripts/release-pipeline.ts +++ b/.github/scripts/release-pipeline.ts @@ -61,15 +61,6 @@ export const NEXT_BUILD_INPUTS = [ ".github/scripts/release-pipeline.ts", ".github/workflows/post-green.yml", ] as const; -/** What under src/ the build never packs, so a change to it alone publishes nothing: tests, e2e scenarios and their - * generators, mock handlers, and the docs prose (a trailing "/" names a directory, a leading "*" a file-name suffix). */ -export const NEXT_BUILD_UNPACKED = [ - "*.test.ts", - "scenarios/", - "*.docs.yml", - "mock.ts", - "generators.ts", -] as const; const LATEST_REF = "refs/tags/latest"; const BUILD_TAG_PREFIX = "refs/tags/build/"; const BUILD_TAG = /^refs\/tags\/build\/([1-9]\d*)\.[0-9a-f]{7}$/; @@ -1118,30 +1109,9 @@ function shippedPaths( * `/licen[cs]e{,.*[^~$]}`, any case; a glob star crosses no separator, and an editor backup suffix is left out. */ const ALWAYS_PACKED = /^(?:readme|copying|licen[cs]e)(?:\.[^/]*[^~$/])?$/i; const ALWAYS_PACKED_TEXT = "or a root README, COPYING, or LICENSE"; -const UNPACKED_TEXT = `(under src/, ${NEXT_BUILD_UNPACKED.join(", ")} are never packed and do not count)`; - -/** Whether a path under src/ is one the build never packs. */ -function unpacked(path: string): boolean { - const segments = path.split("/"); - if (segments[0] !== "src") { - return false; - } - const name = segments[segments.length - 1] ?? ""; - return NEXT_BUILD_UNPACKED.some((entry) => - entry.endsWith("/") - ? segments.slice(1, -1).includes(entry.slice(0, -1)) - : entry.startsWith("*") - ? name.endsWith(entry.slice(1)) - : name === entry, - ); -} -/** Whether `path` is one of `shipped`, lies under one of its directories, or is a root file npm always packs; a path - * under src/ the build never packs is not, whatever it lies under. */ +/** Whether `path` is one of `shipped`, lies under one of its directories, or is a root file npm always packs. */ function ships(shipped: string[], path: string): boolean { - if (unpacked(path)) { - return false; - } return ( ALWAYS_PACKED.test(path) || shipped.some((entry) => { @@ -1239,7 +1209,7 @@ export function nextPublishVerdict( return { publish: false, version, - reason: `no shipped file changed since ${next.version} (source ${next.sha.slice(0, 7)}): no merge to main in ${next.sha.slice(0, 7)}..${sourceSha.slice(0, 7)} touches ${shipped.paths.join(", ")}, ${ALWAYS_PACKED_TEXT} ${UNPACKED_TEXT}`, + reason: `no shipped file changed since ${next.version} (source ${next.sha.slice(0, 7)}): no merge to main in ${next.sha.slice(0, 7)}..${sourceSha.slice(0, 7)} touches ${shipped.paths.join(", ")}, ${ALWAYS_PACKED_TEXT}`, notices, }; } diff --git a/docs/reference/library.md b/docs/reference/library.md index 0911890d..48751ef4 100644 --- a/docs/reference/library.md +++ b/docs/reference/library.md @@ -405,7 +405,6 @@ The npm dist-tag `latest` is not the git tag `latest`: the git tag names the pac - the run's own version already on the registry (a rerun of that commit): publishes nothing. - A `next` build appears when the shipped surface changed, not on every merge: the verdict walks the merges to main after the source of the build `next` names, up to this commit, and skips when none touched a shipped path. A docs-only merge publishes nothing, with a notice naming the base version and its source. - The shipped surface: `package.json`, its `files` list, `src/`, the build config, `bun.lock`, the release manifest, and the publish recipe (`NEXT_BUILD_INPUTS` in the pipeline script derives it). - - Under `src/`, what the build never packs does not count: `*.test.ts`, `scenarios/`, `*.docs.yml`, `mock.ts`, and `generators.ts` (`NEXT_BUILD_UNPACKED` lists them). A merge that touches only those publishes nothing. - A base the checkout cannot place (no `next` yet, a release under it, a sha the checkout lacks, off main, or inside a merged branch), or a `files` list that is not plain paths, publishes and says why: nothing to compare against is no reason to hold a build back. - `latest` publishes nothing when the version is already there or when the dist-tag `latest` names a newer release (a rerun of an older release's job); it does not look at `next`. - Every registry read misses the CDN cache (a cached packument lags a publish by up to 300 s), and a `next` publish job holds the npm-publish lane until the registry's record shows its version (up to 15 reads, 20 s apart: three of the first five publishes were still unreadable after 80 s, so the hold covers that lag with margin). diff --git a/test/scripts/release-pipeline.test.ts b/test/scripts/release-pipeline.test.ts index 3c8f6ca2..b3f05818 100644 --- a/test/scripts/release-pipeline.test.ts +++ b/test/scripts/release-pipeline.test.ts @@ -16,7 +16,6 @@ import { type MainPosition, mainPosition, NEXT_BUILD_INPUTS, - NEXT_BUILD_UNPACKED, type NextVerdict, nextPublishVerdict, npmConfirm, @@ -1208,9 +1207,9 @@ describe("prereleaseVersion", () => { ).toThrow(/not a version this pipeline mints/); }); - /** The surface a skip names: the manifest, the fixture manifest's files list (lib/pkg/), the build inputs, npm's - * always-packed root files, and what under src/ does not count. */ - const SHIPPED = `package.json, lib/pkg/, ${NEXT_BUILD_INPUTS.join(", ")}, or a root README, COPYING, or LICENSE (under src/, ${NEXT_BUILD_UNPACKED.join(", ")} are never packed and do not count)`; + /** The surface a skip names: the manifest, the fixture manifest's files list (lib/pkg/), the build inputs, and npm's + * always-packed root files. */ + const SHIPPED = `package.json, lib/pkg/, ${NEXT_BUILD_INPUTS.join(", ")}, or a root README, COPYING, or LICENSE`; const registry = (versions: string[], tags: Record): Packument => ({ versions: Object.fromEntries(versions.map((v) => [v, {}])), @@ -1558,45 +1557,33 @@ describe("prereleaseVersion", () => { }); }); - test("next: under src/, a merge touching only tests, scenarios, docs prose, mock handlers, or generators publishes nothing; one touching packed source publishes", () => { + test("next: a merge touching only a section's tests, scenarios, mock, or docs prose publishes nothing; one touching its module publishes", () => { const fx = seedFixture(); const main = mainAround(fx); const further = git(fx.work, "ls-remote", "origin", "refs/heads/main").split("\t")[0] ?? ""; const lab = clone(fx.root, fx.origin, "lab"); const at = registry(["2.1.0", main.further], { latest: "2.1.0", next: main.further }); - const skipped = (source: string, count: number): NextVerdict => ({ + // A section's tests, scenarios, mock handlers, and docs prose live under test/src/sections// and + // docs/sections/, outside every build input: the merge that published a byte-identical tarball. + write(lab, "test/src/sections/x/x.test.ts", 'test("x", () => {});\n'); + write(lab, "test/src/sections/x/scenarios/a.yml", "name: a\n"); + write(lab, "test/src/sections/x/mock.ts", "export const handlers = [];\n"); + write(lab, "docs/sections/x.docs.yml", "title: X\n"); + const tests = commitAll(lab, "test(x): the cases, a scenario, the mock, and the docs prose"); + expect(nextPublishVerdict(lab, tests, versionOf(lab, tests, 5), at)).toEqual({ publish: false, - version: versionOf(lab, source, count), - reason: `no shipped file changed since ${main.further} (source ${further.slice(0, 7)}): no merge to main in ${further.slice(0, 7)}..${source.slice(0, 7)} touches ${SHIPPED}`, + version: versionOf(lab, tests, 5), + reason: `no shipped file changed since ${main.further} (source ${further.slice(0, 7)}): no merge to main in ${further.slice(0, 7)}..${tests.slice(0, 7)} touches ${SHIPPED}`, notices: [], }); - // A section's tests alone: the merge that published a byte-identical tarball. - write(lab, "src/sections/x/x.test.ts", 'test("x", () => {});\n'); - const tests = commitAll(lab, "test(x): a case"); - expect(nextPublishVerdict(lab, tests, versionOf(lab, tests, 5), at)).toEqual(skipped(tests, 5)); - // An e2e scenario alone, on top of it: two merges since the base, neither packed. - write(lab, "src/sections/x/scenarios/x-basic.yml", "name: x-basic\n"); - const scenario = commitAll(lab, "test(x): a scenario"); - expect(nextPublishVerdict(lab, scenario, versionOf(lab, scenario, 6), at)).toEqual( - skipped(scenario, 6), - ); - // The rest of what the build never packs, in one merge. - write(lab, "src/sections/x/x.docs.yml", "title: X\n"); - write(lab, "src/sections/x/mock.ts", "export const handlers = [];\n"); - write(lab, "src/sections/x/mock.test.ts", 'test("mock", () => {});\n'); - write(lab, "src/sections/x/generators.ts", "export const draw = 1;\n"); - write(lab, "src/schema.docs.yml", "title: Schema\n"); - const rest = commitAll(lab, "test(x): the mock, its generators, and the docs prose"); - expect(nextPublishVerdict(lab, rest, versionOf(lab, rest, 7), at)).toEqual(skipped(rest, 7)); - // A module named like an excluded directory is packed source, and so is the section's own module. - write(lab, "src/sections/x/scenarios.ts", "export const scenarios = 1;\n"); + // The section's own module is packed source. write(lab, "src/sections/x/index.ts", "export const x = 1;\n"); const packed = commitAll(lab, "feat(x): the section"); - expect(nextPublishVerdict(lab, packed, versionOf(lab, packed, 8), at)).toEqual({ + expect(nextPublishVerdict(lab, packed, versionOf(lab, packed, 6), at)).toEqual({ publish: true, - version: versionOf(lab, packed, 8), + version: versionOf(lab, packed, 6), notices: [ - `2 shipped files changed since ${main.further} (source ${further.slice(0, 7)}): src/sections/x/index.ts, src/sections/x/scenarios.ts`, + `1 shipped file changed since ${main.further} (source ${further.slice(0, 7)}): src/sections/x/index.ts`, ], }); });