The shared release process for Postgres-Extensions repos that distribute a
PGXN extension and build on pgxntool (make tag/make dist,
META.in.json, <extension>.control). Written to be followed standalone.
Does not apply to: non-PGXN tooling repos (linter, this ai repo
itself — neither has a version/release concept at all), or to
pgxntool-test, whose "release" is a different, paired mechanism tied to
pgxntool's own release (see pgxntool-test/.claude/skills/release/SKILL.md
if you land there expecting this process — it doesn't apply).
Only keep a local RELEASE.md in a consuming repo if there's a
legitimate, repo-specific need for one — a real gotcha discovered the hard
way, a genuine historical exception, a deviation from the steps below that
actually affects how that repo is released. A repo with nothing like that
doesn't need a RELEASE.md at all; don't add one just for symmetry with
other repos, and don't copy these steps into a local file "for
completeness" — copies drift (see "Notes / gotchas" at the bottom — several
were found independently, in the same words, in more than one repo's local
copy before this doc existed). If a local file does exist for a genuine
reason, keep it to just that reason and link here for everything else.
Releases use pgxntool's built-in make tag/make dist to produce a .zip,
which is then uploaded to PGXN Manager by hand — there is currently no
CI automation for publishing anywhere in the org. See "Future: CI
automation" below.
Keep the next release ready to cut at any time, so cutting one is just renaming things (step 4 below), not writing a changelog or an update path from scratch under time pressure:
- If a PR makes a user-facing change (bug fix, behavior change, new/changed
function — CI config, contributor-only docs, and other internal-only
changes don't count), add an entry to the repo's changelog file
(
HISTORY.ascorHISTORY.md, depending on the repo) at the repo root:- If the top section is already headed
STABLE/stable, add your entry to it. - If the top section is a real version number (nothing has changed since
the last release yet), insert a new
STABLE/stablesection above it with your entry.
- If the top section is already headed
- If a PR changes an extension's SQL, also maintain the update script from
the last released version to
stable:sql/<ext>--<last-released-version>--stable.sql[.in]. Create it if it doesn't exist yet (first SQL-touching PR since the last release). Every subsequent SQL-touching PR adds whateverALTER .../CREATE OR REPLACE ...statements are needed to bring an install on the last released version up to your changes.
This is the flip side of the stable pseudo-version described in
CLAUDE.md's "Version-specific SQL files" — that section covers why it's
gitignored; this is the workflow that keeps it current.
Before anything else, confirm every committed versioned install script still matches what that version actually shipped:
- For each committed versioned install script, find its last-touching
commit:
git log -1 --format='%H %ad' -- sql/<ext>--<version>.sql[.in]. - Confirm that commit is no later than when that version was actually
tagged/released. Once a repo is on tag-based release history, compare
directly against the tag (
git log -1 --format='%H %ad' <version>). If any releases predate tagging (tracked some other way, or not tracked at all), fall back to the extension's PGXN.org listing — remembering that it lists the distribution version, which can lag or differ from the extension version if this repo distinguishes the two (see step 3). - A version file touched by a commit later than its own release is
a red flag — it likely means
default_versionwas left pointing at that real (non-stable) version after release, and a later source edit on master silently regenerated (corrupted) it. Investigate before proceeding. - Known exception, not necessarily a corruption: a version file whose last-touching commit is much later than its real release can also mean it was legitimately backfilled or reformatted after the fact (e.g. a newer pgxntool version started requiring something that wasn't tracked before). A late touch-date alone isn't suspicious — only worry about a file whose content looks like it might differ from what actually shipped.
- Open issues/PRs intended for this release reviewed, merged, or explicitly deferred.
- CI green on all supported PostgreSQL versions.
- Locally:
make verify-resultspasses. Depending on the pgxntool version this repo vendors, plainmake test/make installcheckmay not be a reliable gate on its own — older pgxntool marksinstallcheck.IGNORE, so it can report success even when everypg_regresstest failed.verify-results(which inspectstest/regression.diffsdirectly) is the gate that's actually trustworthy regardless of vendored version; when in doubt, also eyeball the rawpg_regressoutput rather than trusting a green checkmark alone. - No CI dependency-override toggle is currently set. If this repo's
CI has a toggle to build a dependency from a git ref instead of its
published PGXN version (typically added because the published
version doesn't yet satisfy what this repo actually needs), it must
be unset before you proceed. Check the toggle's live value in CI
config (e.g.
ci.yml'senv:), not just whether the Makefile supports the override — a normally-empty, always-present opt-in variable existing in the Makefile doesn't mean anything is currently pinned. Releasing while it's set produces a real, publishable zip that declares a dependency floor nothing on PGXN can actually satisfy — anyone who installs it gets a build failure, not the tested code. If it's set, stop: wait for the real dependency version to land on PGXN and the override to be reverted before cutting this release.
- Pick the new version (semantic versioning, unprefixed — e.g.
1.0.0, neverv1.0.0). - Some repos track two version numbers that can differ: the
distribution version (
META.in.json's top-levelversion, what PGXN.org lists a release under) and the extension version (<ext>.control'sdefault_version, whatCREATE EXTENSIONinstalls andpg_extension.extversionreports). If this repo makes that distinction, decide here whether the extension version needs to move at all, or only the distribution version does (e.g. a packaging/CI-only fix with no SQL changes). - Decide whether to commit the generated versioned install script for
this release. Default to committing it — for a small extension the
storage cost is negligible, and it's the only thing that makes "can I
install any prior version and
ALTER EXTENSION UPDATEfrom it" testable. Only skip it for a repo that's deliberately chosen not to track these (check this repo's own notes) or for a truly trivial change where that coverage isn't worth even the small cost. The update script (sql/<ext>--<prev>--<version>.sql[.in]) is always committed regardless — without it there is no testable update path at all.
⚠️ CRITICAL — you are temporarily leaving thestablepseudo-version. Master'sdefault_versionnormally sits at the literal stringstable, so ordinary source edits regenerate the current install script and never touch a frozen, already-shipped version's file. Stamping a real version here points that same generation rule at the real version's file instead. The moment this release is merged you must flipdefault_versionback tostable(step 7). If you forget, the next source edit on master will regenerate — and corrupt — the just-released version's install file.
- If the extension version is moving, bump
default_versionin<ext>.controlby hand. If this repo distinguishes distribution vs. extension version and only the distribution version is moving, leavedefault_versionalone. - Bump the version in
META.in.json— the top-levelversion(always, the distribution version) and the extension's entry underprovides(only if it's moving). Do not touchmeta-spec.version— that's the PGXN metadata spec version, always1.0.0regardless of this distribution's version.META.json,control.mk, andmeta.mk(which feedsPGXNVERSION) regenerate viamake— never hand-editMETA.jsondirectly. - Advance
release_statusinMETA.in.jsonas appropriate (unstable→testing→stable). - If the extension version moved: finish the update script from the
previous version to the new one; confirm
ALTER EXTENSION ... UPDATEactually reaches the new version from the previous one, on multiple PostgreSQL majors. - Rename the changelog's top
STABLE/stableheading to the new (distribution) version number, matching its dashes/underline length if the format uses one. -
git mvthe--stableupdate script tosql/<ext>--<last-released-version>--<new-version>.sql[.in]. - Commit the version bump + changelog + renamed update script together in one commit.
-
make verify-resultsgreen (runs the suite, then gates on the results). - From a clean checkout (or
git archiveof the tag once cut):make && make installregenerates and installs cleanly, and creating the extension reports the expected version — confirms a PGXN consumer can build from tracked sources alone. This mirrors whatmake distactually ships, since it archives the tag: committed files only.
- Working tree must be clean —
make tagaborts with "Untracked changes!" on a dirty tree. - Make sure
originin your checkout points at the canonical upstream repo (Postgres-Extensions/<repo>), not a personal fork —make tag/make distpush tooriginby default, and a tag pushed to a fork does nothing for PGXN. If you work from a fork, passPGXN_REMOTE=<remote-name>to every target below instead (added in pgxntool 2.2.0 after exactly this mistake shipped a tag to a fork on a real release — checkgit remote -vrather than assuming the canonical remote is namedupstream). -
make tag— creates a git tag named exactly the (distribution) version, unprefixed (e.g.1.0.0, neverv1.0.0), taken fromPGXNVERSION, and pushes it toorigin(or$(PGXN_REMOTE)). Idempotent if the tag already points at HEAD; errors if it exists on a different commit. To move an existing tag:make forcetag(=make rmtag+make tag). Don't do this once the version has been public for a while — moving a published tag out from under people is disruptive.make rmtagdeletes the tag locally and on the remote. -
make dist— depends ontag(and builds HTML docs), thengit archives the tag into../<dist-name>-<version>.zipin the parent directory. Because it archives the tag, only committed files are included — if.gitattributesexists it must be committed, ordistaborts (git archiveonly honorsexport-ignorefor committed files).make forcedist=forcetag+dist.
- Upload the resulting zip at https://manager.pgxn.org/ (log in with an account that has release rights to this distribution).
- Verify the new version shows up at
https://pgxn.org/dist/<name>/(indexing can take a few minutes).
- As soon as the release is merged, flip
default_versionback tostablein<ext>.control, open a fresh topSTABLE/stablesection in the changelog, and re-seed asql/<ext>--<this-release>--stable.sql[.in]update script (content-identical to the source at this point — it exists purely so the update path tostableis always available) for the next cycle. Leaving master stamped at the real version means the next source edit regenerates and corrupts the just-released install file. - Keep this PR's description small — "Reset version back to
stableafter release." is enough; it's a mechanical, low-risk step that doesn't need the rationale a real content change would.
Right now this is entirely manual across the org. The pgtap extension (a
sibling project, not part of Postgres-Extensions) has a
.github/workflows/release.yml that auto-publishes to PGXN on tag push
using the pgxn/pgxn-tools Docker image, and auto-creates a GitHub release
from the changelog. Adopting the same pattern here would remove step 7
above, but requires storing PGXN Manager credentials as a secret in every
repo that adopts it — not yet done anywhere in the org.
make tag/make distcreate a real git tag, despite olderpgxntool/README.ascwording describing the result as a "branch" — check the vendored version's actual behavior if a repo's docs still say "branch."- CI passing is not proof the test suite passed on every vendored
pgxntool version — see step 2. This has bitten real PRs (every
pg_regresstest failing while CI reported success). - A CI dependency-override toggle (build a dependency from a git ref instead of its published PGXN version) must be unset before cutting a release — see step 2 above.