This extension makes use of pgxntool (https://github.com/Postgres-Extensions/pgxntool);
the release machinery (make tag, make dist) lives in pgxntool/base.mk. These
steps cut a new release.
Two version numbers matter here and they can differ: the distribution version
(META.in.json's top-level version, feeds PGXNVERSION, what PGXN.org lists a
release under) and the extension version (count_nulls.control's
default_version, what CREATE EXTENSION count_nulls; installs by default and
what pg_extension.extversion reports). They're usually bumped together, but
count_nulls has already shipped a release where they weren't: 0.9.7 on PGXN
(2017-01-26) was a distribution-only bump (packaging/CI fixes, no SQL changes) —
the extension version stayed at 0.9.6, unchanged since 2016. 0.9.7 the
extension version has never existed.
Before anything else, confirm every committed versioned install script still
matches what that version actually shipped — this is the check that the
stable-vs-real-version dance (step 4/7 below) exists to make routine, but
it's worth a direct look before relying on it.
- For each versioned install script, find its last-touching commit:
git log -1 --format='%H %ad' -- sql/<extension>--<version>.sql. - Confirm that commit is no later than when that EXTENSION version actually
shipped.
git tagis the authoritative source for this once the project is on a tag-based release history. If any releases predate that (tracked some other way, e.g. release branches, or not tracked at all), fall back to cross-checking the extension's PGXN.org listing, remembering that it lists distribution versions, which can lag the extension version they contain (see the0.9.7example above). - A version file touched by a commit LATER than its own release is a red
flag — it likely means
default_versionwas left pointing at a real (non-stable) version and a later source edit 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 version's real release can also mean the file was legitimately backfilled after the fact (e.g. a newer pgxntool version started requiring committed version files that weren't tracked before). A late add-date alone isn't suspicious — only worry about a file whose content looks like it might differ from what actually shipped.
- Open issues/PRs for this release reviewed, merged or deferred.
- CI green on all supported PostgreSQL versions.
- Locally:
make verify-resultspasses. It depends ontest(so it runs the suite first, then gates on the results).make testalone is non-gating — pgxntool marksinstallcheck.IGNORE, so it never returns non-zero on a regression; onlyverify-results(which inspectstest/regression.diffs) is a real gate.
- Pick the new version (semantic versioning). Decide whether the extension
version needs to move at all, or (per the
0.9.7example above) only the distribution version does, if this release has no SQL changes. - Default to committing every versioned install script. For a small
extension (a handful of tracked versions, a source file measured in
dozens of lines rather than thousands), the storage cost of keeping
every version's file is negligible — there's little reason to skip it
purely to save space. The update-test-coverage value (being able to
install any prior version and
ALTER EXTENSION UPDATEfrom it) is the same regardless of size; only skip committing a version's install script for a truly trivial change where you've already decided that coverage isn't worth even the small cost. Update scripts (following thesql/<extension>--<prev>--<version>.sqlnaming) are ALWAYS committed regardless — they're the only thing that makes the update path testable at all.
⚠️ CRITICAL — you are temporarily leaving thestablepseudo-version. Master'sdefault_version(in the extension's.controlfile) normally sits at the literal string'stable', so that ordinary source edits regenerate the current install script (via the existing rule incontrol.mk, driven by whateverdefault_versionsays) and never touch a frozen, already-shipped version's file. Stamping a real version number here points that same generation rule at the real version's install script instead. The moment this release is merged you MUST flipdefault_versionback tostable(step 7) if the extension version moved. 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 the.controlfile (bumped by hand). If only the distribution version is moving (no SQL changes — see step 3), leavedefault_versionalone. - Bump the version in
META.in.json— the source of truth is the top-levelversion(the distribution version; always bump this) and the extension's own entry underprovides(the extension version; only bump if it's actually moving, per step 3).META.json,control.mk, andmeta.mk(which feedsPGXNVERSION) regenerate viamake. - Advance
release_statusinMETA.in.jsonas appropriate (unstable → testing → stable). - If the extension version moved: add the update script from the previous
version to the new one; confirm the
ALTER EXTENSION UPDATEpath actually reaches the new version from the previous one, on multiple PG majors. - Stamp
HISTORY.md: the topstablesection accumulates user-facing changes as PRs land; at release, rename that header to the new (distribution) version number.
-
make verify-resultsgreen (it runstestfirst, then gates on the results). - From a clean checkout (or
git archiveof the tag):make && make installregenerates and installs cleanly, and creating the extension reports the expected version — confirms a PGXN consumer can build from the tracked sources alone. (This mirrors whatmake distships, since it archives the tag: committed files only.)
- Commit the release changes; working tree must be clean —
make tagaborts with "Untracked changes!" on a dirty tree. -
make tag— creates a git tag named exactly the DISTRIBUTION version, UNPREFIXED (e.g.1.0.0, novprefix), taken fromPGXNVERSION, and pushes it toorigin. Make sureoriginin your checkout actually points at the real upstream repo, not a personal fork. If this project has used a different release-tracking scheme before (release branches, no tracking at all, etc.),make tagis the sole mechanism going forward once migrated. It's idempotent when the tag already points at HEAD, and errors if the tag exists on a different commit. To move an existing tag usemake forcetag(=make rmtagthenmake tag);make rmtagdeletes the tag locally and onorigin. -
make dist— depends ontag(and builds the HTML docs), thengit archives the tag into a distribution zip in the parent directory. Because it archives the tag, only committed files are included. If a.gitattributesexists it must be committed, ordistaborts (git archive only honorsexport-ignorefor committed files).make forcedist=forcetag+dist. - Upload the resulting zip to PGXN (manual).
- As soon as the release is merged, flip
default_versionback tostablein the.controlfile, open a new topstablesection inHISTORY.md, and re-seed a fresh update script from this release tostable(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 released version's install file. - Keep this PR's description small — something like "Reset version back to
stableafter release." is enough. It's a mechanical, low-risk step; it doesn't need the detailed rationale a real content change would.