ci: add a "Cut release" dispatch workflow

Stamps every package.json, commits chore(release): vX.Y.Z, tags and pushes
both refs in one dispatch — patch/minor/major, or an explicit number. Cutting
a release from a laptop is how a manifest bump gets forgotten or a tag lands
on an unpushed commit; the only input here is the number.

Guards: refuses a version that already exists as a tag (releases are
immutable), a no-op bump, a leading `v`, and a malformed number. Checkout is
full-depth because the duplicate-tag check is meaningless against a shallow
clone.

Pushes with a RELEASE_TOKEN PAT rather than the built-in Actions token —
whether a push made with that token re-triggers build.yml depends on the Gitea
version, and a release that quietly publishes no images is worse than one that
fails outright.

Builds and deploys stay separate: the tag push triggers build.yml, and
deploying remains a deliberate dispatch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 01:21:13 -07:00
co-authored by Claude Opus 5
parent b59abda895
commit a8afd87c3f
2 changed files with 181 additions and 1 deletions
+15 -1
View File
@@ -5,13 +5,20 @@ rollbacks possible.
## The short version
Dispatch **Cut release** from the Actions tab and pick `patch`, `minor` or
`major` (or `explicit` plus a number). It stamps every `package.json`, commits
`chore(release): vX.Y.Z`, tags, and pushes both refs in one go. It refuses a
version that already exists as a tag, and refuses a no-op bump.
The equivalent by hand, if you would rather cut it locally:
```bash
pnpm version:set 1.2.0 # stamp every package.json
git commit -am "chore(release): v1.2.0"
git tag v1.2.0 && git push origin master v1.2.0
```
That push triggers `.gitea/workflows/build.yml`, which builds **both** images in
Either way that push triggers `.gitea/workflows/build.yml`, which builds **both** images in
one matrix run and publishes:
| tag pushed | image tags produced |
@@ -28,6 +35,13 @@ Then dispatch a deploy from the Actions tab:
> `{{version}}` strips it. Git tag `v1.2.0`, dispatch `1.2.0`. Dispatching
> `v1.2.0` deploys nothing that exists.
**Cut release needs a `RELEASE_TOKEN` secret** — a Gitea PAT with
`write:repository`. It does not use the built-in Actions token on purpose:
whether a push made with that token re-triggers `build.yml` depends on the Gitea
version, and a release that quietly publishes no images is worse than one that
fails outright. If the build somehow does not start, `build.yml` has
`workflow_dispatch` — run it against the new tag by hand.
Because api and web are built from one matrix run, they cannot drift at build
time. They *can* drift at deploy time if a stack is applied with only one image
moved — the web footer shows both versions and flags a mismatch, and the deploy