Skip to content

Add a tag-driven release process - #9

Merged
tuandm merged 4 commits into
tuandm:mainfrom
spartan-ducduong:ducdt/release-process
Sep 14, 2026
Merged

tuandm merged 4 commits into
tuandm:mainfrom
spartan-ducduong:ducdt/release-process

Conversation

@spartan-ducduong

Copy link
Copy Markdown
Contributor

Releases are cut by hand today: a version bump commit, npm publish from a laptop, no tag, no release page. Nothing links a published tarball to a commit that passed CI, and 0.1.0 / 0.1.1 have no tags at all.

This makes a v* tag push the whole release.

File What it does
.github/workflows/release.yml on: push: tags: ['v*'] - verify, publish, release page
RELEASING.md Two commands for the maintainer, plus the one-time npm setup
scripts/sync-plugin-version.mjs version lifecycle script: copies the version into .claude-plugin/plugin.json and stages it
test/version-sync.test.mjs Asserts the two manifests agree
.claude-plugin/plugin.json 0.1.0 -> 0.1.1 (existing drift, see below)
package.json Wires the version script

Cutting a release

npm version patch && git push origin main --follow-tags

The three jobs

  • verify - refuses the tag unless it is an ancestor of main (so the code went through PR CI) and unless package.json and the plugin manifest both match it. Then the full suite on real Chromium, reusing pr-ci.yml's Chromium cache and its fail-on-skip guard, and npm pack --dry-run so the tarball is visible before it is permanent.
  • publish - npm publish --provenance. Holds id-token: write and nothing else, and deliberately runs no npm ci: there is no prepack or prepublishOnly, so no dependency code executes in the job that holds the OIDC token. Skipped when the version is already on the registry, so a rerun after a flake is not a 403.
  • github-release - gh release create --generate-notes. Runs when publish succeeded or was skipped.

Ordering is deliberate: everything that can say "do not ship this" runs before any job holds a credential. A verify failure costs a deleted tag; a failure after publish costs a version number npm will never let you reuse.

The manifest drift

.claude-plugin/plugin.json has its own version and npm version does not touch it, so it reads 0.1.0 against a published 0.1.1. The lifecycle script puts it in the same commit as the bump; the test means a future drift fails a PR rather than a tag push.

One thing needed from a maintainer before the first release

The workflow uses npm trusted publishing, so no token is stored here - but the publisher has to be registered once on npmjs.com:

  • Package verdict-cli -> Settings -> Trusted publisher
  • Owner tuandm, repository verdict-cli, workflow filename release.yml, environment blank

Until that exists the publish step 401s. The workflow asserts npm >= 11.5.1 (the trusted-publishing floor) rather than assuming the node-version: '24' pin provides it.

Testing

actionlint clean. version-sync.test.mjs passes, and fails as expected against the pre-fix manifest. npm pack --dry-run confirms scripts/ and RELEASING.md stay out of the tarball. The browser suite was not run locally - PR CI runs it here.

Follow-up, not in this PR

The README build badge points at ci.yml; the workflow is pr-ci.yml, so the badge renders as unknown.

Releases were cut by hand: a version bump commit, an npm publish from a
laptop, no tag, and no release page. Nothing tied the published tarball to a
commit that had passed CI.

A push of a `v*` tag now runs release.yml, which refuses the tag unless it is
an ancestor of main and unless package.json matches it, runs the suite on real
Chromium, publishes with npm trusted publishing (OIDC, no stored token), and
creates the release page from the merged PRs.

The plugin manifest carries its own version that `npm version` does not touch,
and it had already drifted to 0.1.0 against a published 0.1.1. A `version`
lifecycle script syncs it into the same commit, and a test asserts the two
agree so the drift surfaces on a PR rather than on a tag push.
Semver lets a 0.x line ship additive changes as a patch, and the 0.1.x
releases have done exactly that. Left as written, the rules would be
contradicted by the first release cut under them.
npm does allow unpublishing within 72 hours; it is the permanent block on
reuse afterwards that makes fixing forward the only option. The advice was
right, the reason given for it was not.

The 0.x clause claimed minor marks a break. Semver says nothing of the
kind for 0.x, and this package has not done that - 0.1.1 carried new flags
and the BROWSE_* to VERDICT_* rename together, as a patch.
The backfill section gave only 0.1.1's target, leaving 0.1.0's to be
derived. Both now come from npm's own gitHead rather than from reading the
log, which is the only source that actually knows what was published.

Says outright that the backfill is optional and not a prerequisite for the
next release, so nobody sequences work around it.
@tuandm
tuandm merged commit 835f525 into tuandm:main Sep 14, 2026
4 checks passed
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.

2 participants