Just a Theory

By David E. Wheeler

Posts about CI/CD

Auto-Release PostgreSQL Extensions on PGXN

I last wrote about auto-releasing PostgreSQL extensions on PGXN back in 2020, but I thought it worthwhile, following my Postgres Extensions Day talk last week, to return again to the basics. With the goal to get as many extensions distributed on PGXN as possible, this post provides step-by-step instructions to help the author of any extension or Postgres utility to quickly and easily publish every release.

TL;DR

  1. Create a PGXN Manager account
  2. Add a META.json file to your project
  3. Add a pgxn-tools powered CI/CD pipeline to publish on tag push
  4. Fully-document your extensions

Release your extensions on PGXN

PGXN aims to become the defacto source for all open-source PostgreSQL extensions and tools, in order to help users quickly find and learn how to use extensions to meet their needs. Currently, PGXN distributes source releases for around 400 extensions (stats on the about page), a fraction of the ca. 1200 known extensions. Anyone looking for an extension might exist to solve some problem must rely on search engines to find potential solutions between PGXN, GitHub, GitLab, blogs, social media posts, and more. Without a single trusted source for extensions, and with the proliferation of AI Slop in search engine results, finding extensions aside from a few well-known solutions proves a challenge.

By publishing releases and full documentation — all fully indexed by its search index — PGXN aims to be that trusted source. Extension authors provide all the documentation, which PGXN formats for legibility and linking. See, for example, the pgvector docs.

If you want to make it easier for users to find your extensions, to read your documentation — not to mention provide sources for binary packaging systems — publish every release on PGXN.

Here’s how.

Create an Account

Step one: create a PGXN Manager account. The Email, Nickname, and Why fields are required. The form asks “why” as a simple filter for bad actors. Write a sentence describing what you’d like to release — ideally with a link to the source repository — and submit. We’ll get the account approved forthwith, which will send a confirmation email to your address. Follow the link in the email and you’ll be good to go.

Anatomy of a Distribution

A PostgreSQL extension source tree generally looks something like this (taken from the pair repository):

pair
├── Changes
├── doc
│   └── pair.md
├── Makefile
├── META.json
├── pair.control
├── README.md
├── sql
│   ├── pair--unpackaged--0.1.2.sql
│   └── pair.sql
└── test
    ├── expected
    │   └── base.out
    └── sql
        └── base.sql

Extension authors will recognize the standard PGXS (or pgrx) source distribution files; only META.json file needs explaining. The META.json file is, frankly, the only file that PGXN requires in a release. It contains the metadata to describe the release, following the PGXN Meta Spec. This example contains only the required fields:

{
  "name": "pair",
  "version": "0.1.0",
  "abstract": "A key/value pair data type",
  "maintainer": "David E. Wheeler <[email protected]>",
  "license": "postgresql",
  "provides": {
    "pair": {
      "file": "sql/pair.sql",
      "version": "0.1.0"
    }
  },
  "meta-spec": {
    "version": "1.0.0"
  }
}

Presumably these fields contain no surprises, but a couple of details:

  • It starts with the name of the distribution, pair, and the release version, 0.1.0.
  • The abstract provides a brief description of the extension, while the maintainer contains contact information.
  • The license stipulates the distribution license, of course, usually one of a few known, but may be customized.
  • The provides object lists the extensions or tools provided, each named by an object key that points to details about the extension, including main file, version, and potentially an abstract and documentation file.
  • The meta-spec object identifies the meta spec version used for the META.json itself.

Release It!

This file with these fields is all you need to make a release. Assuming Git, package up the extension source files like so (replacing your extension name and version as appropriate).

git archive --format zip --prefix=pair-0.1.0 -o pair-0.1.0.zip HEAD

Then navigate to the release page, authenticate, and upload the resulting .zip file.

Screenshot with a box labeled “Upload a Distribution Archive”. It contains an “Archive” label in front of a button labeled “Choose File”. Next to it is a zip file icon and  the text “pair-0.1.0.zip”. Below the box is another button labeled “Release It!”

And that’s it! Your release will appear on pgxn.org and on Mastodon within five minutes.

Let’s Automate it!

All those steps would be a pain in the ass to follow for every release. Let’s automate it using pgxn-tools! This OCI image contains the tools necessary to package and upload an extension release to PGXN. Ideally, use a CI/CD pipeline like a GitHub Workflow to publish a release on every version tag.

Set up Secrets

pgxn-tools uses your PGXN credentials to publish releases. To keep them safe, use the secrets feature of your preferred CI/CD tool. This figure shows the “Secrets and variables” configuration for a GitHub repository, with two repository secrets: PGXN_USERNAME and PGXN_PASSWORD:

Screenshot of GitHub Secrets configuration featuring two repository secrets: PGXN_USERNAME and PGXN_PASSWORD.

Create a Pipeline

Use those secrets and pgxn-tools in CI/CD pipeline. Here, for example, is a minimal GitHub workflow to publish a release for every SemVer tag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
on:
  push:
    tags: ['v[0-9]+.[0-9]+.[0-9]+']
jobs:
  release:
    name: Release on PGXN
    runs-on: ubuntu-latest
    container: pgxn/pgxn-tools
    env:
      PGXN_USERNAME: ${{ secrets.PGXN_USERNAME }}
      PGXN_PASSWORD: ${{ secrets.PGXN_PASSWORD }}
    steps:
    - name: Check out the repo
      uses: actions/checkout@v4
    - name: Bundle the Release
      run: pgxn-bundle
    - name: Release on PGXN
      run: pgxn-release

Details:

  • Line 3 configures the workflow to run on a SemVer tag push, typically used to denote a release.
  • Line 8 configures the workflow job to run inside a pgxn-tools container.
  • Lines 10-11 set environment variables with the credentials from the secrets.
  • Line 16 bundles the release using either git archive or zip.
  • Line 18 publishes the release on PGXN.

Now publishing a new release is as simple as pushing a SemVer tag, like so:

git tag v0.1.0 -sm 'Tag v0.1.0'
git push --follow-tags

That’s it! The workflow will automatically publish the extension for every release, ensuring the latest and greatest always make it to PGXN where users and packagers will find them.

The pgxn-tools image also provides tools to easily test a PGXS or pgrx extension on supported PostgreSQL versions (going back as far as 8.2), also super useful in a CI/CD pipeline. See Test Postgres Extensions With GitHub Actions for instructions. Depending on your CI/CD tool of choice, you might take additional steps, such as publishing a release on GitHub, as previously described.

Optimizing for PGXN

But let’s dig deeper into how to optimize extensions for maximum discoverability and user visibility on PGXN.

Add More Metadata

The META.json file supports many more fields that PGXN indexes and references. These improve the chances users will find what they’re looking for. This detailed example demonstrates how a PostGIS META.json file might start to provide additional metadata:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
{
   "name": "postgis",
   "abstract": "Geographic Information Systems Extensions to PostgreSQL",
   "description": "This distribution contains a module which implements GIS simple features, ties the features to R-tree indexing, and provides many spatial functions for accessing and analyzing geographic data.",
   "version": "3.5.0",
   "maintainer": [
      "Paul Ramsey <[email protected]>",
      "Sandro Santilli <[email protected]>"
   ],
   "license": [ "gpl_2", "gpl_3" ],
   "provides": {
      "postgis": {
         "abstract": "PostGIS geography spatial types and functions",
         "file": "extensions/postgis/postgis.control",
         "docfile": "extensions/postgis/doc/postgis.md",
         "version": "3.5.0"
      },
      "address_standardizer": {
         "abstract": "Used to parse an address into constituent elements. Generally used to support geocoding address normalization step.",
         "file": "extensions/address_standardizer/address_standardizer.control",
         "docfile": "extensions/address_standardizer/README.address_standardizer",
         "version": "3.5.0"
      }
   },
   "prereqs": {
      "runtime": {
         "requires": {
            "PostgreSQL": "12.0.0",
            "plpgsql": 0
         }
      },
      "test": {
         "recommends": {
            "pgTAP": 0
         }
      }
   },
   "resources": {
      "bugtracker": {
         "web": "https://trac.osgeo.org/postgis/"
      },
      "repository": {
         "url": "https://git.osgeo.org/gitea/postgis/postgis.git",
         "web": "https://git.osgeo.org/gitea/postgis/postgis",
         "type": "git"
      }
   },
   "generated_by": "David E. Wheeler",
   "meta-spec": {
      "version": "1.0.0",
      "url": "https://pgxn.org/meta/spec.txt"
   },
   "tags": [
      "gis",
      "spatial",
      "geometry",
      "raster",
      "geography",
      "location"
   ]
}
  • Line 4 contains a longer description of the distribution.
  • Lines 6-9 show how to list multiple maintainers as an array.
  • Line 10 demonstrates support for an array of licenses.
  • Lines 11-24 list multiple extensions included in the distribution, with abstracts and documentation files for each.
  • Lines 25-37 identify dependencies for various phases of the distribution lifecycle, including configure, build, test, runtime, and develop. Each contains an object identifying PostgreSQL or extension dependencies.
  • Lines 38-47 lists resources for the distribution, including issue tracking and source code repository.
  • Lines 53-60 contains an array of tags, an arbitrary list of keywords for a distribution used both in the search index and the PGXN tag cloud.

Admittedly the PGXN Meta Spec provides a great deal of information. Perhaps the simplest way to manage it is to copy an existing META.json from another project (or above) and edit it. In general, only the version fields require updating for each release.

Write Killer Docs

The most successful extensions provide ample descriptive and reference documentation, as well as examples. Most extensions feature a README, of course, which contains basic information, build and install instructions, and contact info. But as the pair tree, illustrates, PGXN also supports extension-specific documentation in a variety of formats, including:

Some examples:

PGXN will also index and format additional documentation files in any of the above formats. See, for example, all the files formatted for orafce.

Exclude Files from Release

Use gitattributes to exclude files from the release. For example, distributions don’t generally include .gitignore or the contents of the .github directory. Exclude them from the archive created by git archive by assigning export-ignore to each path to exclude in the .gitattributes file, like so:

.gitignore export-ignore
.gitattributes export-ignore
.github export-ignore

What’s It All For?

PGXN aims to be the trusted system of record for open-source PostgreSQL extensions. Of course that requires that it contain all (or nearly all) of said extensions. Hence this post.

Please help make it so by adding your extensions, both to help users find the extensions they need, and to improve the discoverability of your extensions. Over time, we aim to feed downstream extension distribution systems, such as Yum, APT, CloudNativePG, OCI, and more.

Let’s make extensions available everywhere to everyone.

Automate Postgres Extension Releases on GitHub and PGXN"

Back in June, I wrote about testing Postgres extensions on multiple versions of Postgres using GitHub Actions. The pattern relies on Docker image, pgxn/pgxn-tools, which contains scripts to build and run any version of PostgreSQL, install additional dependencies, build, test, bundle, and release an extension. I’ve since updated it to support testing on the the latest development release of Postgres, meaning one can test on any major version from 8.4 to (currently) 14. I’ve also created GitHub workflows for all of my PGXN extensions (except for pgTAP, which is complicated). I’m quite happy with it.

But I was never quite satisfied with the release process. Quite a number of Postgres extensions also release on GitHub; indeed, Paul Ramsey told me straight up that he did not want to manually upload extensions like pgsql-http and PostGIS to PGXN, but for PGXN to automatically pull them in when they were published on GitHub. It’s pretty cool that newer packaging systems like pkg.go.dev auto-index any packages on GibHub. Adding such a feature to PGXN would be an interesting exercise.

But since I’m low on TUITs for such a significant undertaking, I decided instead to work out how to automatically publish a release on GitHub and PGXN via GitHub Actions. After experimenting for a few months, I’ve worked out a straightforward method that should meet the needs of most projects. I’ve proven the pattern via the pair extension’s release.yml, which successfully published the v0.1.7 release today on both GitHub and PGXN. With that success, I updated the pgxn/pgxn-tools documentation with a starter example. It looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
name: Release
on:
  push:
    tags:
      - 'v*' # Push events matching v1.0, v20.15.10, etc.
jobs:
  release:
    name: Release on GitHub and PGXN
    runs-on: ubuntu-latest
    container: pgxn/pgxn-tools
    env:
      # Required to create GitHub release and upload the bundle.
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
    - name: Check out the repo
      uses: actions/checkout@v4
    - name: Bundle the Release
      id: bundle
      run: pgxn-bundle
    - name: Release on PGXN
      env:
        # Required to release on PGXN.
        PGXN_USERNAME: ${{ secrets.PGXN_USERNAME }}
        PGXN_USERNAME: ${{ secrets.PGXN_PASSWORD }}
      run: pgxn-release
    - name: Create GitHub Release
      id: release
      uses: softprops/action-gh-release@v2
      with:
        tag_name: ${{ github.ref }}
        name: Release ${{ github.ref }}
        body: |
          Changes in this Release
          - First Change
          - Second Change
        files: ${{ steps.bundle.outputs.bundle }}

Update 2025-02-25

Updated the example and description to use softprops/action-gh-release to create a GitHub release and upload files, replacing two steps that used the now-deprecated actions/create-release and actions/upload-release-asset actions.

Here’s how it works:

  • Lines 4-5 trigger the workflow only when a tag starting with the letter v is pushed to the repository. This follows the common convention of tagging releases with version numbers, such as v0.1.7 or v4.6.0-dev. This assumes that the tag represents the commit for the release.

  • Line 10 specifies that the job run in the pgxn/pgxn-tools container, where we have our tools for building and releasing extensions.

  • Line 13 passes the GITHUB_TOKEN variable into the container. This is the GitHub personal access token that’s automatically set for every build. It lets us call the GitHub API via actions later in the workflow.

  • Step “Bundle the Release”, on Lines 17-19, validates the extension META.json file and creates the release zip file. It does so by simply reading the distribution name and version from the META.json file and archiving the Git repo into a zip file. If your process for creating a release file is more complicated, you can do it yourself here; just be sure to include an id for the step, and emit a line of text so that later actions know what file to release. The output should be appended to the $GITHUB_OUTPUT file like this, with $filename representing the name of the release file, usually $extension-$version.zip:

    echo bundle=$filename >> $GITHUB_OUTPUT
    
  • Step “Release on PGXN”, on lines 20-25, releases the extension on PGXN. We take this step first because it’s the strictest, and therefore the most likely to fail. If it fails, we don’t end up with an orphan GitHub release to clean up once we’ve fixed things for PGXN.

  • With the success of a PGXN release, step “Create GitHub Release”, on lines 26-36, uses the GitHub softprops/action-gh-release action to create a release corresponding to the tag. You’ll want to customize the body of the release; for the pair extension, I added a simple make target to generate a file, then pass it via the body_path config:

    - name: Generate Release Changes
      run: make latest-changes.md
    - name: Create GitHub Release
      uses: softprops/action-gh-release@v2
      with:
        tag_name: ${{ github.ref }}
        name: Release ${{ github.ref }}
        body_path: latest-changes.md
        files: ${{ steps.bundle.outputs.bundle }}
    

Lotta steps, but works nicely. I only wish I could require that the testing workflow finish before doing a release, but I generally tag a release once it has been thoroughly tested in previous commits, so I think it’s acceptable.

Now if you’ll excuse me, I’m off to add this workflow to my other PGXN extensions.

Test Postgres Extensions With GitHub Actions

I first heard about GitHub Actions a couple years ago, but fully embraced them only in the last few weeks. Part of the challenge has been the paucity of simple but realistic examples, and quite a lot of complicated-looking JavaScript-based actions that seem like overkill. But through trial-and-error, I figured out enough to update my Postgres extensions projects to automatically test on multiple versions of Postgres, as well as to bundle and release them on PGXN. The first draft of that effort is pgxn/pgxn-tools1, a Docker image with scripts to build and run any version of PostgreSQL between 8.4 and 12, install additional dependencies, build, test, bundle, and release an extension.

Here’s how I’ve put it to use in a GitHub workflow for semver, the Semantic Version data type:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
name: CI
on: [push, pull_request]
jobs:
  test:
    strategy:
      matrix:
        pg: [12, 11, 10, 9.6, 9.5, 9.4, 9.3, 9.2]
    name: 🐘 PostgreSQL ${{ matrix.pg }}
    runs-on: ubuntu-latest
    container: pgxn/pgxn-tools
    steps:
      - run: pg-start ${{ matrix.pg }}
      - uses: actions/checkout@v3
      - run: pg-build-test

The important bits are in the jobs.test object. Under strategy.matrix, which defines the build matrix, the pg array defines each version to be tested. The job will run once for each version, and can be referenced via ${{ matrix.pg }} elsewhere in the job. Line 10 has the job a pgxn/pgxn-tools container, where the steps run. The are are:

  • Line 12: Install and start the specified version of PostgreSQL
  • Line 13: Clone the semver repository
  • Line 14: Build and test the extension

The intent here is to cover the vast majority of cases for testing Postgres extensions, where a project uses PGXS Makefile. The pg-build-test script does just that.

A few notes on the scripts included in pgxn/pgxn-tools:

  • pg-start installs, initializes, and starts the specified version of Postgres. If you need other dependencies, simply list their Debian package names after the Postgres version.

  • pgxn is a client for PGXN itself. You can use it to install other dependencies required to test your extension.

  • pg-build-test simply builds, installs, and tests a PostgreSQL extension or other code in the current directory. Effectively the equivalent of make && make install && make installcheck.

  • pgxn-bundle validates the PGXN META.json file, reads the distribution name and version, and bundles up the project into a zip file for release to PGXN.

  • pgxn-release uploads a release zip file to PGXN.

In short, use the first three utilities to handle dependencies and test your extension, and the last two to release it on PGXN. Simply set GitHub secrets with your PGXN credentials, pass them in environment variables named PGXN_USERNAME and PGXN_PASSWORD, and the script will handle the rest. Here’s how a release job might look:

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  release:
    name: Release on PGXN
    # Release pon push to main when the test job succeeds.
    needs: test
    if: github.ref == 'refs/heads/main' && github.event_name == 'push' && needs.test.result == 'success'
    runs-on: ubuntu-latest
    container:
      image: pgxn/pgxn-tools
      env:
        PGXN_USERNAME: ${{ secrets.PGXN_USERNAME }}
        PGXN_PASSWORD: ${{ secrets.PGXN_PASSWORD }}
    steps:
      - name: Check out the repo
        uses: actions/checkout@v3
      - name: Bundle the Release
        run: pgxn-bundle
      - name: Release on PGXN
        run: pgxn-release

Note that lines 18-19 require that the test job defined above pass, and ensure the job runs only on a push event to the main branch, where we push final releases. We set PGXN_USERNAME and PGXN_PASSWORD from the secrets of the same name, and then, in lines 27-32, check out the project, bundle it into a zip file, and release it on PGXN.

There are a few more features of the image, so read the docs for the details. As a first cut at PGXN CI/CD tools, I think it’s fairly robust. Still, as I gain experience and build and release more extensions in the coming year, I expect to work out integration with publishing GitHub releases, and perhaps build and publish relevant actions on the GitHub Marketplace.


  1. Not a great name, I know, will probably change as I learn more. ↩︎