Skip to content

Release pipeline

Releases are cut by pushing a version tag. Everything after that runs on GitHub Actions: no artifact is built or published from a workstation. This page describes the mechanics.

What a release contains

ChannelWhat shipsProduced by
GitHub releasepgrdf-<ver>-pg18-glibc-amd64.tar.gz, pgrdf-<ver>-pg18-glibc-arm64.tar.gz, the PGXN source archive pgrdf-<ver>.zip, and an aggregate SHA256SUMSrelease.yml
OCI bundle on GHCRghcr.io/styk-tv/pgrdf-bundle:<ver>-pg18-<arch> for each architecture, and the multi-architecture index :<ver> (also tagged :v<ver>), each with an SLSA Build Provenance v1 attestationoci-publish.yml
LATEST.mdthe current attested versionupdate-latest-md.yml
PGXNthe same source archive; PGXN renders README.pgxn.md as the package pageuploaded by hand

Each tarball's layout and MANIFEST.json are described on Packaging.

Before the tag

A release is prepared in a pull request, so the version bump and the changelog are reviewed before anything is built.

  • One version everywhere. The tag (without its v) must equal the version in Cargo.toml, default_version in pgrdf.control, and both version fields in META.json. The compose mount line and the smoke test's expected output follow the same version, and CI checks them on every push.
  • Changelog. The [Unreleased] section of CHANGELOG.md becomes the new version's section in the same pull request.
  • Annotated tag. The tag annotation becomes the GitHub release body. If the annotation is empty, the [Unreleased] section of the changelog is used instead, and the body must mention the tag.
  • One version, one commit, forever. A tag is never deleted, re-pushed or reused. If a release fails after its tag is pushed, the version is abandoned and the next number is used. Version gaps are normal.
  • One at a time. The next tag is pushed only once the previous release appears in LATEST.md.

release.yml: build

Runs on a push of a v* tag.

  1. Pre-build check. Compares the tag with Cargo.toml, pgrdf.control and META.json; any mismatch stops the build before anything is produced.
  2. Build. One job per architecture, each on its native runner (amd64 on ubuntu-22.04, arm64 on ubuntu-24.04-arm), never cross-compiled. The .so is built inside the PostgreSQL 18 / Debian trixie builder image (compose/builder.Containerfile), so the container's userland, not the runner's, fixes the glibc floor, and both architectures floor the same way. Release builds use no build cache. build_id() is set to the tag.
  3. Repack. The output is repacked into the tarball layout, with every upgrade script from sql/, LICENSE, a MANIFEST.json generated from the repacked files (including the glibc floor measured from the .so), and a SHA256SUMS over the contents.
  4. Post-build check. Confirms the layout carries the tag's install script and control version, and that MANIFEST.json reports version == extversion == runtime == tag.
  5. Release job. Builds the PGXN archive (make dist), writes an aggregate SHA256SUMS over the tarballs and the archive, renders the release body, and creates the GitHub release as a draft. Its last step triggers oci-publish.yml with a repository_dispatch event carrying the tag.

oci-publish.yml: gate, publish, attest

  1. Gate (both architectures, each on its native runner). Boots a clean postgres:18-trixie, installs the exact tarball bytes about to be published, runs CREATE EXTENSION pgrdf VERSION '<ver>', and checks that extversion and pgrdf.version() equal the version, that pgrdf.build_id() equals the tag, and that MANIFEST.json agrees with the running library. If either architecture fails, nothing is pushed or attested.
  2. Leaf (needs the gate). Pushes each tarball with oras as ghcr.io/styk-tv/pgrdf-bundle:<ver>-pg18-<arch>, annotated with the base image, commit and architecture, and attests it with actions/attest-build-provenance.
  3. Index. Creates the multi-architecture index under :<ver> and :v<ver>, attests it, then runs a consumer-style smoke test: pull the published artifact, install it into a clean postgres:18-trixie, and check the identity triple again. It then triggers update-latest-md.yml.

The workflows are chained with repository_dispatch because events caused by the default GITHUB_TOKEN (a release it created, a run it started) do not trigger release: published or workflow_run downstream.

update-latest-md.yml: advertise

  1. Runs gh attestation verify on the digests it is about to advertise.
  2. Only if every digest verifies, renders LATEST.md and commits it to main as docs(auto): refresh LATEST.md to v<ver>.
  3. Flips the GitHub release from draft to published and marks it Latest.

A publicly visible GitHub release therefore always has a verified, advertised OCI bundle behind it. LATEST.md is written only by this workflow and is never edited by hand. A release counts as done when LATEST.md advertises it.

Re-running

There is no manual publish path. If one job fails, use Re-run failed jobs on the existing run; never delete and re-push the tag. oci-publish.yml and update-latest-md.yml also accept a manual workflow_dispatch with the tag or version, to re-run a single hop of the chain.

Verifying a release

sh
VER=0.6.34
ARCH=amd64
curl -fsSLO https://github.com/styk-tv/pgRDF/releases/download/v$VER/pgrdf-$VER-pg18-glibc-$ARCH.tar.gz
curl -fsSLO https://github.com/styk-tv/pgRDF/releases/download/v$VER/SHA256SUMS
sha256sum -c SHA256SUMS --ignore-missing          # the tarball matches the release

tar -xzf pgrdf-$VER-pg18-glibc-$ARCH.tar.gz
(cd pgrdf-$VER-pg18-glibc-$ARCH && sha256sum -c SHA256SUMS)   # each file inside it

gh attestation verify oci://ghcr.io/styk-tv/pgrdf-bundle:$VER --repo styk-tv/pgRDF

After installing, the identity triple confirms which build is running:

sql
SELECT pgrdf.version(), pgrdf.build_id(),
       (SELECT extversion FROM pg_extension WHERE extname = 'pgrdf');
--  0.6.34 | v0.6.34 | 0.6.34

pgRDF is released under the MIT license. Documentation built with VitePress, served via GitHub Pages.