Skip to content

ci: auto-create GitHub Release after each publish#99

Merged
mgoldsborough merged 2 commits intomainfrom
chore/auto-create-release-on-publish
May 7, 2026
Merged

ci: auto-create GitHub Release after each publish#99
mgoldsborough merged 2 commits intomainfrom
chore/auto-create-release-on-publish

Conversation

@mgoldsborough
Copy link
Copy Markdown
Contributor

Summary

After PR #94 shipped without a corresponding GitHub Release page (because the project has historically been tag-only), we created the three releases for this round (schemas-v0.3.0, sdk-typescript-v0.7.0, cli-v0.4.2) by hand. This PR removes the manual step going forward.

Each *-publish.yml workflow gets a new release job that runs after the npm/PyPI/GHCR publish completes. It runs gh release create --generate-notes --verify-tag, which builds the release body from PR titles merged since the previous tag.

Why

  • Discoverability: GitHub renders Releases in the repo sidebar; tags are buried two clicks deep
  • Watcher notifications: users who "Watch → Releases only" get pinged on each release
  • External linkability: blog posts / Discord can link to a release URL with a curated narrative
  • Standard expectation for an OSS registry product

Design choices

  • Separate release job rather than an extra step in publish. Keeps the publish job's permissions narrow (id-token: write only) and the release job's permissions narrow (contents: write only). Also makes the release re-runnable from the GH UI if it fails independently of publish.
  • Scanner waits on publish-docker, which transitively waits on publish-pypi, so PyPI + GHCR + GitHub Release are all aligned.
  • Tag prefix stripping: each workflow uses its own tag prefix (schemas-v, sdk-typescript-v, cli-v, sdk-python-v, scanner-v) to derive the version for the release title.
  • No backfill: existing historical tags stay tag-only. The three releases for this round were created by hand; from the next tag forward, CI handles it.

Test plan

  • Workflow YAML parses (verified locally — all 5 load cleanly)
  • On the next tag push (per-package), CI creates a Release with a sensible auto-generated body. Each workflow can be tested in isolation by tagging a patch bump.

Files changed

  • .github/workflows/cli-publish.yml
  • .github/workflows/schemas-publish.yml
  • .github/workflows/sdk-typescript-publish.yml
  • .github/workflows/sdk-python-publish.yml
  • .github/workflows/scanner-publish.yml

Adds a `release` job to all five publish workflows (cli, schemas,
sdk-typescript, sdk-python, scanner). After a successful publish
to npm / PyPI / GHCR, the job runs `gh release create --generate-notes
--verify-tag` to create a GitHub Release whose body is auto-built
from PR titles since the previous tag.

Why: tag-only releases hide the changelog from users browsing the
repo and don't trigger watcher notifications. This brings the surface
in line with what users expect from an OSS registry product.

Implementation notes:
- Separate `release` job (vs. an extra step in `publish`) keeps
  permissions narrow: just `contents: write`, no `id-token`.
- The scanner release waits on `publish-docker`, which transitively
  waits on `publish-pypi`, so PyPI + GHCR + GitHub Release are all
  consistent.
- Backfill of historical tags is intentionally not done — the
  three releases for this round (schemas-v0.3.0, sdk-typescript-v0.7.0,
  cli-v0.4.2) were created manually; from the next tag forward, CI
  handles it.
Addresses QA feedback on PR #99:

W1 — Without --notes-start-tag, GitHub auto-detects the "previous tag"
by head-SHA date, not by tag prefix. In a monorepo where multiple
packages tag on main, the next schemas-v0.3.1 release would diff
against (e.g.) cli-v0.4.2 and the body would include unrelated
CLI/SDK commits. Fix: derive the previous same-prefix tag explicitly
via `git tag -l '<prefix>-v*' --sort=-v:refname` and pass via
--notes-start-tag. Falls back to gh's auto-detection when there is
no prior same-prefix tag (first release of a new package).

Requires fetch-depth: 0 on the checkout step — actions/checkout@v4
defaults to a shallow clone with no tags, which would make `git tag -l`
return nothing.

S1 — Idempotency: wrap `gh release create` in a `gh release view`
guard so re-runs (e.g. after a transient post-publish failure) skip
instead of erroring with "release already exists."

S3 — Workflow-level concurrency group serializes runs of the same
workflow on the same ref, closing a theoretical race if a tag is
pushed twice in rapid succession. cancel-in-progress: false because
we never want to abort a publish in flight.

Declined S2 (Python SDK title style): the parenthetical "(Python SDK)"
in `mpak (Python SDK) v$VERSION` does real disambiguation against
the npm CLI which is also named `mpak`. Symmetry with the four
@nimblebrain/-prefixed titles is less important than clarity at the
point of consumption.
@mgoldsborough
Copy link
Copy Markdown
Contributor Author

Addressed QA feedback in 96f1083:

W1 — same-prefix notes-start-tag ✅ Applied as suggested. Each workflow's release job now derives PREV_TAG from git tag -l '<prefix>-v*' --sort=-v:refname, requires fetch-depth: 0 on the checkout, and falls back to gh's auto-detection only when there's no prior same-prefix tag (first release of a new package).

S1 — idempotency ✅ Wrapped gh release create in a gh release view guard. Re-runs after a transient failure now skip the create step instead of erroring.

S3 — concurrency group ✅ Workflow-level concurrency: { group: ${{ github.workflow }}-${{ github.ref }}, cancel-in-progress: false } so two near-simultaneous tag pushes serialize.

S2 — Python SDK title ❌ Declined. The PyPI dist is named mpak, which collides with the npm CLI also named mpak (@nimblebrain/mpak). The parenthetical (Python SDK) does real disambiguation work for users browsing releases. Renaming to mpak-py would be non-canonical (not the actual dist name); mpak (PyPI) is no clearer than what we have. Symmetry with the four other titles is worth less than clarity at the point of consumption. Open to overriding if you'd rather take the symmetry win.

Edge cases I checked on the W1 fix:

  • cli-v0.4.10 vs cli-v0.4.2--sort=-v:refname is git's version-sort, handles correctly
  • Pre-release tags (-beta.N) — handled by -v:refname
  • First release of a new prefix — PREV_TAG is empty, ${PREV_TAG:+...} guard omits the flag, gh falls back to its date-based auto-detection (which for a first-of-prefix is fine)
  • Tag matching <prefix>-v* from a non-version tag (e.g. hypothetical cli-vfoo) — would match. Non-issue in practice; we don't use such tags

@mgoldsborough mgoldsborough merged commit 1deb4bc into main May 7, 2026
2 checks passed
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