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
42 changes: 38 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ on:

permissions:
contents: write
pull-requests: write

jobs:
release:
Expand Down Expand Up @@ -50,11 +51,10 @@ jobs:

- run: yarn build

# release-it pushes the version commit and the tag back to master, and
# actions/checkout leaves the branch without an upstream to push to.
- run: git branch --set-upstream-to=origin/${{ github.ref_name }}

# Publishes to npm, pushes the annotated tag and creates the GitHub
# release. The version bump commit stays local, see .release-it.cjs.
- name: Release
id: release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Expand All @@ -74,3 +74,37 @@ jobs:
fi

yarn release $args

echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"

# master requires status checks and a fresh commit has none, so the bump
# has to arrive as a PR. Merge commit, not squash: the tag points at the
# bump commit, and squashing would rewrite it and leave the tag off
# master's history, which breaks the next release's changelog range.
- name: Land the version bump on master
if: ${{ !inputs.dry-run }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.release.outputs.version }}
BASE: ${{ github.ref_name }}
REPO: ${{ github.repository }}
run: |
branch="chore/release-v$VERSION"
git push origin "HEAD:refs/heads/$branch"

cat > /tmp/pr-body.md <<BODY
Version bump and changelog for [v$VERSION](https://github.com/$REPO/releases/tag/v$VERSION), opened by the release workflow. npm and the tag are already out; this lands the commit the tag points at.

Auto-merges as a merge commit once CI and CodeQL pass. A squash would rewrite the commit and leave the tag outside master's history, which breaks the changelog range for the next release.
BODY

url=$(gh pr create --base "$BASE" --head "$branch" \
--title "chore(release): v$VERSION" --body-file /tmp/pr-body.md)
number="${url##*/}"
echo "opened $url"

# The merge commit lands on master too, so it needs a conventional
# subject. `.release-it.cjs` filters `chore(release)` out of the
# changelog, so neither this nor the bump commit shows up next time.
gh pr merge "$number" --auto --merge --delete-branch \
--subject "chore(release): v$VERSION (#$number)" --body ""
63 changes: 63 additions & 0 deletions .release-it.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* master requires status checks, and a brand new commit has none, so the
* release workflow can't push the version bump straight to it. release-it
* commits and tags locally, pushes only the tag (tags aren't covered by a
* branch ruleset), and the workflow lands the bump commit through a PR.
*/
module.exports = {
git: {
requireBranch: "master",
commitMessage: "chore(release): v${version}",
tagName: "v${version}",
tagAnnotation: "v${version}\n\n${changelog}",
// git strips lines starting with `#` from a tag message as comments, which
// would drop every `###` heading including `⚠ BREAKING CHANGES`.
tagArgs: ["--cleanup=verbatim"],
// The workflow opens a PR for the bump commit instead.
push: false,
requireUpstream: false,
},
hooks: {
// Push the annotated tag before the github plugin creates the release,
// otherwise GitHub invents a lightweight tag at master's head and the
// release points at the wrong commit with none of the changelog.
//
// This slot rather than `after:git:release` because `Git.release()` returns
// the result of its push step, and release-it skips `after:` hooks on a
// falsy return. With `push: false` that hook can never fire. Which also
// means the tag only reaches the remote while `github.release` is on.
"before:github:release": "git push origin refs/tags/${tagName}",
},
npm: {
publish: true,
},
github: {
release: true,
releaseName: "v${version}",
},
plugins: {
"@release-it/conventional-changelog": {
infile: "CHANGELOG.md",
header: "# Changelog",
// Each release leaves a `chore(release)` bump commit and the merge commit
// that lands it. Neither is worth a changelog line.
commitFilter: (commit) => !/^chore\(release\)/.test(commit.header ?? ""),
preset: {
name: "conventionalcommits",
types: [
{ type: "feat", section: "Features" },
{ type: "fix", section: "Bug Fixes" },
{ type: "perf", section: "Performance" },
{ type: "refactor", section: "Code Refactoring" },
{ type: "docs", section: "Documentation" },
{ type: "build", section: "Build System" },
{ type: "ci", section: "Continuous Integration" },
{ type: "chore", section: "Chores" },
{ type: "revert", section: "Reverts" },
{ type: "test", hidden: true },
{ type: "style", hidden: true },
],
},
},
},
};
73 changes: 0 additions & 73 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,79 +45,6 @@
"prettier": "^3.9.6",
"release-it": "^21.0.0"
},
"release-it": {
"git": {
"requireBranch": "master",
"commitMessage": "chore(release): v${version} [skip ci]",
"tagName": "v${version}",
"tagAnnotation": "v${version}\n\n${changelog}",
"tagArgs": [
"--cleanup=verbatim"
]
},
"npm": {
"publish": true
},
"github": {
"release": true,
"releaseName": "v${version}"
},
"plugins": {
"@release-it/conventional-changelog": {
"infile": "CHANGELOG.md",
"header": "# Changelog",
"preset": {
"name": "conventionalcommits",
"types": [
{
"type": "feat",
"section": "Features"
},
{
"type": "fix",
"section": "Bug Fixes"
},
{
"type": "perf",
"section": "Performance"
},
{
"type": "refactor",
"section": "Code Refactoring"
},
{
"type": "docs",
"section": "Documentation"
},
{
"type": "build",
"section": "Build System"
},
{
"type": "ci",
"section": "Continuous Integration"
},
{
"type": "chore",
"section": "Chores"
},
{
"type": "revert",
"section": "Reverts"
},
{
"type": "test",
"hidden": true
},
{
"type": "style",
"hidden": true
}
]
}
}
}
},
"resolutions": {
"**/@expo/plist/@xmldom/xmldom": "0.8.13",
"**/@typescript-eslint/typescript-estree/minimatch": "9.0.7",
Expand Down