feat: derive the release bump from conventional commit subjects

The bump was decided by `[major]` / `[minor]` markers alone, so a range of
ordinary `fix:` commits and a range of `feat:` commits released the same way,
and the version number said nothing about what changed.

Read the intent from the subject instead, keeping the explicit markers as
overrides for the cases a subject cannot carry:

  `feat` as its own word   minor   0.1.0 -> 0.2.0
  `!:` or BREAKING         major   0.1.0 -> 1.0.0
  anything else            patch   0.1.0 -> 0.1.1

`feat` has to be a word, so "Fix the feature flag leak" is a patch rather than
a minor. Only the subject line is read; the body is not.

Adds `npm run release:notes`, which prints the notes a push would publish, and
AGENTS.md, which states the rules where an agent will read them.
This commit is contained in:
dsh-slopo agent committed 2026-09-26 06:38:08 +01:00
1 parent e0813bcbea
commit ffefc42e01
4 files changed
+160 -24

No files matched your search

+38 -10
View File
@@ -8,14 +8,21 @@
# them.
#
# The work lives in `.gitea/scripts/release.sh` so it can be run and tested
# without a runner (`bash .gitea/scripts/release.test.sh`).
# without a runner (`bash .gitea/scripts/release.test.sh`, or `npm run
# test:release`).
#
# The bump comes from the commit subjects in the range being released:
# `feat` is a minor, `!:` (or an explicit `[major]`) is a major, and everything
# else — `fix:`, `docs:`, prose — is a patch. `[skip release]` in a subject
# suppresses the release for that push.
name: Release
on:
push:
branches: [master]
# Manual runs, for the first release or to re-cut one after a failed push.
# Manual runs, for the first release (no tags yet) or to publish a release
# whose push failed after the tag was created.
workflow_dispatch:
# One release at a time. Two pushes in quick succession would otherwise race to
@@ -24,11 +31,13 @@ concurrency:
group: release-master
cancel-in-progress: false
permissions:
# The job token is what pushes the version commit, the tag, and the release.
contents: write
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
env:
BRANCH: master
steps:
@@ -37,6 +46,10 @@ jobs:
# Tags are the whole point, so the clone must carry them.
fetch-depth: 0
ref: master
# Gitea mints a token for every job and exposes it as
# secrets.GITEA_TOKEN; `permissions: contents: write` above is what
# lets it push. No hand-made secret is needed.
token: ${{ secrets.GITEA_TOKEN }}
- name: Decide whether this push releases
id: decide
@@ -69,14 +82,23 @@ jobs:
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
git config user.name "release bot"
git config user.email "release@notexpectedyet.com"
git config user.name "gitea-actions"
git config user.email "actions@notexpectedyet.com"
# Idempotency: a run that pushed the tag and then failed at the
# release call must not fail here on the re-run.
if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then
echo "tag v${VERSION} already exists — publishing the release for it"
exit 0
fi
bash .gitea/scripts/release.sh set-version "$VERSION"
bash .gitea/scripts/release.sh changelog-prepend "$VERSION" "$(date -u +%Y-%m-%d)" /tmp/notes.md
git add package.json CHANGELOG.md
# The marker is the second line of defence against releasing this very
# commit: the script also refuses an empty range.
git commit -m "Release v${VERSION} [skip release]"
# `[skip release]` is the guard that has to hold, because the script
# is what decides. `[skip actions]` is the cheap extra: Gitea reads it
# and can skip queueing a run for this commit at all.
git commit -m "Release v${VERSION} [skip release] [skip actions]"
git tag "v${VERSION}"
git push origin "HEAD:${BRANCH}"
git push origin "v${VERSION}"
@@ -87,7 +109,7 @@ jobs:
VERSION: ${{ steps.version.outputs.version }}
SERVER: ${{ github.server_url }}
REPO: ${{ github.repository }}
TOKEN: ${{ github.token }}
TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -euo pipefail
# The body is built by an encoder rather than by string interpolation:
@@ -113,6 +135,12 @@ jobs:
-H 'Content-Type: application/json' \
--data-binary @/tmp/release.json)"
# 409 is "a release for this tag already exists": the job has nothing
# left to do rather than something to report.
if [ "$status" = "409" ]; then
echo "v${VERSION} is already released"
exit 0
fi
if [ "$status" != "201" ] && [ "$status" != "200" ]; then
echo "release creation failed (HTTP ${status})" >&2
cat /tmp/release-response.json >&2