-
-
Notifications
You must be signed in to change notification settings - Fork 18
Description
Problem
Releases are currently a manual process: bump package.json, create a git tag, push the tag (which triggers publish.yml for npm), and manually draft a GitHub release. This is handled partly through mise but remains a manual, error-prone workflow.
Additionally, the project already follows conventional commits (feat(), fix(), refactor(), etc.) but does not leverage them for automated versioning or changelog generation.
Current state
| Aspect | Status |
|---|---|
| Version scheme | YYYY.MM.PATCH (e.g., 2025.12.3) |
| Tag format | Inconsistent — v2025.12.3 vs 2025.11.2 |
| Release trigger | Manual tag push → publish.yml |
| Changelog | Manual, hand-written release notes |
| Pre-releases | Not automated, next branch exists but unused for releases |
| Commit convention | Conventional commits already adopted |
Proposal
Introduce semantic-release to fully automate the release pipeline — version calculation, changelog generation, GitHub releases, and npm publishing — while preserving the existing YYYY.MM.PATCH calendar versioning scheme.
Version scheme: YYYY.MM.PATCH
This is not standard semver. The scheme works as follows:
YYYY— current year (e.g.,2026)MM— current month, no leading zero for single digits (e.g.,2for February)PATCH— incremental counter within that year+month, starting at1, resets each month
Examples:
2025.11.1 → 2025.11.2 → 2025.11.3 (same month, patch increments)
2025.11.3 → 2025.12.1 (new month, patch resets to 1)
2025.12.3 → 2026.1.1 (new year+month, patch resets to 1)
Pre-releases from the next branch should follow:
2026.2.1-next.1 → 2026.2.1-next.2 (incremental pre-release)
Why not just use semver?
The project already has 5 published releases under the YYYY.MM.PATCH scheme on npm. Switching to semver would break the version continuity and confuse existing users. The calendar scheme also communicates release freshness at a glance, which is a deliberate choice.
Technical approach
Custom version plugin
Since semantic-release expects semver internally, we need a custom plugin (or a calver adapter) to override version calculation. The logic:
function getNextVersion(lastRelease, commits):
now = currentDate()
currentYM = now.year + "." + now.month
if lastRelease is in same YYYY.MM:
return currentYM + "." + (lastRelease.patch + 1)
else:
return currentYM + ".1"
Key detail: commit type does not affect version bumping — any releasable commit (feat, fix, perf, etc.) increments the patch. There is no "major" or "minor" bump concept in this scheme. Commit types are only used for changelog categorization.
Options to implement this:
semantic-release-calver— community plugin for calendar versioning. Needs evaluation for compatibility withYYYY.MM.PATCH(most calver plugins targetYYYY.MM.DDorYYYY.0M.PATCH).- Custom inline plugin — a small
.releaserc.cjsplugin (~30 lines) that implements theanalyzeCommitsstep and overrides version viasemantic-release/exec. Most flexible and keeps control in-repo. - Fork/wrap
@semantic-release/commit-analyzer— intercept after analysis and remap to calver. More complex than needed.
Recommendation: Option 2 (custom inline plugin) — minimal dependency, full control, easy to understand and maintain.
Branch configuration
{
"branches": [
{ "name": "main" },
{ "name": "next", "prerelease": "next" }
]
}main→ stable releases (2026.2.1)next→ pre-releases (2026.2.1-next.1)
Plugin pipeline
{
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
"@semantic-release/github"
]
}- commit-analyzer — determines if a release is needed (any
feat,fix,perf,revertcommit) - release-notes-generator — generates structured changelog from conventional commits
- npm — publishes to npm with provenance (replaces current
publish.ymllogic) - github — creates GitHub release with generated notes
Tag format
Standardize on v prefix going forward:
{
"tagFormat": "v${version}"
}This aligns with the most recent tags (v2025.12.3, v2025.12.2) and the existing publish.yml trigger pattern.
CI workflow
Replace the current manual publish.yml with a semantic-release job in ci.yml (or a new release.yml):
release:
name: Release
runs-on: ubuntu-latest
needs: [test, lint]
if: >-
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next')
permissions:
contents: write # Create tags + GitHub releases
issues: write # Comment on released issues
pull-requests: write # Comment on released PRs
id-token: write # npm provenance
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Release
run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}Migration checklist
- Install semantic-release and plugins as dev dependencies
- Create
.releaserc.cjs(or equivalent) with calver plugin + branch config - Implement the custom
YYYY.MM.PATCHversion calculation - Add release job to CI workflow (gated on
test+lintpassing) - Configure repository secrets (
NPM_TOKEN—GITHUB_TOKENis automatic) - Normalize existing tags to
vprefix (or configure semantic-release to recognize both) - Test dry-run on
nextbranch to validate pre-release flow - Remove or deprecate the manual
publish.ymlworkflow - Remove release-related mise tasks (if any beyond tool versions)
- Update
AGENTS.md/ contributor docs with new release process - Document that version bumps in
package.jsonare now automated (don't manually edit)
Risks and considerations
- Existing tag inconsistency: Tags
2025.11.1(nov) andv2025.12.3(withv) coexist. semantic-release needs to find the latest tag regardless of prefix. May need a one-time tag normalization or configuretagFormatto handle both. - npm provenance: Current
publish.ymluses OIDC (id-token: write). The new workflow should preserve this. package.jsonversion: semantic-release will update this automatically. Contributors should not manually bump it.- Protected branches: If
mainis protected, semantic-release needs a PAT or GitHub App token to push tags (the defaultGITHUB_TOKENmay be insufficient depending on branch protection rules).