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
37 changes: 36 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,33 @@ jobs:
fi
echo "${VERSION} is unreleased on both npm and this repository"

# The CHANGELOG entry is what the GitHub release will say. Generated notes
# are a list of merged pull requests, which puts "stop setup-node's .npmrc
# breaking every yarn step" next to the reason the library got faster, and
# leaves the release and the CHANGELOG telling different stories.
#
# It doubles as a guard: the script exits non-zero when the section is
# missing or empty, and a version nobody wrote an entry for is not ready
# to publish.
- name: Build the release notes from the CHANGELOG
env:
VERSION: ${{ steps.version.outputs.version }}
GH_TOKEN: ${{ github.token }}
run: |
node scripts/changelog-section.mjs "$VERSION" > "${RUNNER_TEMP}/notes.md"
# the release being made does not exist yet, so the latest one is the
# previous one
previous="$(gh release view --json tagName --jq .tagName 2>/dev/null || true)"
if [ -n "$previous" ]; then
{
echo
echo '---'
echo
echo "**Full changelog**: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/compare/${previous}...v${VERSION}"
} >> "${RUNNER_TEMP}/notes.md"
fi
echo "release notes: $(wc -l < "${RUNNER_TEMP}/notes.md") lines from the CHANGELOG"

# A missing token otherwise surfaces as an npm 403 after the whole gate
# has run, which reads like a problem with the account rather than a
# secret that is not reaching the job.
Expand Down Expand Up @@ -155,7 +182,7 @@ jobs:
gh release create "$TAG" \
--target "$GITHUB_SHA" \
--title "$VERSION" \
--generate-notes
--notes-file "${RUNNER_TEMP}/notes.md"

# The run page otherwise looks identical whether or not anything was
# published, which is genuinely confusing when the default is a rehearsal.
Expand All @@ -178,6 +205,14 @@ jobs:
echo "Verified \`${NAME}@${VERSION}\`. No npm release, no tag, no GitHub release."
echo
echo "To publish for real, run this workflow again with **Rehearse only** unchecked."
if [ -f "${RUNNER_TEMP}/notes.md" ]; then
echo
echo "<details><summary>Release notes this would publish</summary>"
echo
cat "${RUNNER_TEMP}/notes.md"
echo
echo "</details>"
fi
elif [ "${{ job.status }}" != "success" ]; then
echo "## Failed while releasing ${VERSION}"
echo
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,15 @@ yarn check-package # entrypoints and types agree, across all resolution modes
Releasing is one button: **Actions → Publish → Run workflow**.

It reads the version from `package.json`, refuses to go on if that version is
already tagged or already on npm, runs the whole gate, publishes to npm with
already tagged or already on npm, takes the release notes from that version's
CHANGELOG entry, runs the whole gate, publishes to npm with
[provenance](https://docs.npmjs.com/generating-provenance-statements), and
creates the GitHub release.

The CHANGELOG entry is required. A version with no `## <version>` section stops
the run before anything is published, so the release and the CHANGELOG cannot
drift apart — and a rehearsal shows the notes it would publish.

**Rehearse only** is ticked by default, so the default action of that button
publishes nothing — it runs every check and stops. Untick it to release for
real. Either way the run's summary says plainly what did or did not happen.
Expand All @@ -270,7 +275,7 @@ Bumping the version stays a pull request, because the CHANGELOG has to be
written by a person anyway. Everything after that point is what this automates:

```
# 1. a PR bumping the version in package.json and moving the CHANGELOG heading
# 1. a PR bumping package.json and adding the CHANGELOG entry for it
# 2. merge it
# 3. Actions -> Publish -> Run workflow, with Rehearse only unticked
```
Expand Down
54 changes: 54 additions & 0 deletions scripts/changelog-section.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Print the CHANGELOG entry for one version.
*
* GitHub's generated release notes are a list of merged pull requests, which
* for this repository means a consumer reading the release sees "stop
* setup-node's .npmrc breaking every yarn step" next to the reason the library
* got faster. The CHANGELOG already says what changed and why; this hands that
* text to `gh release create --notes-file` so the release and the file agree.
*
* Exits non-zero when the section is missing or empty, which is what makes it
* useful as a release guard: a version nobody wrote a CHANGELOG entry for is
* not ready to publish.
*/
import { readFile } from 'node:fs/promises';

const version = process.argv[2];

if (!version) {
console.error('usage: changelog-section.mjs <version>');
process.exit(2);
}

const changelog = await readFile(
new URL('../CHANGELOG.md', import.meta.url),
'utf8',
);
const lines = changelog.split('\n');
const heading = `## ${version}`;
const start = lines.findIndex((line) => line.trim() === heading);

if (start === -1) {
console.error(`changelog-section: no "${heading}" in CHANGELOG.md`);
process.exit(1);
}

let end = lines.length;

for (let i = start + 1; i < lines.length; i++) {
if (lines[i].startsWith('## ')) {
end = i;
break;
}
}

// The heading itself is dropped: the release already carries the version as
// its title, and repeating it reads like a mistake.
const body = lines.slice(start + 1, end).join('\n').trim();

if (!body) {
console.error(`changelog-section: the "${heading}" section is empty`);
process.exit(1);
}

console.log(body);
Loading