Skip to content

ci: release with semantic-release from conventional commits - #35

Open
kurkle wants to merge 1 commit into
chartjs:masterfrom
kurkle:semantic-release
Open

ci: release with semantic-release from conventional commits#35
kurkle wants to merge 1 commit into
chartjs:masterfrom
kurkle:semantic-release

Conversation

@kurkle

@kurkle kurkle commented Sep 12, 2026

Copy link
Copy Markdown
Member

@etimberg — third proposal, independent of #33 and #34. This one has a part only a maintainer can do, so it is up front.

npm publishing here is already broken, and the fix is a setting on npm

The publishing rules changed after this package's last release in 2022:

  • Classic tokens were permanently revoked on 2025-12-09 (changelog). Creation was disabled a month earlier. The NPM_AUTH_TOKEN secret this repository publishes with predates that by three years, so unless it has been rotated since, the current publish-npm.yml would fail at npm publish — whether or not this PR is merged.
  • Granular tokens with write access are capped at 90 days, and tokens that were set to expire later were moved to 2026-02-03. So a replacement token is a recurring chore with a deadline attached.
  • New write tokens require 2FA by default, with an explicit opt-out needed for CI.

The way out is trusted publishing: npm mints a short-lived credential for one workflow run over OIDC, and the repository holds no npm secret at all. @semantic-release/npm@13 supports it — it tries an OIDC token exchange first and only falls back to a token if that fails.

What to configure on npmjs.com (maintainer action)

On the chartjs-test-utils package page → SettingsTrusted publisher:

Field Value
Publisher GitHub Actions
Organization or user chartjs
Repository chartjs-test-utils
Workflow filename release.yml
Environment (leave empty)

That is the release job added in this PR (.github/workflows/release.yml), which runs on pushes to master with id-token: write. npm requires npm CLI ≥ 11.5.1 and Node ≥ 22.14.0 for this; the job runs Node 24, which ships a new enough npm, and prints npm --version so a regression is visible in the log.

Two things worth knowing:

  • The binding is to the file name. If release.yml is ever renamed or the job moves into another workflow file, publishing stops until the trusted publisher is reconfigured — so add the new configuration before renaming, not after.
  • Once a release has gone through it, the package can be set to "Require two-factor authentication and disallow tokens" and the NPM_AUTH_TOKEN secret deleted. OIDC keeps working; only token auth is disabled.

If you would rather keep token publishing, the same workflow works with NPM_TOKEN set to a granular token — but that is the 90-day treadmill described above.

Why semantic-release

Today the version is chosen by whoever publishes, and the notes are assembled by release-drafter from labels. semantic-release derives both from what the commits already say: version, notes, tag, npm publish, GitHub release, and comments on the issues and PRs a release closes. package.json carries 0.0.0-development; the published version is written during the release. The existing v0.5.0 tag is the baseline, so the next release continues from there.

Two configuration choices, both measured rather than assumed

Preset conventionalcommits, not the default angular. Running analyzeCommits with this repo's own .releaserc.json:

patch  <- fix: ...
minor  <- feat: ...
major  <- feat!: ...
null   <- docs: / chore: / refactor: / revert: / test: / ci: / build: / style:
patch  <- perf: ...
major  <- feat: ... + BREAKING CHANGE: footer

The same feat!: ... message under the angular preset analyzes to null — no release at all. The ! marker is silently ignored there, which is a bad way to find out that a breaking change shipped as nothing.

The preset is pinned to ^9. Version 10 requires conventional-changelog-writer >= 9, while @semantic-release/release-notes-generator@14 depends on writer@^8. With ^10 installed, analyzeCommits succeeds and generateNotes throws Missing helper: ... — the version would be decided and the release would then fail while writing the notes. Both plugins were run against this configuration to confirm the pinned pair actually renders notes; the rendered output is a normal ## [1.0.0] ... ### ⚠ BREAKING CHANGES / ### Features / ### Bug Fixes changelog.

Squash merges make the PR title the trigger

This repository allows only squash merges, with the squash commit taking the pull request title (COMMIT_OR_PR_TITLE) and the individual commit messages as the body (COMMIT_MESSAGES). So the pull request title is the release trigger, and a local commit hook would enforce the convention in the one place it does not matter.

pr-title.yml therefore checks the title on every pull request. The squash body still carries the individual commit messages, so a BREAKING CHANGE: footer written in any commit of the PR is honoured — verified with a squash-shaped message (title + concatenated bodies + footer), which analyzes to major.

Verified (re-measured after rebasing onto master with #33 and #34 in)

Running the analyzer and the notes generator over the real commit range
v0.5.0..master, with this branch's own .releaserc.json:

commits since v0.5.0: 3
  major  feat!: replace Karma and Jasmine with Vitest (v1.0.0) (#34)
  null   chore: replace eslint with biome (#33)
  null   Bump engine.io from 6.2.0 to 6.2.1 (#31)

release type for the whole range: major  ->  v0.5.0 becomes 1.0.0

So the first release this produces is 1.0.0, with notes listing the Vitest
change under ⚠ BREAKING CHANGES and Features — no version chosen by hand,
and nothing lost by #34 having merged first.

  • semantic-release --dry-run loads the config and every plugin, and the
    GitHub plugin verifies. The npm plugin fails locally with EINVALIDNPMTOKEN,
    as it should: OIDC exists only inside a GitHub Actions run, and there is no
    token here. That is exactly what the trusted publisher configuration above
    removes.
  • npm test on this branch: Biome clean over 23 files, typecheck clean,
    7 node specs, 24 browser specs plus the 2 deliberate skips in Chromium and
    Firefox.

One thing worth knowing about squash merges and footers

In the generated notes above, the BREAKING CHANGES section carries one extra
line: * chore: adopt biome for the Vitest sources. That is not a bug here —
it is how footers parse. The squash commit of #34 concatenated two commit
messages, the BREAKING CHANGE: footer came from the first one, and
everything after a footer token belongs to that footer until the next token, so
the following commit's subject was absorbed into it.

Practical rule for multi-commit pull requests: put the BREAKING CHANGE:
footer in the last commit of the branch. Same for issue references — a
#123 written anywhere in a commit body shows up in the notes as a closed
reference, which is why the notes above say closes #33.

Also in this PR

  • release-drafter is removed (workflow and config): semantic-release writes the
    same notes from the same commits, and two sources of release notes disagree
    the first time someone forgets a label.
  • publish-npm.yml is removed; release.yml replaces it.
  • The release job installs Chromium and Firefox before npm test, as ci.yml
    does, and no longer runs npm run buildfeat!: replace Karma and Jasmine with Vitest (v1.0.0) #34 removed the rollup bundle.

Relationship to the other proposals

#33 and #34 are both merged, and this rebases cleanly on top of them. The order
turned out not to matter: semantic-release analyzes every commit since the last
tag, not just the newest one, so #34's feat! title still produces the 1.0.0
it describes — measured above rather than assumed.

🤖 Generated with Claude Code

@kurkle
kurkle requested a review from etimberg September 12, 2026 08:36
@etimberg

Copy link
Copy Markdown
Member

Happy with this one as well. Will have to figure out how to make the NPM changes, I need to remember how to get access to the chartjs-ci npm account so I can setup the publishing

Releases are cut by hand today: release-drafter collects labels into a draft,
someone publishes it, and `publish-npm.yml` runs `npm publish` with a token.
That path has two problems. The version is chosen by whoever publishes, and the
npm side of it no longer works: `NPM_AUTH_TOKEN` here predates the classic
token revocation of 2025-12-09, so the next release would fail at the publish
step.

semantic-release decides the version from what the commits say, writes the
notes, tags, publishes, and comments on the issues and pull requests a release
closes. `package.json` carries `0.0.0-development`; the published version is
set during the release.

Preset: `conventionalcommits`, not the default `angular`. Measured with this
repo's own `.releaserc.json`: `feat!: ...` analyzes to `major` under
conventionalcommits and to **null** under angular -- the `!` marker is silently
ignored there, and a breaking change would ship as a patch, or as nothing at
all. Under conventionalcommits: `fix` and `perf` patch, `feat` minor, `!` or a
`BREAKING CHANGE:` footer major, everything else no release.

The preset is pinned to ^9 on purpose. Version 10 requires
conventional-changelog-writer >= 9, while `@semantic-release/release-notes-generator@14`
depends on writer ^8; with ^10 installed, `analyzeCommits` succeeds and
`generateNotes` throws "Missing helper", so a release would fail after the
version had been decided. Both plugins were run against this config to confirm
the pinned combination renders notes.

Because this repository allows only squash merges, and the squash commit takes
the pull request title, the title is the release trigger. `pr-title.yml`
therefore checks the title on every pull request -- that is the enforcement
point, not a local commit hook. The squash body keeps the individual commit
messages, so a `BREAKING CHANGE:` footer written in any of them still counts.

npm publishing moves to trusted publishing (OIDC): no token in the repository,
`id-token: write` on the release job instead. This requires a trusted publisher
to be configured on npm for the `release.yml` workflow of this repository --
see the pull request description.

release-drafter is removed: semantic-release generates the same notes from the
same commits, and two sources of release notes would disagree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kurkle

kurkle commented Sep 12, 2026

Copy link
Copy Markdown
Member Author

Rebased on master now that #33 and #34 are in. Two corrections to what I wrote earlier, both measured this time:

Order does not matter. I implied it did. semantic-release analyzes every commit since the last tag, not only the newest, so #34 having merged first changes nothing. Over the real range v0.5.0..master with this branch's config:

commits since v0.5.0: 3
  major  feat!: replace Karma and Jasmine with Vitest (v1.0.0) (#34)
  null   chore: replace eslint with biome (#33)
  null   Bump engine.io from 6.2.0 to 6.2.1 (#31)

release type for the whole range: major  ->  v0.5.0 becomes 1.0.0

The release job had a stale step. It ran npm run build, which #34 removed along with the rollup bundle, and it did not install browsers — so it would have failed twice over. It now installs Chromium and Firefox and runs npm test, the same way ci.yml does.

The rebase also surfaced something about footers that is worth writing down. In the notes generated above, BREAKING CHANGES carries an extra line, * chore: adopt biome for the Vitest sources. The squash commit of #34 concatenated two commit messages, the BREAKING CHANGE: footer came from the first one, and everything after a footer token belongs to that footer until the next token — so the next commit's subject was absorbed. For multi-commit pull requests the rule is: put the BREAKING CHANGE: footer in the last commit. Likewise a bare #123 anywhere in a commit body becomes a closed reference in the notes, which is why they read closes #33.

Still the one thing I cannot do from here: the npm trusted publisher for this package — GitHub Actions, chartjs / chartjs-test-utils, workflow file release.yml, no environment. Until that exists, this merges safely but the release job will stop at the npm step.

🤖 Generated with Claude Code

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