For most of the last decade, every copy of pgAdmin that anyone has ever downloaded was built by a Jenkins server, and for most of that time it did the job perfectly well. It built the Debian and Red Hat packages, the Windows installer, the macOS app bundle, the Python wheel, the container image, the Helm chart and the documentation; it signed them, published them to the download site, and told me when something had gone wrong. It was also, and there is no polite way to put this, a machine (and a bunch of worker machines) that almost nobody could see, running jobs configured through a web interface whose settings existed nowhere else. A handful of us could log into it and push a release, although in practice I was the one who knew how it was wired together and therefore the one who fixed it when it broke, which is its own kind of single point of failure. If you contributed a change to pgAdmin's packaging, you could not read the thing that would build it, and if I had been hit by the proverbial bus, the recovery plan involved somebody logging into a server and reading XML.

We have now moved the whole lot to GitHub Actions, and since this changes things for both users and contributors, it seems worth explaining what has changed and why.

moving_the_pgadmin_1

Why move at all

The honest answer is that the build farm had become the least visible part of the project. pgAdmin's source code is reviewed, discussed and version controlled; its build process was none of those things. A Jenkins job is a configuration in a database, and whilst we did eventually keep that configuration in a repository, the repository was a backup rather than the source of truth: the running system was authoritative, and a change made in the interface was live immediately, reviewed by nobody.

That matters more than it sounds. The packaging is where a surprising number of user-visible bugs live, because it decides which libraries get bundled, which Python interpreter ships, how the installer is signed, and where the packages land in the repositories that apt and dnf read. None of that is exercised by the unit tests, and until recently none of it was reviewable either.

There was also a practical problem: Jenkins needs machines, and those machines need maintaining, patching, and occasionally replacing, which is time spent on infrastructure rather than on pgAdmin.

What it looks like now

Everything lives in .github/workflows in the main pgAdmin repository, as ordinary files that turn up in pull requests like any other change. There are twenty-odd of them, covering the build checks that run on every commit, the nightly snapshot builds, the release candidate builds, and the promotion of a candidate to a published release.

The interesting one is the acceptance test, which runs against a release candidate before anybody is allowed to promote it. Rather than inspecting the tree and confirming that the files we expected are present, which is what our old checks did, it follows the instructions in our own repository README, in a clean container, and installs pgAdmin the way a user would: it adds the APT repository on Debian 12 and 13 and Ubuntu 22.04, 24.04 and 26.04, and the YUM repository on AlmaLinux 9 and 10 and Fedora 43 and 44, with no verification bypassed and no --nogpgcheck anywhere. It downloads and silently installs the Windows installer and then checks the Authenticode signature on the executable that landed on disk. It mounts the macOS disk image with the quarantine attribute that a browser would have set, runs it past Gatekeeper, and confirms the notarisation is stapled. It verifies every detached signature published alongside the artefacts.

This sounds like belt and braces, and it is, but it has already earned its place. Two bugs found whilst building it were exactly the sort that our old checks could never have caught: packages published one directory deeper than the repository metadata claimed, and noarch RPMs filed in a directory where no dnf client would ever look for them. In both cases every file existed, every index parsed and every signature validated; the repository simply did not work. A test that installs the software is the only kind that notices.

What it means if you use pgAdmin

Mostly, it should mean that you notice nothing, which is rather the point of build infrastructure. Behind that, though, a few things have genuinely improved.

Releases are now built by a process that anybody can read, and tested by installing them on nine different Linux distributions before they reach you, rather than by somebody clicking a button and then downloading a package to check it by hand. The container image is published to the GitHub Container Registry at ghcr.io/pgadmin-org/pgadmin4 as well as to Docker Hub, with identical tags and both amd64 and arm64 architectures; Docker Hub remains the canonical home for now, because every tutorial and compose file in the world refers to it, but the registry under the pgAdmin organisation is where we would eventually like everybody to be, not least because it is owned by the project rather than by an individual. The Helm chart has moved outright to oci://ghcr.io/pgadmin-org/pgadmin4-helm, its user base being small enough that moving it breaks far less than moving the image would.

There is also a longer-term benefit that we have not cashed in yet. We have never built Linux packages for anything other than x86_64, largely because adding an architecture meant finding, provisioning and maintaining another machine. That is no longer true, and whilst I am not promising anything on a timescale, arm64 packages are now a question of workflow configuration rather than of hardware procurement.

What it means if you contribute

If you change anything under pkg/, you can now read the thing that will build it, and so can the person reviewing your pull request. The workflows deliberately mirror the make check-* targets, so reproducing a CI failure locally is a matter of finding the matching target rather than guessing at what the build server does differently.

Nightly snapshots are built and published as before, so if you want to test the current state of the development branch without building it yourself, the packages are there, signed, and installable from the snapshot repositories.

One change worth knowing about if you touch the packaging: the Python interpreter that the Windows and macOS packages bundle is now pinned in the tree, in pkg/python-version.txt, and both platforms download exactly that version during the build rather than using whatever happened to be installed on the machine. Bumping Python is therefore a commit, reviewed like any other, rather than a change somebody made on a build agent at some point that nobody can now date. The Linux packages are unaffected and continue to use the distribution's own Python, as they should.

What has not moved, and will not

Two pieces of the build cannot live on hosted runners, and it is worth being straightforward about them. The Windows installer is signed with a certificate on a hardware token, which by definition is plugged into a physical machine, and that machine runs a self-hosted runner. The macOS bundle is signed and notarised with Apple credentials, which the build fetches from an encrypted secret rather than from a keychain that lives somewhere. Publishing to the download site needs SSH keys, and those keys live on one machine which is also a self-hosted runner, reachable only through a wrapper that accepts a fixed list of verbs and will not give anybody a shell.

That is a smaller amount of bespoke infrastructure than we had, and all of it is now documented in the repository rather than in the head of whoever set it up, which was the other thing that needed fixing.

In closing

None of this is glamorous work, and if we have done it properly you will never think about it again. The part I am most pleased with is not any individual workflow but the fact that the whole thing is now in the repository, reviewable, and recoverable: if every machine we own vanished tomorrow, rebuilding the release process would be a matter of reading the files rather than reconstructing what somebody once configured.

Next time I will write about pgbuild, which is the repository that builds PostgreSQL and its dependencies for the Windows and macOS packages, and about the work we have been doing to pin and verify exactly what goes into a pgAdmin release.