diff --git a/.changeset/README.md b/.changeset/README.md index c5b8835..2ec6bc9 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -34,5 +34,8 @@ untouched. It works in two modes: recorded in `.changeset/pre.json`, which you commit. Run `npx changeset pre exit` when the line is done. +Either way the GitHub release is marked as a prerelease, so it never becomes +the repository's "Latest release". + `npm run publish` refuses to run while `.changeset/pre.json` exists, so a forgotten pre mode can't quietly turn a real release into a prerelease. diff --git a/AGENTS.md b/AGENTS.md index 590b473..a2fa9e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,8 +28,8 @@ Use it instead of `npm link` (a symlinked React library breaks with duplicate-Re ## Gotchas - 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 and pushes. An interrupted release is resumed by re-running it, never undone. See `bin/publish.sh`. -- Git tags are per-package now (`datocms-plugin-sdk@2.2.7`), not the single `vX.Y.Z` Lerna used to create. +- 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. ## More detail diff --git a/README.md b/README.md index 386a4c4..8e5c893 100644 --- a/README.md +++ b/README.md @@ -42,14 +42,16 @@ released together. 2. **Release.** From an up-to-date, clean `master`, run `npm run publish`. It builds and tests first, then applies the pending changesets (bumping the versions and writing the `CHANGELOG.md`s), publishes to npm, and only then - tags and pushes to GitHub. + tags `vX.Y.Z`, pushes, and publishes the GitHub release — its notes are the + changelog entries changesets just wrote. If a release is interrupted, **do not undo anything**: run `npm run publish` again. It detects that some package is still missing from the registry and resumes the publish instead of starting a new release. `npm run publish-next` does the same under the `next` dist-tag, leaving -`latest` untouched. +`latest` untouched; its GitHub release is marked as a prerelease, so it doesn't +become the repository's "Latest release" either. ## License diff --git a/bin/publish.sh b/bin/publish.sh index ff37a78..a2c6e57 100755 --- a/bin/publish.sh +++ b/bin/publish.sh @@ -28,10 +28,10 @@ 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" pairs. +# Every workspace package, as "name version location" triples. 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)})' + | 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)})' } version() { node -p "require('./packages/sdk/package.json').version"; } pending_changesets() { find .changeset -maxdepth 1 -name '*.md' ! -name 'README.md' | wc -l | tr -d ' '; } @@ -40,13 +40,30 @@ pending_changesets() { find .changeset -maxdepth 1 -name '*.md' ! -name 'README. # the registry. Checking a single package would be wrong — a release can die # after publishing the first one. unpublished() { - local name ver missing="" - while read -r name ver; do + local name ver loc missing="" + while read -r name ver loc; do npm view "$name@$ver" version >/dev/null 2>&1 || missing="$missing $name@$ver" done < <(packages) echo "${missing# }" } +# The section of a package's CHANGELOG for one version, without its "## x.y.z" +# heading — changesets has already written exactly the prose we want. +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. +release_notes() { + local name ver loc section + while read -r name ver loc; do + section="$(changelog_section "$loc" "$VERSION")" + [ -n "$section" ] || continue + printf '## %s\n%s\n\n' "$name" "$section" + done < <(packages) +} + # --------------------------------------------------------------------------- # Preflight: no mutations, just refuse to start from a state we can't finish. # --------------------------------------------------------------------------- @@ -68,6 +85,9 @@ git fetch --quiet origin "$BRANCH" npm whoami >/dev/null 2>&1 || fail "you are not logged in to npm. Run 'npm login'." +command -v gh >/dev/null 2>&1 || fail "the GitHub CLI is not installed, so the release notes can't be published." +gh auth status >/dev/null 2>&1 || fail "you are not logged in to GitHub. Run 'gh auth login'." + echo "on $BRANCH, in sync with origin, npm user: $(npm whoami)" # --------------------------------------------------------------------------- @@ -121,20 +141,54 @@ fi VERSION="$(version)" # --------------------------------------------------------------------------- -# The irreversible step. npm first; changeset creates the git tags only for the -# packages it actually managed to publish. +# The irreversible step, npm first. +# +# --no-git-tag: changesets would tag every package separately +# (datocms-plugin-sdk@2.2.7, datocms-react-ui@2.2.7). The two move in lockstep, +# so we tag the release once, below, the way this repo always has. Tagging after +# the publish keeps the property that matters: a tag can only exist for a +# version that is actually on the registry. # --------------------------------------------------------------------------- step "Publishing v$VERSION to npm" if [ -n "$DIST_TAG" ]; then - npx changeset publish --tag "$DIST_TAG" + npx changeset publish --no-git-tag --tag "$DIST_TAG" else - npx changeset publish + npx changeset publish --no-git-tag fi # --------------------------------------------------------------------------- # git follows npm. # --------------------------------------------------------------------------- +step "Tagging v$VERSION" +if git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null; then + echo "v$VERSION already tagged" +else + git tag -a "v$VERSION" -m "v$VERSION" +fi + step "Pushing to GitHub" git push --follow-tags origin "$BRANCH" +# --------------------------------------------------------------------------- +# The release notes. Last, because it's the only step a human can redo by hand +# from the changelog if it goes wrong. +# --------------------------------------------------------------------------- +step "Publishing the release notes" +if gh release view "v$VERSION" >/dev/null 2>&1; then + echo "the v$VERSION release already exists, leaving it alone" +else + # A prerelease must not become the repo's "Latest release": that's reserved + # for whatever is on the `latest` dist-tag. + PRERELEASE="" + case "$VERSION" in *-*) PRERELEASE="--prerelease" ;; esac + [ -z "$DIST_TAG" ] || PRERELEASE="--prerelease" + + release_notes | gh release create "v$VERSION" --title "v$VERSION" --notes-file - $PRERELEASE +fi + +# Asked for rather than parsed out of `gh release create`, so the link is the +# same whether we just created the release or found one already there. +RELEASE_URL="$(gh release view "v$VERSION" --json url --jq .url 2>/dev/null || true)" + printf '\n\033[32mReleased v%s\033[0m\n' "$VERSION" +[ -z "$RELEASE_URL" ] || printf '%s\n' "$RELEASE_URL" diff --git a/packages/react-ui/README.md b/packages/react-ui/README.md index 87cdff3..f4cde72 100644 --- a/packages/react-ui/README.md +++ b/packages/react-ui/README.md @@ -44,6 +44,6 @@ rm -rf node_modules/datocms-react-ui node_modules/.vite && npm install Every user-visible change needs a changeset: run `npx changeset` from the repo root in the same PR, pick the bump level (`patch` is for bug fixes only, new API surface is `minor`) and commit the file it writes under `.changeset/`. -To release, from an up-to-date, clean `master`, run `npm run publish` from the repo root. It builds and tests, applies the pending changesets — bumping **both** packages to the same version (fixed group) and writing the `CHANGELOG.md`s — publishes to npm, and only then tags (`datocms-react-ui@X.Y.Z`) and pushes. An interrupted release is resumed by re-running it, never undone. Use `npm run publish-next` for a prerelease under the `next` dist-tag. +To release, from an up-to-date, clean `master`, run `npm run publish` from the repo root. It builds and tests, applies the pending changesets — bumping **both** packages to the same version (fixed group) and writing the `CHANGELOG.md`s — publishes to npm, and only then tags the release `vX.Y.Z`, pushes, and publishes the GitHub release, whose notes come straight from those changelog entries. An interrupted release is resumed by re-running it, never undone. Use `npm run publish-next` for a prerelease under the `next` dist-tag. For deeper architectural notes (CSS Modules pipeline, dual CJS/ESM output, theming via `ctx`), see [`AGENTS.md`](https://github.com/datocms/plugins-sdk/blob/master/packages/react-ui/AGENTS.md) in this directory. diff --git a/packages/sdk/README.md b/packages/sdk/README.md index aae8c16..8ef3dee 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -44,4 +44,4 @@ rm -rf node_modules/datocms-plugin-sdk node_modules/.vite && npm install Every user-visible change needs a changeset: run `npx changeset` from the repo root in the same PR, pick the bump level (`patch` is for bug fixes only, new API surface is `minor`) and commit the file it writes under `.changeset/`. -To release, from an up-to-date, clean `master`, run `npm run publish` from the repo root. It builds and tests, applies the pending changesets — bumping **both** packages to the same version (fixed group) and writing the `CHANGELOG.md`s — publishes to npm, and only then tags (`datocms-plugin-sdk@X.Y.Z`) and pushes. An interrupted release is resumed by re-running it, never undone. Use `npm run publish-next` for a prerelease under the `next` dist-tag. +To release, from an up-to-date, clean `master`, run `npm run publish` from the repo root. It builds and tests, applies the pending changesets — bumping **both** packages to the same version (fixed group) and writing the `CHANGELOG.md`s — publishes to npm, and only then tags the release `vX.Y.Z`, pushes, and publishes the GitHub release, whose notes come straight from those changelog entries. An interrupted release is resumed by re-running it, never undone. Use `npm run publish-next` for a prerelease under the `next` dist-tag.