ci: release pull requests, and conventional-commit PR titles (#1)
CI / build (push) Successful in 9s
Release / release (push) Successful in 2s
CI / image (push) Successful in 21s

Releases as pull requests, the same in all five app repos (hub, catalog, live, riftbound, karaoke). The Gitea side (PRs only into main, squash merges, required checks, `v*` tags only by release-bot, its `RELEASE_TOKEN` org secret) is dhkatz/infra `modules/app-repo`.

- **`.gitea/workflows/pull-request.yml`**: the PR title must be a conventional commit (`feat(api): …`, `fix: …`, `feat!: …`); it becomes the commit on main. `chore(release)` only from release-bot's `release` branch. Required to merge (`Pull request / title`).
- **`.gitea/default_merge_message/SQUASH_TEMPLATE.md`**: the squash commit is `<PR title> (#<n>)` plus the description.
- **`.gitea/workflows/release.yml`**, on every push to main, as release-bot (`RELEASE_TOKEN`):
  - if the commit is a merged `chore(release): vX.Y.Z`: tag it vX.Y.Z (lightweight), push the tag (CI builds the image, Komodo deploys prod), publish the release with git-cliff's notes;
  - otherwise, if anything releasable landed since the last tag: rewrite the `release` branch (CHANGELOG.md) and open or update `chore(release): vX.Y.Z`.
- **`cliff.toml`** (git-cliff 2.14.2, pinned by sha512): feat → minor, fix/perf/revert → patch, breaking → minor while 0.x; chore/ci/docs/test/style/build/refactor alone don't release. Only conventional commits count, so the history before this is left out.
- **`CHANGELOG.md`**: header only; release PRs fill it.
- Agent notes (CLAUDE.md / AGENTS.md): the PR flow and title format; never tag by hand.

After merging, `Release` runs on main and should report nothing to release (this PR is `ci:`); the first `feat:`/`fix:` PR merged after it opens the first release PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #1
Co-authored-by: dhkatz <[email protected]>
This commit was merged in pull request #1.
This commit is contained in:
dhkatz authored and dhkatz committed 2026-11-03 08:40:38 +00:00
1 parent f60ba346f8
commit fbf1e01314
6 files changed
+174

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
# Releases. release-bot keeps one pull request open from the `release` branch, "chore(release): vX.Y.Z": the next
# version and its CHANGELOG.md entry, which git-cliff (cliff.toml) works out from the conventional commits on main
# since the last tag. Merging it makes release-bot tag vX.Y.Z on main and publish the release notes; the tag builds the
# image (ci.yml) and Komodo deploys it to prod (dhkatz/infra). Only release-bot may create v* tags. It acts with its
# own token (the org secret RELEASE_TOKEN), not this workflow's: Gitea starts no workflows for what that one does, so
# the release pull request's checks and the tag's build would never run.
name: Release
on:
push:
branches: [main]
concurrency:
group: release
jobs:
release:
runs-on: ubuntu-latest
env:
CLIFF_VERSION: 2.14.2
CLIFF_SHA512: 95b0c55ab3e351ca7c0285fbbde8f1e8c42906466a173e6884f5c7ca814523bda44cbd4ab26b0aa28cace4c02c5ee6b553e9742bd9d7df47a73b9ef58ea429a9
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Install git-cliff
run: |
archive="git-cliff-$CLIFF_VERSION-x86_64-unknown-linux-musl.tar.gz"
curl -fsSLO "https://github.com/orhun/git-cliff/releases/download/v$CLIFF_VERSION/$archive"
echo "$CLIFF_SHA512 $archive" | sha512sum -c -
mkdir -p "$HOME/.local/bin"
tar -xzf "$archive" --strip-components=1 -C "$HOME/.local/bin" "git-cliff-$CLIFF_VERSION/git-cliff"
rm "$archive"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Tag a merged release, or update the release pull request
env:
TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -euo pipefail
api="$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY"
host="${GITHUB_SERVER_URL#https://}"
git config user.name release-bot
git config user.email "release-bot@${host#git.}"
basic=$(printf 'release-bot:%s' "$TOKEN" | base64 -w0)
push() { git -c http.extraHeader="Authorization: Basic $basic" push "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git" "$@"; }
gitea() { curl -fsS -H "Authorization: token $TOKEN" -H "Content-Type: application/json" "$@"; }
# The release pull request was merged: tag its commit and publish the release.
if [[ "$(git log -1 --format=%s)" =~ ^chore\(release\):\ (v[0-9]+\.[0-9]+\.[0-9]+) ]]; then
tag="${BASH_REMATCH[1]}"
if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then echo "$tag exists already"; exit 0; fi
git tag "$tag"
push "refs/tags/$tag"
jq -n --arg tag "$tag" --arg body "$(git cliff --latest --strip all)" '{tag_name: $tag, name: $tag, body: $body}' |
gitea -o /dev/null -X POST "$api/releases" -d @-
echo "Released $tag"
exit 0
fi
# Otherwise: anything to release since the last tag? (Nothing printed when no commit counts.)
last=$(git describe --tags --abbrev=0 --match 'v[0-9]*' 2>/dev/null || true)
next=$(git cliff --bumped-version 2>/dev/null || true)
next="v${next#v}"
if [ "$next" = v ] || [ "$next" = "$last" ]; then echo "Nothing to release since ${last:-the start}"; exit 0; fi
notes=$(git cliff --unreleased --bump --strip all)
git switch -C release
git cliff --bump --output CHANGELOG.md
git add CHANGELOG.md
git commit -q -m "chore(release): $next"
push --force HEAD:refs/heads/release
title="chore(release): $next"
body=$(printf 'Merging this releases **%s**: release-bot tags it on main, CI builds its image, and Komodo deploys it to prod. It updates itself as pull requests are merged.\n\n%s\n' "$next" "$notes")
payload=$(jq -n --arg title "$title" --arg body "$body" '{title: $title, body: $body}')
open=$(gitea "$api/pulls?state=open&limit=50" | jq -r '[.[] | select(.head.ref == "release")][0].number // empty')
if [ -n "$open" ]; then
gitea -o /dev/null -X PATCH "$api/pulls/$open" -d "$payload"
echo "Updated the release pull request #$open: $title"
else
jq '. + {head: "release", base: "main"}' <<<"$payload" | gitea -o /dev/null -X POST "$api/pulls" -d @-
echo "Opened the release pull request: $title"
fi