ci: automate the release and keep the generated binding in sync - #220
Draft
marshallku wants to merge 8 commits into
Draft
ci: automate the release and keep the generated binding in sync#220marshallku wants to merge 8 commits into
marshallku wants to merge 8 commits into
Conversation
index.js and index.d.ts are generated but committed, and the release job publishes what is committed rather than what it builds. Nothing checked that the two agreed, and they had not agreed since the 0.7.0 bump: the committed binding still expected 0.6.0. Build on one leg of the CI matrix and fail when a fresh build disagrees with what is in the tree, so the drift is caught on the pull request that causes it. Regenerating index.js along the way is the 0.6.0 to 0.7.0 correction.
The release job never builds -- it downloads .node artifacts and publishes -- so index.js and index.d.ts ship exactly as committed. Have the darwin-arm64 build leg upload the binding it just generated and have the release job download it before publishing, so the tarball always carries a binding built from the commit being released. The JS binding is target-independent, so one leg is enough. npm assigns the latest dist-tag to whatever is published last with no regard for semver order, so tagging v0.6.9 after 0.7.0 is out would quietly move latest backwards and npm would not stop it. Gate the tagged release on the tag matching package.json and on that version being strictly newer than the one npm currently serves as latest.
Cutting a release meant hand-writing the CHANGELOG section, hand-editing the version, committing it as vX.Y.Z and pushing a tag. Everything there is mechanical except deciding how big the release is, so ask only for that: a choice of patch, minor or major, never a version to type, which is what makes releasing 1.0.1 while 1.1.0 is out impossible rather than merely discouraged. It runs as a workflow because a fresh checkout of main answers by itself the questions a local script would have to ask, namely whether the tree is clean and whether main is level with origin. npm version raises the version, git-cliff writes the changelog into the shape release.yml expects to find with submark, and yarn build regenerates the binding the bump would otherwise leave behind. The workflow pushes the branch and stops. Opening the pull request is left to a person, because a pull request opened with GITHUB_TOKEN gets no CI run and CI on the release branch is what checks the binding it carries. Tagging stays manual, so release.yml is triggered by a human push exactly as before. Refused: a bump from a prerelease version, which npm version resolves rather than raises; a tag or release branch that already exists; nothing to release since the last tag. The two races the minutes-long build opens are closed at the end, when main is rechecked and the branch is created with a lease rather than pushed.
The tag was the last thing done by hand, and the thing most able to go wrong: cut from the wrong commit, or naming a version package.json does not carry. Nothing needed it to stay manual except an earlier decision to leave this file alone. Merging the release pull request is now the release. A push to main whose package.json version has no tag yet is a release; every other push carries an already-tagged version and goes to the next channel as before. Creating the GitHub release is what creates the tag, pinned to the commit this run built rather than to wherever the default branch has got to by the time the API call lands. The token problem that ruled out release-please does not arise here. Nothing bot-created has to trigger anything: a person opens the pull request and a person merges it, and that merge is an ordinary push. Runs on main serialise, and every waiting one is kept. Without that, two pushes in flight would both find the tag missing and both publish, and the default queue would let a later push cancel a pending release. The tag lookup reads git ls-remote by exit code rather than by truthiness, because 'not found' and 'the lookup failed' are different answers and only one of them may publish to latest. verify-release-version drops the tag comparison. The tag is derived from package.json now, so it cannot disagree with it; what remains is the check that matters, that the version outranks what npm serves as latest.
The one thing left to edit by hand after preparing a release was the sentence thanking outside contributors. GitHub already knows who they are: the role badge it shows on a pull request is author_association, so the release can ask for it. Which people it names had to be read off the existing changelog rather than guessed. @racgoo appears in two releases running and @other-yuka in three, so it is not a list of first-timers -- it thanks people outside the org, every time. That rules out git-cliff's GitHub integration, which knows usernames and first contributions but nothing about membership. The filter names the insiders and keeps everyone else, rather than naming the one outsider value. Someone's first pull request is a FIRST_TIME_CONTRIBUTOR, not a CONTRIBUTOR, and picking out the latter would have missed exactly the people this line is for. Bots are dropped explicitly, since dependabot counts as a contributor, and so are authors GitHub cannot resolve, which would otherwise read as @null. A lookup that fails ends the step. Thanking a shorter list than the release earned is worse than not writing the line at all.
Neither was written anywhere. The release procedure lived only in the workflow files, and CONTRIBUTING mentioned just build without saying that index.js and index.d.ts are committed -- so a contributor who changes the Rust API meets a CI failure with nothing to read about it.
Opening it by hand was the last manual step, and it existed only because CI does not run on a pull request GITHUB_TOKEN opened. A maintainer's token gets around that, which is the same workaround changesets and release-please document for the same constraint. The token is an optimisation over a flow that already works, not a dependency of it. It expires in a year at most and this repository releases about twice a year, so it will sometimes be dead when reached for. Pushing the branch still uses GITHUB_TOKEN, which cannot expire, and only opening the pull request needs the personal one; the step tolerates its own failure, and the summary falls back to the compare link. An expired token costs a click, not a release. A missing secret exits clean rather than failing. Not being configured is not a fault, and a step that is red every run teaches you to ignore it. A token that is set and does not work still goes red.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
index.jsandindex.d.tsare generated but committed, and the release job publishes what iscommitted rather than what it builds. Nothing checked that the two agreed, and they had not agreed
since the 0.7.0 bump — the committed binding still expected
0.6.0.Releasing was manual throughout: a hand-written changelog section, a hand-edited version, a commit
named
vX.Y.Z, and a pushed tag.What changes
Before:
After:
The workflow raises the version, writes the changelog section from the commits since the last
release, thanks the outside contributors among them, rebuilds the binding, and opens a pull request.
Merging it is the release. A commit landing on
mainwhose version has no tag yet is tagged, gets aGitHub release built from its changelog section, and is published to npm. Every other push to
mainkeeps publishing to the
nextdist-tag exactly as before.Guards
index.js/index.d.tsdiffer from a fresh build.latest. npmmoves that dist-tag to whatever is published last with no regard for semver order, so releasing
0.6.9 after 0.7.0 would quietly move
latestbackwards and npm would not stop it.with nothing to release.
mainserialise, and the tag is pinned to the commit that was built.Also
Adds me to
CODEOWNERS. This whole change is groundwork for taking over maintenance of therepository, and the release pipeline was the part that most needed to stop depending on someone
remembering the steps.
Before merging
This wants a
RELEASE_PR_TOKENrepository secret — a fine-grained token scoped to this repositorywith
Pull requests: Write— so the workflow can open the release pull request. CI does not run ona pull request opened with
GITHUB_TOKEN, which is the same constraint changesets andrelease-please document.
It is not blocking. Without the secret the branch is still pushed and the pull request is opened by
hand, which is what the step summary falls back to telling you.
Not verified
The publish path runs twice a year, so the tag creation, the
ghcalls and the merge trigger allexecute for the first time on the next release. Everything that could be exercised locally was:
regenerating the v0.6.0..v0.7.0 range reproduces the 13 bullets written by hand for v0.7.0 in the
same order, the version gate was run against the real npm registry in both directions, and each
guard was driven in an isolated clone with a bare origin.