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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
46 changes: 39 additions & 7 deletions bin/publish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ' '; }
Expand All @@ -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"
}

# ---------------------------------------------------------------------------
Expand Down