Release Process
This document describes how to create a release of pg_trickle.
Overview
Releases are fully automated via GitHub Actions. Pushing a version tag (v*)
triggers the Release workflow, which:
- Runs a preflight version-sync check to ensure all version references match the tag
- Builds extension packages for Linux (amd64), macOS (arm64), and Windows (amd64)
- Smoke-tests the Linux artifact against a live PostgreSQL 18 instance
- Creates a GitHub Release with archives and SHA256 checksums
- Builds and pushes a multi-arch extension image to GHCR (for CNPG Image Volumes)
A separate PGXN workflow also fires on the same
v* tag and publishes the source archive to the PostgreSQL Extension Network.
Prerequisites
- Push access to the repository (or a PR merged by a maintainer)
- All CI checks passing on
main(verify the last run on the version-bump commit succeeded) - The version in
Cargo.tomlmatches the tag you intend to push - Required GitHub secrets configured (see Required GitHub Secrets below)
Required GitHub Secrets
The release automation uses the following GitHub Actions secrets. Set them under Settings → Secrets and variables → Actions → New repository secret.
| Secret | Used by | Description |
|---|---|---|
PGXN_USERNAME | pgxn.yml | Your PGXN account username. Used to authenticate the curl upload to PGXN Manager when publishing source archives to the PostgreSQL Extension Network. Register at pgxn.org. |
PGXN_PASSWORD | pgxn.yml | Password for the PGXN account above. Never hardcode this — it must be stored as a secret so it is never exposed in logs or committed to the repository. |
CODECOV_TOKEN | coverage.yml | Upload token for Codecov. Used to publish unit and E2E coverage reports. Obtain it from the Codecov dashboard after linking the repository. The workflow degrades gracefully (fail_ci_if_error: false) if absent. |
BENCHER_API_TOKEN | benchmarks.yml | API token for Bencher, the continuous benchmarking platform. Used to track Criterion benchmark results on main and detect regressions on pull requests. The benchmark steps are skipped entirely when this secret is absent, so CI still passes without it. Create a project at bencher.dev and copy the token from the project settings. |
Note: The
GITHUB_TOKENsecret is provided automatically by GitHub Actions and does not need to be configured manually. It is used by the release workflow to create GitHub Releases, by the Docker workflow to push images to GHCR, and by Bencher to post PR comments.
Step-by-Step
1. Decide the version number
Follow Semantic Versioning:
| Change type | Bump | Example |
|---|---|---|
| Breaking SQL API or config change | Major | 1.0.0 → 2.0.0 |
| New feature, backward-compatible | Minor | 0.1.0 → 0.2.0 |
| Bug fix, no API change | Patch | 0.2.0 → 0.2.1 |
| Pre-release / release candidate | Suffix | 0.3.0-rc.1 |
2. Update the version
Four files must have their version bumped together:
# 1. Cargo.toml — the canonical version source for the extension
# Change: version = "0.7.0" → version = "0.8.0"
# 2. pgtrickle-tui/Cargo.toml — the TUI binary; must always match Cargo.toml
# Change: version = "0.7.0" → version = "0.8.0"
# 3. META.json — the PGXN package metadata
# Change both top-level "version" and the nested "provides" version
# 4. CHANGELOG.md
# Rename ## [Unreleased] → ## [0.8.0] — YYYY-MM-DD
# Add a new empty ## [Unreleased] section at the top
Important:
Cargo.toml(extension) andpgtrickle-tui/Cargo.toml(TUI) must always carry the same version. They are built and released together, and a mismatch causescargo install --path pgtrickle-tuito report the wrong version. Thejust check-version-syncscript does not currently enforce this, so it must be checked manually.
The extension control file (pg_trickle.control) uses
default_version = '@CARGO_VERSION@', which pgrx substitutes automatically at
build time — no manual edit needed there.
After editing, verify all version-related files are in sync:
just check-version-sync
3. Commit the version bump
git add Cargo.toml META.json CHANGELOG.md
git commit -m "release: v0.8.0"
git push origin main
4. Wait for CI to pass and verify upgrade completeness
Ensure the CI workflow passes on main with
the version bump commit. All unit, integration, E2E, and pgrx tests must be
green.
Critical: Before tagging, verify that the upgrade script covers all SQL schema changes:
# Run comprehensive upgrade completeness checks
just check-upgrade-all
# If any check fails (e.g. "ERROR: X new function(s) missing from upgrade script"),
# fix the issue by adding the missing SQL objects to:
# sql/pg_trickle--<prev>--<new>.sql
#
# Then re-run until all checks pass:
just check-upgrade-all # Should print "All 15 upgrade step(s) passed completeness checks."
Why this matters: New SQL functions, views, tables, and columns added in any prior
release must be carried forward in the upgrade script, even if the current release
doesn't change them. The upgrade script is the source of truth for what PostgreSQL
applies when users run ALTER EXTENSION pg_trickle UPDATE.
Confirm the local and CI upgrade-E2E defaults were advanced to the new release:
just check-version-sync # Verifies ci.yml, justfile, and test defaults
5. Create and push the tag
git tag -a v0.2.0 -m "Release v0.2.0"
git push origin v0.2.0
This triggers the Release workflow automatically.
6. Monitor the release
Watch the Actions tab for progress. The release workflow runs these jobs in order:
preflight ──► build-release (linux, macos, windows)
│
▼
test-release ──► publish-release
──► publish-docker-arch (linux/amd64 + linux/arm64)
│
▼
publish-docker (merge manifest + push :latest)
The PGXN workflow (pgxn.yml) runs independently and publishes the source
archive to pgxn.org in parallel with the release workflow.
7. Make the GHCR package public (first release only)
When a package is pushed to GHCR for the first time it is private by default. Because this is an open-source project, packages linked to the public repository inherit public visibility — but you must make the package public once to unlock that:
- Go to github.com/⟨owner⟩ → Packages → pg_trickle-ext
- Click Package settings
- Scroll to Danger Zone → Change package visibility → set to Public
After that first change:
- All future pushes keep the package public automatically
- Unauthenticated
docker pull ghcr.io/grove/pg_trickle-ext:...works - Storage and bandwidth are free (GHCR open-source advantage)
- The package page shows the README, linked repository, license, and description from the OCI labels
8. Verify the release
Once both workflows complete:
- Check the GitHub Releases page for the new release
-
Verify all three platform archives are attached (
.tar.gzfor Linux/macOS,.zipfor Windows) -
Verify
SHA256SUMS.txtis present -
Verify the extension image is available at
ghcr.io/grove/pg_trickle-ext:<version> -
Verify the PGXN upload succeeded:
pgxn info pg_trickleshould show the new version - Optionally verify the extension image layout:
docker pull ghcr.io/grove/pg_trickle-ext:<version>
ID=$(docker create ghcr.io/grove/pg_trickle-ext:<version>)
docker cp "$ID:/lib/" /tmp/ext-lib/
docker cp "$ID:/share/" /tmp/ext-share/
docker rm "$ID"
ls -la /tmp/ext-lib/ /tmp/ext-share/extension/
Post-Release Checklist
Complete these steps immediately after a release tag has been pushed and both the Release and PGXN workflows have finished successfully.
-
Create a post-release branch from
main(e.g.post-release-<ver>-a) -
Bump
Cargo.tomlversionto the next development version (e.g.0.12.0→0.13.0) -
Bump
pgtrickle-tui/Cargo.tomlversionto the same next development version — must always matchCargo.toml -
Bump
META.json— both the top-level"version"and the nested"provides" → "pg_trickle" → "version"to match -
Write
plans/PLAN_0_<next>_0.md— initial planning document for the next milestone -
Delete
plans/PLAN_0_<released>_0.md— remove the now-completed plan -
Wrap roadmap items — in
ROADMAP.md, wrap all completed items from the old release with<details>tags to archive them -
Add
## [Unreleased]stub toCHANGELOG.mdabove the just-released entry -
Create
sql/pg_trickle--<released>--<next>.sql— empty upgrade script stub for the next migration hop -
Copy
sql/archive/pg_trickle--<released>.sql→sql/archive/pg_trickle--<next>.sql— placeholder archive baseline for the next version -
Update
justfile— advancebuild-upgrade-imageandtest-upgradetodefaults to<next>; update thebuild-hubDocker image tag -
Update
tests/e2e_upgrade_tests.rs— advance allunwrap_or("<released>".into())fallback strings to<next> -
Update version numbers in
README.md— search for occurrences of the released version (e.g.0.17.0) and advance them to<next>: CNPG image reference (ghcr.io/grove/pg_trickle-ext:<version>), dbtrevisiontag, and any other hardcoded version strings. A quick check:grep -n '<released>' README.md -
Run
just check-version-sync— must exit 0 before opening the PR -
Open a PR against
mainwith the commit titlechore: start v<next> development cycle
Preparing for the Next Release (Pre-Work Checklist)
Use this checklist at the start of each new release milestone to ensure the repository is properly set up before development begins. This maps directly to what just check-version-sync verifies.
| File / target | Action | check-version-sync check |
|---|---|---|
Cargo.toml | version = "<next>" | canonical version source |
META.json | both "version" fields set to <next> | PGXN manifest |
CHANGELOG.md | ## [Unreleased] section present | (manual hygiene) |
sql/pg_trickle--<prev>--<next>.sql | stub file exists | upgrade SQL exists |
sql/archive/pg_trickle--<next>.sql | placeholder file exists (copy of <prev>) | archive SQL exists |
.github/workflows/ci.yml | upgrade matrix and chain end at <next> | CI matrix up to date |
justfile | build-upgrade-image and test-upgrade to defaults = <next> | justfile defaults |
tests/e2e_upgrade_tests.rs | all unwrap_or fallbacks = "<next>" | e2e fallback strings |
Quick-verify with:
just check-version-sync
# Should print: All version references are in sync.
Release Artifacts
Each release produces:
| Artifact | Description |
|---|---|
pg_trickle-<ver>-pg18-linux-amd64.tar.gz | Extension files for Linux x86_64 |
pg_trickle-<ver>-pg18-macos-arm64.tar.gz | Extension files for macOS Apple Silicon |
pg_trickle-<ver>-pg18-windows-amd64.zip | Extension files for Windows x64 |
SHA256SUMS.txt | SHA-256 checksums for all archives |
ghcr.io/grove/pg_trickle-ext:<ver> | CNPG extension image for Image Volumes (amd64 + arm64) |
Installing from an archive
tar xzf pg_trickle-<version>-pg18-linux-amd64.tar.gz
cd pg_trickle-<version>-pg18-linux-amd64
sudo cp lib/*.so "$(pg_config --pkglibdir)/"
sudo cp extension/*.control extension/*.sql "$(pg_config --sharedir)/extension/"
Then add to postgresql.conf and restart:
shared_preload_libraries = 'pg_trickle'
See INSTALL.md for full installation details.
Pre-releases
Tags containing -rc, -beta, or -alpha (e.g., v0.3.0-rc.1) are
automatically marked as pre-releases on GitHub. Pre-release extension images are
tagged but do not update the latest tag.
Hotfix Releases
For urgent fixes on an older release:
# Branch from the tag
git checkout -b hotfix/v0.2.1 v0.2.0
# Apply fix, bump version to 0.2.1
git commit -am "fix: ..."
git push origin hotfix/v0.2.1
# Tag from the branch (CI will still run the release workflow)
git tag -a v0.2.1 -m "Release v0.2.1"
git push origin v0.2.1
Files to Update for Each Release
Every release requires manual updates to the files below. Missing any of them leads to version skew between the code, the docs, and the packages.
| File | What to change | Why |
|---|---|---|
Cargo.toml | version = "x.y.z" field | The canonical version source. pgrx reads this at build time and substitutes it into pg_trickle.control via @CARGO_VERSION@. The git tag must match. |
META.json | Both "version" fields (top-level and inside "provides") | The PGXN package manifest. The pgxn.yml workflow uploads this file as part of the source archive; a stale version here means the wrong version appears on pgxn.org. |
CHANGELOG.md | Rename ## [Unreleased] → ## [x.y.z] — YYYY-MM-DD; add a new empty ## [Unreleased] at the top | Keeps the public changelog accurate and gives downstream users a dated record of changes. |
ROADMAP.md | Update the preamble's latest-release/current-milestone lines; mark the released milestone done; advance the "We are here" pointer to the next milestone | Keeps the forward-looking plan aligned with reality. Leaves no confusion about what just shipped versus what is next. |
README.md | Update test-count line (~N unit tests + M E2E tests) if test counts changed significantly | The README is the first thing users read; stale numbers erode trust. |
INSTALL.md | Update any version numbers in install commands or example URLs | Users copy-paste installation commands; stale versions cause failures. |
docs/UPGRADING.md | Add the new version-specific migration notes and extend the supported upgrade-path table | Documents exactly what ALTER EXTENSION ... UPDATE will do and which chains are supported. |
sql/pg_trickle--<old>--<new>.sql | Add or update the hand-authored upgrade script for every SQL-surface change (new objects, changed signatures, changed defaults, view changes). Also carry forward all functions/views/tables added in previous releases — the upgrade script is cumulative. | ALTER EXTENSION ... UPDATE only applies what is explicitly scripted; function defaults and signatures stored in pg_proc do not update themselves. Omitting a function that existed in <old> but is expected in <new> will break user upgrades. |
sql/archive/pg_trickle--<new>.sql | Regenerate and commit the full-install SQL baseline for the new version. This file was created as a placeholder copy of <prev> at the start of the development cycle — it must be replaced with the actual generated SQL before tagging. Run cargo pgrx schema (or the equivalent just target) to produce the final schema, then overwrite the placeholder. | Future upgrade-completeness checks and upgrade E2E tests need an exact baseline for the released version. A stale placeholder from the start of the cycle will cause spurious failures. |
.github/workflows/ci.yml, justfile, tests/build_e2e_upgrade_image.sh, tests/Dockerfile.e2e-upgrade | Advance the upgrade-check chain and default upgrade-E2E target version to the new release | Prevents release automation and local upgrade validation from getting stuck on the previous version after a new migration hop is added. |
pg_trickle.control | No manual edit needed — default_version is set to '@CARGO_VERSION@' and pgrx substitutes it at build time. Verify the substitution in the built artifact. | Ensures the SQL CREATE EXTENSION command installs the right version. |
CRITICAL: After updating
sql/pg_trickle--<old>--<new>.sql, always runjust check-upgrade-allto verify that the upgrade script is complete. This checks not just the immediate hop to the new version, but the entire upgrade chain from v0.1.3 onwards. If the check fails (e.g. "ERROR: 3 new function(s) missing"), it means the upgrade script is missing one or more SQL objects that users will expect to have after upgrading. Fix all failures before tagging.
Checklist summary
[ ] Cargo.toml — version bumped
[ ] META.json — both "version" fields updated to match
[ ] CHANGELOG.md — [Unreleased] renamed to [x.y.z] with date; new empty [Unreleased] added
[ ] ROADMAP.md — preamble updated; released milestone marked done
[ ] README.md — test counts current (if materially changed)
[ ] INSTALL.md — version references current
[ ] docs/UPGRADING.md — latest migration notes and supported chains added
[ ] sql/pg_trickle--<old>--<new>.sql — covers every SQL-surface change AND carries forward all previous release functions
[ ] sql/archive/pg_trickle--<new>.sql — regenerated from final schema and committed (replaces the dev-cycle placeholder)
[ ] just check-upgrade-all — all upgrade steps pass completeness checks (not just the one-step hop)
[ ] Upgrade automation defaults — CI/local upgrade checks and E2E target the new version
[ ] just check-version-sync — all version references in sync
[ ] All CI checks on main have passed (verify the last run on the version-bump commit succeeded)
[ ] git tag matches Cargo.toml version
Troubleshooting
Release workflow failed
Go to the Actions tab and identify which job failed. Then follow the appropriate recovery path below.
Option A: Re-run (transient failure)
If the failure is transient — network timeout, registry hiccup, runner issue — you can re-run without changing anything:
- Open the failed workflow run in the Actions tab
- Click Re-run all jobs (or re-run just the failed job)
This works because the v* tag still points to the same commit, and the
workflow uses cancel-in-progress: false so a re-run won't be cancelled.
Option B: Fix code and re-tag
If the failure is a real build or code issue:
# 1. Delete the remote tag
git push origin :refs/tags/v0.2.0
# 2. Delete the local tag
git tag -d v0.2.0
# 3. Fix the issue, commit, and push
git add <files>
git commit -m "fix: ..."
git push origin main
# 4. Re-tag on the new commit and push
git tag -a v0.2.0 -m "Release v0.2.0"
git push origin v0.2.0
This triggers a fresh release workflow run.
Option C: Clean up a partial GitHub Release
If the workflow created a draft or partial Release before failing:
- Go to Releases in the repository
- Delete the broken release (this does not delete the tag)
- Then follow Option A or Option B above
Upgrade script completeness check failed
If just check-upgrade-all reports errors like "ERROR: X new function(s) missing from upgrade script", it means the upgrade SQL script is incomplete:
# 1. Look at the error — it tells you exactly what's missing
just check-upgrade-all # e.g. "ERROR: 3 new function(s) missing from upgrade script:
# - pgtrickle.\"explain_refresh_mode\"
# - pgtrickle.\"fuse_status\"
# - pgtrickle.\"reset_fuse\""
# 2. Find where those objects are defined in the previous release
# (they should already exist in sql/archive/pg_trickle--<prev>.sql)
grep -n "CREATE.*FUNCTION.*explain_refresh_mode" sql/archive/pg_trickle--*.sql
# 3. Copy the function definitions (CREATE OR REPLACE FUNCTION) to the
# upgrade script you're fixing. They should go into:
# sql/pg_trickle--<old>--<new>.sql
#
# Typically, carry-forward functions are grouped in their own section
# at the top of the upgrade script with a comment explaining they're
# from a prior release.
# 4. Re-run the check to verify it passes
just check-upgrade-all
Why this happens: When a new release (e.g. v0.11.0) adds SQL functions, those
functions must be explicitly included in all subsequent upgrade scripts. The upgrade
script is the ground truth — PostgreSQL only applies what is listed in the .sql file.
If you skip a function that users expect, their upgraded extension will be missing
that object.
Common failure causes
| Symptom | Cause | Fix |
|---|---|---|
| Version mismatch error | Cargo.toml version doesn't match the git tag | Run just check-version-sync, fix any skew, commit, delete tag, re-tag (Option B) |
| Build failure | Compilation error in release profile | Fix on main, re-tag (Option B) |
| Docker push failed | Missing permissions | Verify packages: write is in the workflow and GITHUB_TOKEN has GHCR access, then re-run (Option A) |
| Smoke test failed | Extension doesn't load in PostgreSQL | Fix the issue, re-tag (Option B) |
| PGXN upload failed | Missing PGXN_USERNAME / PGXN_PASSWORD secrets, or META.json version not updated | Add the secrets in repository settings; verify META.json version matches the tag; re-run the pgxn.yml workflow from the Actions tab |
just check-upgrade-all reports missing functions/views | Upgrade script is incomplete — new objects from prior releases not carried forward | See "Upgrade script completeness check failed" above for recovery steps |
| Rate limited | GitHub API or GHCR throttling | Wait a few minutes, then re-run (Option A) |
Yanking a release
If a release has a critical issue:
- Mark it as pre-release on the GitHub Releases page (uncheck "Set as the latest release")
- Add a warning to the release notes
- Publish a patch release with the fix