Open the version bump as a pull request #6

Merged
thatguygriff merged 2 commits from bump-version-by-pull-request into main 2026-08-27 23:57:24 +00:00
2 changed files with 72 additions and 30 deletions
Showing only changes of commit b94c43a10c - Show all commits
+56 -20
View File
@@ -157,19 +157,26 @@ jobs:
# it is a candidate for, so there is nothing yet to move past.
if: github.ref_type == 'tag' && !contains(github.ref_name, '-')
runs-on: ubuntu-latest
# npm does the bump, so this one job wants node. A job in a container is
# given `sh -e {0}` as its shell rather than the bash the runner's own jobs
# get, and dash has neither `pipefail` nor the `10#` below — which failed
# the first line of the first step the one time this ran. node:22 is Debian
# and carries bash, so asking for it keeps these scripts the same as the
# ones in the job above.
container:
image: node:22
defaults:
run:
shell: bash
steps:
# The tag names a commit in main's history, but the bump belongs on the
# branch, so this checks out main rather than the tag.
# branch, so this checks out main rather than the tag. The full history
# because a shallow clone cannot reliably push a branch back.
- uses: actions/checkout@v4
with:
ref: main
# The task token can push only if the instance allows Actions to
# write to the repository. Where it does not, set VERSION_BUMP_TOKEN
# to a personal access token with write access and it is used
# instead.
token: ${{ secrets.VERSION_BUMP_TOKEN || secrets.GITEA_TOKEN }}
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Work out the next patch version
id: next
@@ -211,30 +218,59 @@ jobs:
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "package.json ${current} -> ${NEXT}"
- name: Commit it to main
# The bump arrives as a pull request rather than as a commit straight to
# main. Pushing a branch asks nothing of the task token beyond ordinary
# write access, so nothing here depends on being allowed past whatever
# protects main; and the pull request puts the changed package.json
# through the build before it lands. Since main is never pushed, the
# `[skip ci]` that would otherwise be needed to stop this rebuilding the
# image just published is not.
- name: Open a pull request for it
if: steps.bump.outputs.changed == 'true'
env:
NEXT: ${{ steps.next.outputs.next }}
RELEASED: ${{ github.ref_name }}
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
branch="release/bump-${NEXT}"
# A name that is not a person, and a reserved address that can never
# resolve to one. Nothing here names the instance it runs on.
git config user.name 'Release bot'
git config user.email '[email protected]'
git checkout -b "${branch}"
git add package.json package-lock.json
# `[skip ci]` because this commit is a number and nothing else:
# without it the push to main starts another build of the very image
# that was just published.
git commit -m "Set the working version to ${NEXT} [skip ci]"
git commit -m "Set the working version to ${NEXT}"
git push origin "${branch}"
if ! git push origin HEAD:main; then
echo >&2
echo "Could not push the version bump to main. Either the Actions" >&2
echo "token has no write access to this repository, or main is" >&2
echo "protected against direct pushes. Set VERSION_BUMP_TOKEN to a" >&2
echo "token that may push to main, or allow that token past the" >&2
echo "branch protection." >&2
exit 1
fi
# node rather than jq to build the request body: jq is not in this
# image, and node is the one thing that certainly is.
payload="$(BRANCH="${branch}" node -e 'process.stdout.write(JSON.stringify({
head: process.env.BRANCH,
base: "main",
title: `Set the working version to ${process.env.NEXT}`,
body: `${process.env.RELEASED} has shipped, so the tree was left on a version that is published and immutable. This moves it on to ${process.env.NEXT}, which is deliberately not a version that exists.`,
}))')"
api="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
response="$(mktemp)"
code="$(curl -sS -o "${response}" -w '%{http_code}' \
-X POST "${api}/pulls" \
-H "Authorization: token ${TOKEN}" \
-H 'Content-Type: application/json' \
-d "${payload}")"
case "${code}" in
201) echo "Opened ${branch} against main." ;;
# A release re-run that got this far: the branch and its pull
# request are already there, which is the state we wanted anyway.
409) echo "A pull request for ${branch} is already open." ;;
*)
echo "Could not open the pull request (HTTP ${code}):" >&2
cat "${response}" >&2
exit 1
;;
esac
+16 -10
View File
@@ -210,16 +210,22 @@ The registry comes from the `REGISTRY` repository variable, the image name from
`IMAGE_NAME` or the repository name, and credentials from `REGISTRY_USER` and the
`REGISTRY_TOKEN` secret. Nothing about any particular deployment is committed here.
A release also moves `package.json` on to the next patch version, committed to main by
the `bump` job — so the number in the tree is never one that has already shipped and
been made immutable. It lives in `publish.yml` rather than a workflow of its own so it
can say `needs: build`: a version that failed to publish has not been released, and
bumping past it would claim otherwise. Prereleases are skipped, being candidates for a
version that has not shipped. The bump goes through `npm version` rather than an edit in
place, because the version is in the lockfile too, in more than one place, and the two
have to agree. The commit carries `[skip ci]`, or pushing it would rebuild the image
that was just published. Pushing to main needs a token with write access —
`VERSION_BUMP_TOKEN` overrides the task token where that one cannot.
A release also moves `package.json` on to the next patch version, opened as a pull
request by the `bump` job — so the number in the tree is never one that has already
shipped and been made immutable. It lives in `publish.yml` rather than a workflow of its
own so it can say `needs: build`: a version that failed to publish has not been
released, and bumping past it would claim otherwise. Prereleases are skipped, being
candidates for a version that has not shipped. The bump goes through `npm version`
rather than an edit in place, because the version is in the lockfile too, in more than
one place, and the two have to agree.
It arrives as a pull request rather than as a commit straight to main: pushing a branch
asks nothing of the task token beyond ordinary write access, so it does not matter what
protects main, and the changed `package.json` goes through a build before it lands.
Nothing pushes to main, so no `[skip ci]` is needed to stop the bump rebuilding the
image just published. The job is the only one that runs in a container (`node:22`, for
npm), and a job in a container is handed `sh`, not bash — hence the explicit
`shell: bash`, without which `set -o pipefail` fails the first line of the first step.
Two things any deployment has to get right, both learned the hard way: