My job at pgEdge allows me a significant amount of time to work on pgAdmin, a project I started nearly 30 years ago. The previous blog and this followup is an update on some of the recent work I’ve been doing.

In my last post I wrote about moving pgAdmin's build farm from Jenkins to GitHub Actions, and I promised a follow-up about pgbuild, which is the least famous repository in the pgAdmin organisation and quietly one of the most important. If you have installed pgAdmin on Windows in the last year or so, or on macOS since the move to GitHub Actions, you ran code that came from the buildfarm.

pgbuild_and_Knowing

What pgbuild is for

pgAdmin's desktop packages are self-contained, because that is what people expect on Windows and macOS: you download an installer or a disk image, you run it, and pgAdmin works, without you first having to install PostgreSQL's client libraries, a Kerberos implementation and an SSL library, and without pgAdmin quietly binding itself to whatever versions happen to be on your machine. That convenience has to come from somewhere, and where it comes from is pgbuild, which compiles PostgreSQL itself along with OpenSSL, Kerberos, zstd, lz4, ICU, gettext, libiconv, libxml2, libxslt and a handful of build tools, for Windows on x86_64 and for macOS on both Intel and Apple silicon, and publishes each as an archive that the pgAdmin build then downloads and links against.

It grew out of a repository called winpgbuild, which as the name suggests did this only for Windows, and which itself came out of conversations at PGConf.dev 2025 in Montreal, with Dave Cramer and Andrew Dunstan amongst others, about how useful it would be to have a standardised reference build of PostgreSQL and its dependencies for Windows: something that anybody shipping a PostgreSQL client on that platform could build against, rather than every project solving the same problem privately and slightly differently. The macOS dependencies, meanwhile, were built on the Jenkins farm right up until the migration, by a separate set of jobs with their own conventions, so bringing them into the same repository and having both platforms build against one manifest of pinned upstream versions was one of the tidier outcomes of the whole exercise.

This is unglamorous work, and it is the sort of thing that only becomes interesting when it goes wrong, which is precisely why it is worth thinking about carefully.

The problem we found

Whilst reviewing the new publishing machinery, we took a hard look at the whole path from source to signed artefact, and the weakest link turned out not to be in pgAdmin at all.

Every release in pgbuild was published under a tag ending -latest: openssl-macos-arm64-latest, postgresql-18-windows-x86_64-latest, and so on. Each nightly build moved that tag onto the newest commit and replaced the archive attached to it, which is a perfectly reasonable way to publish "here is the current build", and it is how pgAdmin's own build found its dependencies. The pgAdmin build downloaded those archives, unpacked them, compiled against them, and then signed the result with a certificate on a hardware token or with an Apple Developer ID.

There was no checksum anywhere in that chain, and the tags were mutable, which has two consequences that are worth stating plainly.

The first is a supply chain problem: write access to pgbuild was effectively write access to what pgAdmin signs. Anybody who could alter a release there could change the libraries inside the next pgAdmin build, and nothing downstream would have noticed, because nothing downstream was checking. The people with that access are long term highly trusted PostgreSQL community members, so this was not a gaping hole so much as an unnecessary second door into the same room, but a code signing certificate exists precisely to say "this is what we built", and we could not actually demonstrate that we knew what we had built.

The second is more prosaic and, day to day, more annoying: a pgAdmin build was not reproducible. Building the same commit on two different days could produce two different binaries, because the dependencies were whatever the tags pointed at that morning, and if a regression turned out to come from a dependency there was no way to say which change introduced it or to rebuild without it.

What we have done about it

The fix has two halves, one in each repository, and they only work together.

In pgAdmin, there is now a lock file recording the SHA-256 of every archive the build consumes, and the action that fetches them verifies each download against it before anything is unpacked. If the bytes are not what the lock says, the build fails, which is the behaviour you want from a check whose entire purpose is to notice a substitution. Refreshing the lock is a small tool and a commit, so a dependency change arrives as a reviewable diff naming exactly which dependency moved and what its new digest is, which is an audit trail we previously did not have in any form.

In pgbuild, each release now publishes a checksum alongside the archive and records the upstream version in the release notes, so that what a release contains can be established from the release page rather than by downloading it and hoping. More importantly, alongside the rolling -latest release there is now an immutable release for each upstream version, created once and never touched again, and it is these that pgAdmin pins to. That last part matters more than it might appear: if pgAdmin pinned a digest against a tag whose asset is replaced nightly, then the first time anything was rebuilt the pinned bytes would simply cease to exist, and the build would fail in a way that could only be resolved by abandoning the pin. Keeping the old releases means a pinned build keeps working indefinitely.

There is a deliberate asymmetry in how the two failure modes are treated, and it is worth explaining, because it took a conversation to get right. A digest that does not match fails the build, because that means we did not get what we pinned. Building against a dependency that is merely out of date does not fail the build; it emits a loud warning naming the dependency, the version we are pinned to and the newer one that is available, and then carries on and ships what was pinned. Being behind is a thing somebody should deal with soon, but it is not a reason to stop shipping, and a build system that goes red because somebody else published something is a build system people learn to ignore.

What this means for you

If you use pgAdmin, this is one of those changes that is meant to be invisible: the same packages, built the same way, except that we can now demonstrate that the libraries inside them are the ones we intended rather than merely believing it. If a security advisory lands against OpenSSL or libpq, we can say precisely which version went into which release, which is a question we have been asked before and answered with more hedging than I was comfortable with.

If you contribute to pgAdmin, the visible change is that bumping a dependency is now two steps rather than one: rebuild it in pgbuild, then refresh the lock in pgAdmin and commit the result. That is genuinely more work, and we went into it knowing so; the reason it is worth paying is that the second step is the one that makes the change visible to a reviewer, dated, and attributable, rather than something that happened overnight between two identical commits.

In closing

The theme running through both of these posts is the same, which is that the interesting part of a build system is not how clever it is but how much of it you can see. Moving the build farm into the repository made the process reviewable; pinning the dependencies made its inputs knowable. Neither will make pgAdmin faster or add a single feature, but the next time somebody asks what is actually inside the installer they downloaded, we will be able to tell them exactly, and that seems like a reasonable thing for a project to be able to do.

pgbuild lives at github.com/pgadmin-org/pgbuild, and as with everything else in the project, patches and issues are welcome.