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
3 changes: 3 additions & 0 deletions .changeset/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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"
2 changes: 1 addition & 1 deletion packages/react-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.