diff --git a/.changeset/README.md b/.changeset/README.md index e7c4c48..1326948 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -12,25 +12,36 @@ npm run release # build, then publish `release` builds first because the `.d.ts` files are generated output — they have to exist before packing, or the published packages ship without types. -## One version for everything - -All packages are a **fixed** group: a single version across the repo, always -released together. A consumer cannot end up with `@pdsjs/node` built against a -different `@pdsjs/core` than it expects, which is the failure mode a repo full -of independently-versioned packages invites. Internal `workspace:*` ranges -resolve to that exact version at pack time. - -`@pdsjs/spaces` is in the group too, even though it implements -[proposal 0016](https://github.com/bluesky-social/proposals/tree/main/0016-permissioned-data) -and takes breaking changes whenever that moves. Versioning it separately does -not decouple it: `@pdsjs/cloudflare` imports it statically — the Durable Object -constructor is synchronous and cannot defer the import the way `@pdsjs/node` -does — so a spaces bump propagates through cloudflare to the whole group either -way. Splitting the version scheme would buy nothing and cost the single-version -guarantee. - -That means an upstream change to 0016 lands as a major across every package, -including for people who never enabled spaces. The lever on that is the -cloudflare import, not the release config. +## Two release groups + +The ten runtime and adapter packages are a **fixed** group: one version across +all of them, always released together. A consumer cannot end up with +`@pdsjs/node` built against a different `@pdsjs/core` than it expects, which is +the failure mode a repo full of independently-versioned packages invites. +Internal `workspace:*` ranges resolve to that exact version at pack time. + +`@pdsjs/spaces` versions on its own, and must stay out of the fixed group. +Inside it, **every** release escalated to a major regardless of what the +changesets asked for — a minor and a patch shipped as 2.0.0 with no +`### Major Changes` in any changelog. Measured from a 2.0.0 baseline with a +single `core: minor` changeset: + +| fixed group | result | +|---|---| +| all eleven packages | 3.0.0 | +| spaces excluded | 2.1.0 | +| no fixed group | 2.1.0 | + +`@pdsjs/cloudflare` depends on spaces and cannot defer the import — the Durable +Object constructor is synchronous — so a spaces bump still propagates to the +whole group. That is one major per upstream change to +[proposal 0016](https://github.com/bluesky-social/proposals/tree/main/0016-permissioned-data), +which is the price of the coupling; it is not a reason to put spaces back in the +group and pay a major on every release instead. The lever on the remaining +propagation is that cloudflare import, not the release config. + +`npm run version-packages` refuses to continue if the version moves further +than any changeset asked for, because this failure was otherwise invisible +until after publishing. Private packages (the examples) are neither versioned nor published. diff --git a/.changeset/config.json b/.changeset/config.json index be4d6f1..a3c49cb 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -4,7 +4,16 @@ "commit": false, "fixed": [ [ - "@pdsjs/*" + "@pdsjs/core", + "@pdsjs/node", + "@pdsjs/deno", + "@pdsjs/cloudflare", + "@pdsjs/readonly", + "@pdsjs/storage-sqlite", + "@pdsjs/lexicon-resolver", + "@pdsjs/blobs-fs", + "@pdsjs/blobs-s3", + "@pdsjs/blobs-deno" ] ], "linked": [], diff --git a/package.json b/package.json index 2434d03..4ffaf5c 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "test:reference": "./scripts/space-reference/run.sh", "dev:runclub": "npm run dev --workspace=pds-runclub-example", "changeset": "changeset", - "version-packages": "changeset version && node scripts/sync-version.js && pnpm install --lockfile-only", + "version-packages": "node scripts/version-packages.js", "release": "npm run build && changeset publish" }, "dependencies": { diff --git a/scripts/version-packages.js b/scripts/version-packages.js new file mode 100644 index 0000000..403d04c --- /dev/null +++ b/scripts/version-packages.js @@ -0,0 +1,78 @@ +// Applies pending changesets, then checks that the version actually moved the +// way the changesets asked. +// +// This exists because it did not. A release carrying one minor and one patch +// changeset — no `major` anywhere, and no "### Major Changes" in any changelog — +// published as 2.0.0. The cause was `@pdsjs/spaces` sitting in the fixed group: +// with it there, every bump escalated to major, and the only visible sign was +// the version number in the success output, after publishing. +// +// Config alone would not have caught that, because the failure was silent. This +// makes it loud and puts it before `npm run release` rather than after. +import { execFileSync } from 'node:child_process'; +import { readdirSync, readFileSync } from 'node:fs'; + +const RANK = { patch: 1, minor: 2, major: 3 }; +const CORE = new URL('../packages/core/package.json', import.meta.url); + +/** @param {URL} url */ +const versionOf = (url) => JSON.parse(readFileSync(url, 'utf8')).version; + +/** + * Highest bump any pending changeset asks for. + * @returns {'patch'|'minor'|'major'|null} Null when nothing is pending. + */ +function requestedBump() { + const dir = new URL('../.changeset/', import.meta.url); + let highest = null; + for (const file of readdirSync(dir)) { + if (!file.endsWith('.md') || file === 'README.md') continue; + const text = readFileSync(new URL(file, dir), 'utf8'); + const frontmatter = text.split('---')[1] ?? ''; + for (const match of frontmatter.matchAll(/:\s*(patch|minor|major)\s*$/gm)) { + const bump = /** @type {'patch'|'minor'|'major'} */ (match[1]); + if (!highest || RANK[bump] > RANK[highest]) highest = bump; + } + } + return highest; +} + +/** + * How a version actually moved. + * @param {string} before + * @param {string} after + */ +function actualBump(before, after) { + const [bMaj, bMin] = before.split('.').map(Number); + const [aMaj, aMin] = after.split('.').map(Number); + if (aMaj > bMaj) return 'major'; + if (aMin > bMin) return 'minor'; + return 'patch'; +} + +const wanted = requestedBump(); +if (!wanted) { + console.error('No pending changesets — nothing to version.'); + process.exit(1); +} + +const before = versionOf(CORE); +execFileSync('pnpm', ['exec', 'changeset', 'version'], { stdio: 'inherit' }); +const after = versionOf(CORE); +const got = actualBump(before, after); + +if (RANK[got] > RANK[wanted]) { + console.error( + `\nRefusing to continue: @pdsjs/core went ${before} -> ${after}, a ${got} ` + + `bump, but the highest any changeset asked for was ${wanted}.\n\n` + + 'Something in .changeset/config.json is escalating the bump — a package ' + + 'in the fixed group that drags the rest is the usual cause. The working ' + + 'tree now holds the wrong versions: run `git checkout -- . && git clean ' + + '-fd packages` before trying again.\n', + ); + process.exit(1); +} + +console.log(`\n@pdsjs/core ${before} -> ${after} (${got}, as requested)`); +execFileSync('node', ['scripts/sync-version.js'], { stdio: 'inherit' }); +execFileSync('pnpm', ['install', '--lockfile-only'], { stdio: 'inherit' });