diff --git a/AGENTS.md b/AGENTS.md index a2fa9e0..327e60d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,7 +29,7 @@ Use it instead of `npm link` (a symlinked React library breaks with duplicate-Re - Every user-visible change needs a changeset (`npx changeset`) in the same PR, or it ships with no release note. `patch` is for bug fixes only; new API surface is `minor`. - Releasing (maintainers only): `npm run publish` from the root, on a clean `master`. It builds and tests, applies the pending changesets, publishes to npm, then tags `vX.Y.Z`, pushes, and opens the GitHub release. An interrupted release is resumed by re-running it, never undone. See `bin/publish.sh`. -- One `vX.Y.Z` tag per release, as always — `changeset publish` runs with `--no-git-tag` so it doesn't tag each package separately. The tag carries a GitHub release whose body is assembled from both `CHANGELOG.md`s. +- One `vX.Y.Z` tag per release, as always — `changeset publish` runs with `--no-git-tag` so it doesn't tag each package separately. The tag carries a GitHub release whose body is assembled from the `CHANGELOG.md`s: a section for each package that actually has something to say, and a footer listing everything that shipped at that version. ## More detail diff --git a/bin/publish.sh b/bin/publish.sh index a2c6e57..1ce7143 100755 --- a/bin/publish.sh +++ b/bin/publish.sh @@ -28,10 +28,26 @@ fail() { printf '\n\033[31mAborted: %s\033[0m\n' "$1" >&2; exit 1; } BRANCH="$(git rev-parse --abbrev-ref HEAD)" -# Every workspace package, as "name version location" triples. +# Every publishable workspace package, as "name version location" triples. +# +# Read off the filesystem rather than asked of npm: `npm query` resolves +# workspace locations through node_modules, which during a release rehearsal is +# a symlink to another checkout, so it answers with paths outside the copy you +# are actually rehearsing. packages() { - npm query .workspace --no-workspaces-update 2>/dev/null \ - | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{for(const p of JSON.parse(s))console.log(p.name,p.version,p.location)})' + node -e ' + const fs = require("node:fs"), path = require("node:path"); + for (const pattern of require("./package.json").workspaces) { + const dir = path.dirname(pattern); + for (const entry of fs.readdirSync(dir).sort()) { + const location = path.join(dir, entry); + let pkg; + try { pkg = JSON.parse(fs.readFileSync(path.join(location, "package.json"), "utf8")); } catch { continue; } + if (pkg.private) continue; + console.log(pkg.name, pkg.version, location); + } + } + ' } version() { node -p "require('./packages/sdk/package.json').version"; } pending_changesets() { find .changeset -maxdepth 1 -name '*.md' ! -name 'README.md' | wc -l | tr -d ' '; } @@ -53,15 +69,31 @@ changelog_section() { # $1 = package location, $2 = version awk -v want="## $2" '$0 == want { found = 1; next } found && /^## / { exit } found' "$1/CHANGELOG.md" } -# The body of the GitHub release: every package's entry for this version, under -# its own heading. The packages move in lockstep, so one release covers them all. +# True when a changelog section says something a human wrote, as opposed to the +# dependency bookkeeping every package in a fixed group accumulates: +# +# - Updated dependencies [5b90e51] +# - datocms-plugin-sdk@2.2.7 +# +# A bullet counts as prose unless it is exactly one of those lines. +has_prose() { + grep -vE '^- Updated dependencies( \[[0-9a-f]+\])?$|^ *- [^ ]+@[0-9][^ ]*$' <<<"$1" | grep -qE '^- ' +} + +# The body of the GitHub release. The packages move in lockstep, so one release +# covers them all — but only the ones with something to say get a section, or +# the page fills up with each package restating that the others moved too. The +# footer keeps every published package visible. release_notes() { - local name ver loc section + local name ver loc section shipped="" while read -r name ver loc; do + shipped="$shipped +- $name@$ver" section="$(changelog_section "$loc" "$VERSION")" - [ -n "$section" ] || continue + has_prose "$section" || continue printf '## %s\n%s\n\n' "$name" "$section" done < <(packages) + printf -- '---\n\nReleased in lockstep:%s\n' "$shipped" } # ---------------------------------------------------------------------------