diff --git a/.github/workflows/self-test.yml b/.github/workflows/self-test.yml index 70ab619..ab1a105 100644 --- a/.github/workflows/self-test.yml +++ b/.github/workflows/self-test.yml @@ -173,6 +173,23 @@ jobs: } const exec = rel.plugins.find(p => Array.isArray(p) && p[0] === "@semantic-release/exec"); if (!/git tag -f v/.test(exec[1].successCmd)) throw new Error("releaserc: the major tag is never moved"); + const analyzer = rel.plugins.find(p => Array.isArray(p) && p[0] === "@semantic-release/commit-analyzer"); + const deps = (analyzer[1].releaseRules || []).some(r => r.type === "chore" && r.scope === "deps" && r.release === "patch"); + if (!deps) throw new Error("releaserc: chore(deps) no longer cuts a patch, so v1 stops moving on dependency updates"); + const notes = rel.plugins.find(p => Array.isArray(p) && p[0] === "@semantic-release/release-notes-generator"); + const types = ((notes[1].presetConfig || {}).types) || []; + if (!types.length) throw new Error("releaserc: no changelog sections configured"); + // Written as literal UTF-8. An earlier attempt lost every emoji in + // transit and produced a plain changelog that looked deliberate, + // so this asserts they survived rather than trusting the file. + const plain = types.filter(t => !t.hidden && !/[^\x00-\x7F]/.test(t.section || "")); + if (plain.length) throw new Error("releaserc: changelog section without an emoji: " + plain.map(t => t.section).join(", ")); + // conventional-changelog takes the FIRST matching entry, so a bare + // chore listed before the scoped one swallows chore(deps) and hides it. + const depsAt = types.findIndex(t => t.type === "chore" && t.scope === "deps"); + const choreAt = types.findIndex(t => t.type === "chore" && !t.scope); + if (depsAt === -1) throw new Error("releaserc: chore(deps) has no changelog section, so a dependency release would be empty"); + if (choreAt !== -1 && choreAt < depsAt) throw new Error("releaserc: bare chore is listed before chore(deps), which hides dependency updates"); const ren = JSON.parse(fs.readFileSync("renovate.json", "utf8")); if (!ren.baseBranchPatterns.includes("main")) throw new Error("renovate: baseBranchPatterns must name main, the preset says develop"); JSON.parse(fs.readFileSync("package.json", "utf8")); diff --git a/.releaserc.json b/.releaserc.json index cebcf53..4bd241b 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -1,16 +1,107 @@ { - "branches": ["main"], + "branches": [ + "main" + ], "plugins": [ - ["@semantic-release/commit-analyzer", { "preset": "conventionalcommits" }], - ["@semantic-release/release-notes-generator", { "preset": "conventionalcommits" }], + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "type": "chore", + "scope": "deps", + "release": "patch" + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "✨ Features", + "hidden": false + }, + { + "type": "fix", + "section": "🐛 Bug Fixes", + "hidden": false + }, + { + "type": "perf", + "section": "⚡ Performance Improvements", + "hidden": false + }, + { + "type": "revert", + "section": "⏪ Reverts", + "hidden": false + }, + { + "type": "chore", + "scope": "deps", + "section": "⬆️ Dependencies", + "hidden": false + }, + { + "type": "docs", + "hidden": true + }, + { + "type": "style", + "hidden": true + }, + { + "type": "refactor", + "hidden": true + }, + { + "type": "test", + "hidden": true + }, + { + "type": "build", + "hidden": true + }, + { + "type": "ci", + "hidden": true + }, + { + "type": "chore", + "hidden": true + } + ] + }, + "writerOpts": { + "commitsSort": [ + "scope", + "subject" + ] + } + } + ], "@semantic-release/changelog", - ["@semantic-release/git", { - "assets": ["CHANGELOG.md"], - "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" - }], + [ + "@semantic-release/git", + { + "assets": [ + "CHANGELOG.md" + ], + "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" + } + ], "@semantic-release/github", - ["@semantic-release/exec", { - "successCmd": "git tag -f v${nextRelease.version.split('.')[0]} ${nextRelease.gitTag} && git push -f origin v${nextRelease.version.split('.')[0]}" - }] + [ + "@semantic-release/exec", + { + "successCmd": "git tag -f v${nextRelease.version.split('.')[0]} ${nextRelease.gitTag} && git push -f origin v${nextRelease.version.split('.')[0]}" + } + ] ] } diff --git a/AGENTS.md b/AGENTS.md index e1abde4..03bf2a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -121,6 +121,13 @@ Two consequences: comes from the commit messages and the tag follows. Write conventional commits or nothing is released. +This repo also releases on `chore(deps)`, which the shared Renovate preset +deliberately makes inert everywhere else. The dependencies here are the action +versions these workflows run on, so a bump that never reached a release would +leave every caller pinned to `@v1` on the old ones. The rule is scoped to the +`deps` scope, so a plain `chore:` still releases nothing, and `Self test` fails +if the rule is removed. + ## This repo's own CI is hosted, and has to be `Self test` and `Publish release` both run on `ubuntu-latest`. That is not a diff --git a/README.md b/README.md index 0da89a6..eb613bf 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,12 @@ Releases are cut by semantic-release from `main`, so the version comes from the commit messages. `feat:` opens a minor, `fix:` a patch, and a breaking change footer a major. +`chore(deps)` also cuts a patch, which is specific to this repo. Renovate labels +every dependency update `chore(deps)`, and elsewhere that deliberately releases +nothing. Here the dependencies are the action versions these workflows run on, so +a bump that never reached a release would leave every caller pinned to `@v1` on +the old ones. A plain `chore:` with no `deps` scope still releases nothing. + Do not pin `@main`. Every caller used to, which meant one commit here took effect everywhere at once, with no staging and no way back except another commit while CI was already broken.