Skip to content

ci: automate the release and keep the generated binding in sync - #220

Draft
marshallku wants to merge 8 commits into
mainfrom
chore/automatically-update-indexjs
Draft

ci: automate the release and keep the generated binding in sync#220
marshallku wants to merge 8 commits into
mainfrom
chore/automatically-update-indexjs

Conversation

@marshallku

Copy link
Copy Markdown
Collaborator

Problem

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.

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:

edit CHANGELOG → edit package.json → commit vX.Y.Z → git tag → git push origin vX.Y.Z

After:

Actions → Prepare release → patch | minor | major → merge the pull request it opens

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 main whose version has no tag yet is tagged, gets a
GitHub release built from its changelog section, and is published to npm. Every other push to main
keeps publishing to the next dist-tag exactly as before.

Guards

  • CI fails when the committed index.js / index.d.ts differ from a fresh build.
  • A publish is refused unless the version is strictly newer than the one npm serves as latest. npm
    moves 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 latest backwards and npm would not stop it.
  • What ships is the binding built from the commit being released, not whatever sits in the tree.
  • Preparing refuses a prerelease version, a tag or release branch that already exists, and a range
    with nothing to release.
  • Runs on main serialise, 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 the
repository, 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_TOKEN repository secret — a fine-grained token scoped to this repository
with Pull requests: Write — so the workflow can open the release pull request. CI does not run on
a pull request opened with GITHUB_TOKEN, which is the same constraint changesets and
release-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 gh calls and the merge trigger all
execute 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.

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.
@marshallku marshallku self-assigned this Aug 19, 2026
@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
es-git Ready Ready Preview Aug 19, 2026 7:47am

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant