Gate build + release on the version bump #490

Merged
johnsturgeon merged 1 commit from gate-build-release-on-the-version-bump-488 into main 2026-09-07 11:46:19 +02:00
Owner

Makes releases deliberate: the version bump is the trigger. A merge whose pyproject.toml version differs from the latest release cuts one release and builds one image; a merge that leaves the version untouched produces nothing.

Changes

  • release-on-merge.yml — keeps the "release for this version already exists → no-op" check as the bump gate, then on a real bump aggregates notes across the whole span: every PR merged in (prev-release-target .. MERGE_SHA], each with its title/link and the issues it closed (reusing the close/fix/resolve keyword scan), plus a compare changelog link.
  • build-image.yml — triggers on release: [published] (chained off the release, which is created with the real FJ_TOKEN PAT so the event isn't suppressed) plus workflow_dispatch for re-running a failed build against an existing tag. No longer builds on every push to main.
  • CLAUDE.md — Deployment section updated for the gated flow.

Note on this PR's own merge

The merge event runs release-on-merge.yml from the base branch (the old single-PR version), so this landing cuts an old-style single-PR release for 2.31.2. The aggregated span-notes flow takes effect from the next release onward.

resolves #488

🤖 Generated with Claude Code

Makes releases deliberate: the version bump is the trigger. A merge whose `pyproject.toml` version differs from the latest release cuts one release and builds one image; a merge that leaves the version untouched produces nothing. ## Changes - **`release-on-merge.yml`** — keeps the "release for this version already exists → no-op" check as the bump gate, then on a real bump aggregates notes across the whole span: every PR merged in `(prev-release-target .. MERGE_SHA]`, each with its title/link and the issues it closed (reusing the close/fix/resolve keyword scan), plus a compare changelog link. - **`build-image.yml`** — triggers on `release: [published]` (chained off the release, which is created with the real `FJ_TOKEN` PAT so the event isn't suppressed) plus `workflow_dispatch` for re-running a failed build against an existing tag. No longer builds on every push to `main`. - **CLAUDE.md** — Deployment section updated for the gated flow. ## Note on this PR's own merge The merge event runs `release-on-merge.yml` from the base branch (the old single-PR version), so this landing cuts an old-style single-PR release for 2.31.2. The aggregated span-notes flow takes effect from the next release onward. resolves #488 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Gate build and release on the version bump
All checks were successful
Claude Code Review / claude-review (pull_request) Successful in 1m41s
Python FastAPI Jinja Linting / build (pull_request) Successful in 32s
Tests / pytest (pull_request) Successful in 16s
Release on merge / release (pull_request) Successful in 0s
8feb6478cf
Cut a release only when a merge changes pyproject's version, and build
the image off that published release instead of every push to main. An
unbumped merge now produces no release, no tag, and no image.

Release notes aggregate every PR merged since the previous release
(prev-release-target..MERGE_SHA), each with its title/link and closed
issues, rather than just the triggering PR.

resolves #488

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

Both workflow files reviewed against the diff and the surrounding logic. Here's my review.


This PR cleanly inverts the release trigger so the version bump — not the merge — is what cuts a release and build, and the aggregated span-notes logic is well thought through (correct handling of the no-previous-release case, untrusted PR text kept out of shell interpolation via jq -n, and the triggering PR always force-included even if the compare walk finds nothing). I found no blocking issues; a few robustness gaps in the notes aggregation are worth a look.

Worth fixing

  • Intermediate PRs merged without a merge commit are dropped from the notes. release-on-merge.yml:81 filters the compare commits with grep -E '^Merge pull request ' and pulls the number from (#N). That only matches Forgejo's merge-commit strategy; any PR in the span that was squash- or rebase-merged has no such commit and silently vanishes from the aggregated changelog. The triggering PR is safe (it's prepended from $PR_NUMBER at release-on-merge.yml:89), but unbumped PRs that landed since the last release are exactly the ones this feature is meant to surface. Since the repo currently uses merge commits this is latent, but worth a comment noting the dependency, or matching on the PR-number pattern more loosely.

  • The compare walk may be paginated. release-on-merge.yml:79 reads .commits[] from a single compare/$base...$MERGE_SHA call. If Forgejo's compare endpoint caps the returned commit list (Gitea/Forgejo default page sizes apply to several list endpoints), a release spanning many unbumped merges could quietly lose the older PRs. Worth verifying whether that endpoint returns the full range or needs ?limit=/paging — a silent truncation reads as "complete notes" when it isn't.

Nits

  • workflow_dispatch builds main's current HEAD, not the dispatched tag. build-image.yml:62-63 falls back to parsing pyproject.toml from the checked-out tree, and the checkout at build-image.yml:47 has no ref, so a manual re-run resolves to whatever main points at now. The header comment (build-image.yml:11) calls this "rebuilds an existing tag," but if main has advanced past that release the :<ver> and :<sha> tags won't match the release you meant to rebuild. Consider accepting a ref/tag dispatch input, or softening the comment.

  • :latest can still be raced by two release builds. The concurrency group is now build-image-${{ github.ref }} (build-image.yml:20), and on a release event github.ref is the tag — so two distinct releases publishing close together fall into different groups and could overlap on the :latest push. Release creation is serialized upstream by the release-on-merge concurrency group, so this is low-probability, but the old comment that explained the :latest race was removed while the race isn't fully gone. A one-line note or a static group for the push step would preserve the rationale.

  • PR-title numbers can leak into the issue/PR scan. release-on-merge.yml:82-83 greps every (#N) out of the merge message; if a PR title itself contains (#123) you'd pick up a spurious number. Cosmetic, and the per-PR lookup degrades to a bare - #N line, so no crash — just noise.

The version bump to 2.31.2 and matching uv.lock update are correct per the CLAUDE.md convention, and the CLAUDE.md Deployment section accurately reflects the new gated flow.

<!-- claude-code-review --> Both workflow files reviewed against the diff and the surrounding logic. Here's my review. --- This PR cleanly inverts the release trigger so the version bump — not the merge — is what cuts a release and build, and the aggregated span-notes logic is well thought through (correct handling of the no-previous-release case, untrusted PR text kept out of shell interpolation via `jq -n`, and the triggering PR always force-included even if the compare walk finds nothing). I found no blocking issues; a few robustness gaps in the notes aggregation are worth a look. ## Worth fixing - **Intermediate PRs merged without a merge commit are dropped from the notes.** `release-on-merge.yml:81` filters the compare commits with `grep -E '^Merge pull request '` and pulls the number from `(#N)`. That only matches Forgejo's merge-commit strategy; any PR in the span that was squash- or rebase-merged has no such commit and silently vanishes from the aggregated changelog. The triggering PR is safe (it's prepended from `$PR_NUMBER` at `release-on-merge.yml:89`), but unbumped PRs that landed since the last release are exactly the ones this feature is meant to surface. Since the repo currently uses merge commits this is latent, but worth a comment noting the dependency, or matching on the PR-number pattern more loosely. - **The compare walk may be paginated.** `release-on-merge.yml:79` reads `.commits[]` from a single `compare/$base...$MERGE_SHA` call. If Forgejo's compare endpoint caps the returned commit list (Gitea/Forgejo default page sizes apply to several list endpoints), a release spanning many unbumped merges could quietly lose the older PRs. Worth verifying whether that endpoint returns the full range or needs `?limit=`/paging — a silent truncation reads as "complete notes" when it isn't. ## Nits - **`workflow_dispatch` builds `main`'s current HEAD, not the dispatched tag.** `build-image.yml:62-63` falls back to parsing `pyproject.toml` from the checked-out tree, and the checkout at `build-image.yml:47` has no ref, so a manual re-run resolves to whatever `main` points at now. The header comment (`build-image.yml:11`) calls this "rebuilds an existing tag," but if `main` has advanced past that release the `:<ver>` and `:<sha>` tags won't match the release you meant to rebuild. Consider accepting a `ref`/`tag` dispatch input, or softening the comment. - **`:latest` can still be raced by two release builds.** The concurrency group is now `build-image-${{ github.ref }}` (`build-image.yml:20`), and on a `release` event `github.ref` is the tag — so two distinct releases publishing close together fall into different groups and could overlap on the `:latest` push. Release *creation* is serialized upstream by the `release-on-merge` concurrency group, so this is low-probability, but the old comment that explained the `:latest` race was removed while the race isn't fully gone. A one-line note or a static group for the push step would preserve the rationale. - **PR-title numbers can leak into the issue/PR scan.** `release-on-merge.yml:82-83` greps every `(#N)` out of the merge message; if a PR title itself contains `(#123)` you'd pick up a spurious number. Cosmetic, and the per-PR lookup degrades to a bare `- #N` line, so no crash — just noise. The version bump to `2.31.2` and matching `uv.lock` update are correct per the CLAUDE.md convention, and the CLAUDE.md Deployment section accurately reflects the new gated flow.
johnsturgeon deleted branch gate-build-release-on-the-version-bump-488 2026-09-07 11:46:19 +02:00
Sign in to join this conversation.
No description provided.