Skip to content
Closed
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
34 changes: 2 additions & 32 deletions .github/scripts/release-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}$/;
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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,
};
}
Expand Down
1 change: 0 additions & 1 deletion docs/reference/library.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
49 changes: 18 additions & 31 deletions test/scripts/release-pipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
type MainPosition,
mainPosition,
NEXT_BUILD_INPUTS,
NEXT_BUILD_UNPACKED,
type NextVerdict,
nextPublishVerdict,
npmConfirm,
Expand Down Expand Up @@ -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<string, string>): Packument => ({
versions: Object.fromEntries(versions.map((v) => [v, {}])),
Expand Down Expand Up @@ -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/<key>/ 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`,
],
});
});
Expand Down
Loading