ci: make a release a shipment rather than a merge
CI / check (push) Skipped
CI / e2e (push) Skipped
CI / check (pull_request) Successful in 2m25s
CI / e2e (pull_request) Successful in 6m13s

release.yml fired on every push to main, so the trigger was "a PR was
merged" and nothing else decided. That is a version per unit of *work*
rather than per *shipment*: eight releases in twenty-two hours, v0.0.1
through v0.3.1, for one session -- each fanning out to four publishers on
a runner with capacity 1, so roughly forty packaging jobs shipped three
issues while ordinary PR CI queued behind them. pacman, Homebrew and
Obtainium see every one.

The push trigger is gone and workflow_dispatch, which was already there
and already worked, is the whole mechanism. Nothing else had to change to
batch releases, because semantic-release already reads every commit since
the last tag: five fixes and two feats become one minor release with all
seven in the notes. Release frequency was only ever how often this file
fired.

This is the rule index-artifact.yml states and is the other instance of:
a job that mutates state which cannot be rebuilt in ten minutes is
triggered deliberately, not by a push. A release here is a tag, a Gitea
release, an Arch package, a Homebrew formula, a signed APK and desktop
assets -- and an Android version going backwards costs the user their
library.

`dry_run` is what makes a manual trigger usable: the point of pulling a
lever by hand is being able to look first, so the input runs
semantic-release --dry-run -- the version and the notes, no tag, no
release, no publishers. Anything but the literal string "true" releases
for real, because a typo in a dispatch box must not silently turn a
shipment into a green no-op.

Two alternatives were considered and rejected, both recorded on the
issue. A `beta` integration branch relocates the trigger rather than
removing one: it needs a second protected branch carrying the same
required checks, and it *adds* a full check + e2e run per batch on the
very runner whose queue is the complaint. A schedule batches without
anyone having to remember, but puts the decision back on a timer, which
is the thing being removed.

Closes #115
This commit is contained in:
2026-08-18 22:25:11 -04:00
parent b2fe1cb1e0
commit 90f1239fba
7 changed files with 156 additions and 33 deletions
+11 -5
View File
@@ -192,15 +192,21 @@ skill-check: ## Fail if the agent docs name a missing make target, or AGENTS.md
commit-check: ## Fail if a commit subject is not a Conventional Commit
@./scripts/commit-check.sh $(if $(RANGE),--range $(RANGE))
# What a merge to main would release, without releasing it. Reads the
# same .releaserc.yml CI does, so "why did that not cut a version" is
# answerable locally instead of by pushing and watching. Needs no
# credentials: --dry-run neither tags nor publishes.
# What running the release workflow now would ship, without shipping it.
# Reads the same .releaserc.yml CI does, so "why did that not cut a
# version" is answerable locally instead of by pushing and watching.
# Needs no credentials: --dry-run neither tags nor publishes.
#
# release.yml is dispatch-only, so this answers the question that
# actually gets asked now -- what has accumulated since the last tag --
# rather than what one merge would have done. The workflow's own
# `dry_run` input is the same answer from the runner, against whatever
# main points at rather than the working tree.
#
# The pins must stay identical to release.yml's, which is where the note
# on holding the conventionalcommits preset at 9 lives -- at 10 the
# release notes come out empty with everything green.
release-dry: ## Print the version a merge to main would release
release-dry: ## Print the version a release run would cut right now
@npx --yes \
-p semantic-release@25 \
-p @semantic-release/commit-analyzer@13 \