Skip to content

ci: create a GitHub Release per version tag - #40

Merged
carolsimone merged 4 commits into
mainfrom
release-workflow
Aug 21, 2026
Merged

ci: create a GitHub Release per version tag#40
carolsimone merged 4 commits into
mainfrom
release-workflow

Conversation

@carolsimone

Copy link
Copy Markdown
Owner

What this changes

Adds .github/workflows/release.yml, adapted from continuo's workflow of the same name, so a version tag produces an actual GitHub Release.

Why

publish-pypi.yml and images.yml already do the real work of a release (PyPI trusted publishing; building + smoke-testing + pushing both engine images to ghcr.io), each independently on their own push: tags: v* trigger. Neither of them creates a GitHub Release, and PyPI's release list only ever knows about the two packages — not the two ghcr.io images a tag also ships. A Release is the one place to say "this tag = these packages + these images."

What was stripped from continuo's version, and why

continuo's release.yml is a 5-job pipeline (retag-images, install-test, publish-chart, smoke-published-chart, github-release) built around things this repo does not have: a Helm chart, a Kubernetes deploy directory, a self-hosted runner, a kind cluster, and a separate deploy.yml that builds images once for retagging later. None of that applies here — images.yml builds and pushes the images directly on the tag, it isn't retagging a pre-existing sha-tagged image. So this PR keeps only the equivalent of continuo's github-release job.

The one real design problem this repo's version has to solve that continuo's didn't

continuo's github-release job uses needs: on jobs in the same workflow file. Here, the two things this job needs to wait for — publish-pypi.yml and images.yml succeeding — are separate workflow files triggered independently by the same tag push, and needs: can't cross files. I considered workflow_run (fire when either sibling completes, check if the other already finished too) but rejected it: it depends on workflow_run.head_branch correctly carrying the tag name for a tag-triggered source run, which nothing else in this repo relies on, so I'd be introducing an assumption I can't verify.

Instead, release.yml triggers on the same push: tags: v[0-9]* as its siblings and polls the Actions API (gh api repos/.../actions/workflows/{file}/runs -f head_sha=... -f event=push) for each sibling's conclusion at this exact commit, up to a 30-minute timeout per sibling. event=push specifically avoids picking up one of images.yml's pull_request-triggered runs by accident. Any non-success conclusion (or a timeout) fails the job outright rather than silently skipping the Release.

Other details:

  • -test tags (e.g. v0.3.1-test1) are excluded — those are publish-pypi.yml's TestPyPI dry run, and images.yml's publish job already skips itself for them by design.
  • Release creation is idempotent (gh release viewedit or create), matching continuo's pattern, in case this ever re-runs against an existing tag.
  • No CHANGELOG.md exists anywhere in this repo (unlike continuo's deploy/continuo/CHANGELOG.md), so there's no changelog-extraction step to port — notes are always --generate-notes.

How it was verified

  • YAML validated: python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml'))".
  • Not verified: the actual cross-workflow wait/poll behavior. That needs a real v* tag push to exercise — a PR can't trigger it, and there's no local way to simulate publish-pypi.yml/images.yml completing. The next tag push (v0.3.1) will be the first real test; worth watching that run closely.

Checklist

  • Every commit is signed off (git commit -s)
  • Tests added or updated for the behavior changed — no test harness exists for GitHub Actions workflow files in this repo; verification is YAML validation plus the reasoning above
  • scripts/security-scan.sh not relevant (no dependency or credential changes)

Mirrors continuo's release.yml, stripped to what actually applies here: no
Helm chart, no Kubernetes deploy dir, no self-hosted runner or kind cluster
in this repo, so none of retag-images/install-test/publish-chart/
smoke-published-chart exist. publish-pypi.yml and images.yml already do the
real publishing work independently, each on their own push:tags trigger —
this workflow's only job is to create the GitHub Release once both have
succeeded for the tagged commit, since a tag also ships two ghcr.io images
that PyPI's own release list knows nothing about.

Triggered by the same tag push as its two siblings rather than by
workflow_run watching them complete, since `needs:` cannot cross workflow
files and this avoids relying on workflow_run's head_branch semantics for a
tag-triggered source run. Instead it polls the Actions API for the sibling
runs at this commit (event=push, to not pick up one of images.yml's
pull_request runs) until both report a conclusion, exits non-zero on any
non-success conclusion, and times out after 30 minutes per sibling. -test
tags are excluded (TestPyPI dry run, no public Release). Release creation
itself is idempotent (create-or-edit), matching continuo's approach; no
CHANGELOG.md exists in this repo so notes are always --generate-notes.

Verified: YAML validates (python3 -c "import yaml; yaml.safe_load(...)").
NOT verified: the actual cross-workflow wait/poll behavior, which needs a
real tag push to exercise — there is no way to integration-test this
locally or in a PR.

Signed-off-by: Simone Carolini <simonecarolini.sc@gmail.com>
-f/-F on `gh api` switches the request to POST unless overridden — this
listing endpoint only accepts GET, so the poll would 404 on its very first
call under `set -e` and abort the job every time, regardless of whether
publish-pypi.yml/images.yml actually succeeded. Verified against this repo's
real API: the -f form 404s, the URL-query-string form returns real run data
(tested against main's actual security.yml/ci.yml runs, both "success").

Also raised the job's timeout-minutes from 40 to 70: the two sequential
wait_for calls can themselves take up to 30 minutes each — 60 minutes worst
case on the polling math alone, before the siblings' own run time — so 40
could never actually cover it.

Signed-off-by: Simone Carolini <simonecarolini.sc@gmail.com>
Backfilled from actual tag history (0.1.0 through 0.3.0, plus what's pending
for 0.3.1), Keep a Changelog style. release.yml now extracts the section
matching the pushed tag's version and uses it as the Release body, falling
back to GitHub's generated notes if a tag has no matching section, instead of
always using generated notes. Verified the extraction awk script against the
real file for 0.3.1/0.3.0/0.2.1 (all extract correctly) and a nonexistent
version (correctly falls through to the no-match branch).

CONTRIBUTING.md now says where release notes come from, so the file doesn't
silently go stale.

Signed-off-by: Simone Carolini <simonecarolini.sc@gmail.com>
ci failed: CHANGELOG.md's 0.3.0 entry names continuo-validation-contract, the
package that release actually replaced — the same reason docs/superpowers/'s
dated design records are already exempt from this sweep (test_no_legacy_names.py's
own words: rewriting them "would falsify the design history"). Extended the
existing EXEMPT_PREFIXES mechanism rather than reword the changelog entry to
dance around the name of what it's describing.

Signed-off-by: Simone Carolini <simonecarolini.sc@gmail.com>
@carolsimone carolsimone self-assigned this Aug 21, 2026
@carolsimone
carolsimone merged commit ef9514a into main Aug 21, 2026
6 checks passed
@carolsimone
carolsimone deleted the release-workflow branch August 25, 2026 14:25
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