Skip to content

ci(release): adopt the verified release flow - #439

Merged
jimisola merged 3 commits into
mainfrom
ci/adopt-verified-release-flow
Aug 16, 2026
Merged

ci(release): adopt the verified release flow#439
jimisola merged 3 commits into
mainfrom
ci/adopt-verified-release-flow

Conversation

@jimisola

Copy link
Copy Markdown
Member

What & Why

Migrates this repo to the reworked org release flow in
reqstool/.github#66.

The old arrangement stopped at a draft. A human clicked Publish, and only then did
the publish workflows run — so everything that verified a release ran after it was already
public, and a failure meant a bad release was live.

Now the release is created as a prerelease and promoted only once everything that can
fail has succeeded. A prerelease rather than a draft because a draft is invisible to the
verification too (/releases/tags/<tag> 404s unauthenticated), while a prerelease is
readable by exact tag and still excluded from /releases/latest.

prepare  ->  lint + build  ->  [approval]  tag  ->  ...  ->  publish  ->  promote

The confirmation step is an environment approval on the job that tags, reached after
lint and build are already green — so a reviewer approves something verified rather than a
version string.

The version is no longer typed by hand. It is auto-detected from Conventional Commits;
passing one that disagrees requires force, which is the guard that catches "meant 0.3.0,
typed 0.2.0"
before it becomes a tag. prerelease: rc cuts a release candidate — verified
exactly like a release, but never promoted to latest.

dry-run defaults to on: dispatching Release shows the resolved version, the generated
notes, and the raw commits they came from, without tagging or creating anything.

See RELEASING.md for the
full flow.

Author checklist

  • actionlint clean
  • Conventional Commit title, DCO sign-off
  • No behaviour removed without a replacement

Test Plan

Merge order matters: this cannot merge before reqstool/.github#66. The workflows it
calls do not exist on main until that lands.

After merging:

  1. Dispatch Release with dry-run left on. Confirm the job summary shows the resolved
    version, the notes, and the commit list — and that no tag or release exists afterwards.
  2. Dispatch with prerelease: rc and dry-run off. Confirm it pauses for approval, then
    produces a prerelease with its artifacts attached, and that /releases/latest is
    unchanged.
  3. Only then cut a real release with prerelease: none.

Note on pinning

Workflows are referenced at @main rather than a commit SHA, because the SHA does not exist
until #66 is squash-merged. Re-pinning to that SHA is a follow-up.

Repo-specific

  • release_prod.yml and release_test.yml are replaced. The GHCR image job is unchanged in substance — it still builds from the PyPI release — but takes the version from the flow rather than github.event.release.tag_name, so it works for a dispatched release too.
  • build.yml gains ref and artifact-name, and its checkout fetches tags.
  • release_test.ymlpublish-dev-to-testpypi.yml, now calling the shared workflow. Behaviour unchanged.

Two things deliberately not carried over

Was Why not
sign-artifacts: true on pypa/gh-action-pypi-publish That input does not exist. It was silently ignored. attestations — presumably what was meant — defaults to true, and the shared workflow sets it explicitly.
skip-existing: true on the real index A re-run now fails on a version collision instead of silently succeeding. A collision on PyPI means something is wrong and should be seen. Test PyPI keeps skip-existing, where re-running after a downstream failure is routine.

The old arrangement created a draft, a human clicked Publish, and only then did
the publish workflows run -- so everything that verified a release ran after it
was already public. The release is now created as a prerelease and promoted only
once everything that can fail has succeeded; the confirmation step is an
environment approval on the job that tags, reached after lint and build are
already green.

The version no longer has to be typed: it is auto-detected from Conventional
Commits, and passing one that disagrees needs `force`. `prerelease: rc` cuts a
release candidate, verified exactly like a release but never promoted to latest.

See RELEASING.md in reqstool/.github for the whole flow.

release_prod.yml and release_test.yml are replaced. The GHCR image job is
unchanged in substance -- it still builds from the PyPI release -- but now takes
the version from the flow rather than from github.event.release.tag_name, so it
works for a dispatched release too.

Two things the old workflow did are deliberately not carried over:

- `sign-artifacts: true` was passed to pypa/gh-action-pypi-publish, which has no
  such input. It was silently ignored; `attestations` (what it presumably meant)
  defaults to true and the shared workflow sets it explicitly.
- `skip-existing: true` on the *real* index. A re-run now fails on a version
  collision instead of silently succeeding, which is the point -- a collision
  means something is wrong. Test PyPI keeps skip-existing, where a re-run after a
  downstream failure is routine.

build.yml gains `ref` so the flow can re-run it against the tag it just created
-- that build is what gives the artifacts their version, since hatch-vcs reads it
from git -- and `artifact-name`, because the flow builds twice in one run and
upload-artifact rejects a duplicate name.

release_test.yml is renamed publish-dev-to-testpypi.yml and calls the shared
workflow. Behaviour is unchanged.

Signed-off-by: Jimisola Laursen <jimisola@jimisola.com>
check-semantic-pr.yml referenced
`reqstool/.github/.github/workflows/check-semantic-pr.yml@main`, but that file
was renamed to common-check-semantic-pr.yml. GitHub cannot resolve a
workflow_call to a path that does not exist, so the run fails at startup with no
jobs -- and because the failure produces no check run, it never appeared in the
PR checks list.

PR titles have therefore not been validated here since the rename.

Signed-off-by: Jimisola Laursen <jimisola@jimisola.com>
common-release-tag.yml now validates the version and the ref itself rather than
trusting that prepare validated the same values (CodeQL flagged the privileged
checkout on an unvalidated ref in reqstool/.github#66). Validating the version
needs to know which format to validate against.

Signed-off-by: Jimisola Laursen <jimisola@jimisola.com>
@jimisola
jimisola merged commit 83617e3 into main Aug 16, 2026
7 checks passed
@jimisola
jimisola deleted the ci/adopt-verified-release-flow branch August 16, 2026 16:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant