Create a release on every merge to main #478

Merged
johnsturgeon merged 1 commit from create-a-new-release-every-time-a-pr-lands-477 into main 2026-09-06 19:36:29 +02:00
Owner

Adds a Forgejo workflow that cuts a release + tag whenever a PR merges to main.

  • Fires on pull_request: closed when merged == true
  • Reads the version from pyproject.toml at the merge commit (via the /raw/ API, no checkout)
  • Tag = plain version, so git tag / image tag / pyproject version are one string
  • Release notes = PR title + body + a bulleted list of issues closed (scanned from the PR body and its commit messages for resolves/closes/fixes keywords)
  • Skips (green, with a warning) if a release for that version already exists, covering a PR that forgot to bump

Note: this PR itself will not produce a release — the closed-PR event runs the workflow as it exists on main, which does not have the file until after this lands. The first real release will be the next PR merged after this.

resolves #477

🤖 Generated with Claude Code

Adds a Forgejo workflow that cuts a release + tag whenever a PR merges to `main`. - Fires on `pull_request: closed` when `merged == true` - Reads the version from `pyproject.toml` at the merge commit (via the `/raw/` API, no checkout) - Tag = plain version, so git tag / image tag / pyproject version are one string - Release notes = PR title + body + a bulleted list of issues closed (scanned from the PR body and its commit messages for `resolves`/`closes`/`fixes` keywords) - Skips (green, with a warning) if a release for that version already exists, covering a PR that forgot to bump Note: this PR itself will not produce a release — the closed-PR event runs the workflow as it exists on `main`, which does not have the file until after this lands. The first real release will be the next PR merged after this. resolves #477 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Create a release on every merge to main
All checks were successful
Claude Code Review / claude-review (pull_request) Successful in 1m48s
Python FastAPI Jinja Linting / build (pull_request) Successful in 28s
Tests / pytest (pull_request) Successful in 15s
Release on merge / release (pull_request) Successful in 1s
e28674063b
Add a Forgejo workflow that fires on a merged PR, reads the version from
pyproject.toml at the merge commit, and publishes a matching release and
tag. The notes carry the PR title and body plus a bulleted list of every
issue the PR closed (scanned from the body and its commit messages).

resolves #477

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

I have enough context. The version-parsing approach matches the existing build-image.yml convention, and the FJ_API_URL/FJ_TOKEN env pattern is consistent with claude.yml and claude-code-review.yml. Here is my review.


This adds a well-constructed workflow that publishes a Forgejo release + tag on each merge to main, with untrusted PR text handled safely (env vars + jq --arg, never interpolated into the shell) and a sensible no-op path for un-bumped versions. The version-parsing, FJ_API_URL/FJ_TOKEN, and self-hosted conventions all match the existing workflows, and the version bump (2.29.0 → 2.30.0, with uv.lock) follows CLAUDE.md. No blocking issues.

Worth fixing

  • Commit-message scan is silently capped at one pagerelease-on-merge.yml:87. GET /pulls/{n}/commits paginates (Forgejo default ~30 per page), so a PR with more than a page of commits will only have its early commits scanned for closing keywords. In practice the PR body is the primary source and usually carries the resolves #n, so this rarely bites — but when it does it fails silently (fewer issues listed, no warning). If you want it robust, either add ?limit=50 (or paginate) or lean on the body only. Minor, since the body scan is the real workhorse.

Nits

  • The graceful "could not parse version" branch is effectively unreachablerelease-on-merge.yml:65-70. Under set -euo pipefail, if the raw/pyproject.toml fetch 404s (bad ref) or the file has no version = line, either curl -sSf or grep -m1 exits non-zero, the command substitution's pipeline fails, and set -e aborts before the if [[ -z "$version" ]] check runs. The script still fails loudly, so this is cosmetic — the custom ::error:: message just never prints. (Same latent pattern exists in build-image.yml:56-60, but there grep reads a local file so it's even less likely to trip.)

  • Duplicate detection only covers releases, not bare tagsrelease-on-merge.yml:76-82. GET /releases/tags/$version returns 404 if a git tag named $version exists without an associated release (e.g. one created manually or by a prior partial run). The subsequent POST /releases would then fail on the tag conflict rather than hitting the clean "already exists" no-op. Edge case; the normal path where build-image/this workflow own the tags is fine.

Nice touches worth calling out: curl -sS (no -f) for the existence check so a 404 doesn't trip set -e, the global concurrency group to serialize back-to-back merges carrying the same version, and the header comment explaining why this PR won't release itself.

<!-- claude-code-review --> I have enough context. The version-parsing approach matches the existing `build-image.yml` convention, and the `FJ_API_URL`/`FJ_TOKEN` env pattern is consistent with `claude.yml` and `claude-code-review.yml`. Here is my review. --- This adds a well-constructed workflow that publishes a Forgejo release + tag on each merge to `main`, with untrusted PR text handled safely (env vars + `jq --arg`, never interpolated into the shell) and a sensible no-op path for un-bumped versions. The version-parsing, `FJ_API_URL`/`FJ_TOKEN`, and `self-hosted` conventions all match the existing workflows, and the version bump (2.29.0 → 2.30.0, with `uv.lock`) follows CLAUDE.md. No blocking issues. ### Worth fixing - **Commit-message scan is silently capped at one page** — `release-on-merge.yml:87`. `GET /pulls/{n}/commits` paginates (Forgejo default ~30 per page), so a PR with more than a page of commits will only have its early commits scanned for closing keywords. In practice the PR *body* is the primary source and usually carries the `resolves #n`, so this rarely bites — but when it does it fails silently (fewer issues listed, no warning). If you want it robust, either add `?limit=50` (or paginate) or lean on the body only. Minor, since the body scan is the real workhorse. ### Nits - **The graceful "could not parse version" branch is effectively unreachable** — `release-on-merge.yml:65-70`. Under `set -euo pipefail`, if the `raw/pyproject.toml` fetch 404s (bad `ref`) or the file has no `version =` line, either `curl -sSf` or `grep -m1` exits non-zero, the command substitution's pipeline fails, and `set -e` aborts *before* the `if [[ -z "$version" ]]` check runs. The script still fails loudly, so this is cosmetic — the custom `::error::` message just never prints. (Same latent pattern exists in `build-image.yml:56-60`, but there `grep` reads a local file so it's even less likely to trip.) - **Duplicate detection only covers releases, not bare tags** — `release-on-merge.yml:76-82`. `GET /releases/tags/$version` returns 404 if a git *tag* named `$version` exists without an associated release (e.g. one created manually or by a prior partial run). The subsequent `POST /releases` would then fail on the tag conflict rather than hitting the clean "already exists" no-op. Edge case; the normal path where `build-image`/this workflow own the tags is fine. Nice touches worth calling out: `curl -sS` (no `-f`) for the existence check so a 404 doesn't trip `set -e`, the global `concurrency` group to serialize back-to-back merges carrying the same version, and the header comment explaining why this PR won't release itself.
johnsturgeon deleted branch create-a-new-release-every-time-a-pr-lands-477 2026-09-06 19:36:29 +02:00
Sign in to join this conversation.
No description provided.