From f2078c0e9d20e4470cac2abc994117a002eedbb0 Mon Sep 17 00:00:00 2001 From: Jack Marsh Date: Wed, 7 Oct 2026 15:43:24 +0100 Subject: [PATCH 1/3] feat(ci): build, smoke-test and release binaries for linux and macOS Releases were built by hand with release/release.sh, so darwin_arm64 assets exist only where someone happened to build on a Mac: there is no AI build for darwin after 15.x. The Release binaries workflow builds every version under binaries/ on ubuntu-24.04 (pinned: the Linux binaries link the runner's libicu74 and glibc 2.38, so consumers need Ubuntu 24.04 or newer) and macos-14, smoke-tests each tarball (initdb, start, create the extensions it ships), and on a v* tag publishes one release only if every build passed and the tag matches RELEASE_VERSION. make now runs -j: a serial 17.6 AI build is ~16 CPU-minutes, too close to the old 20-minute rule timeout on a CI runner (now 60). RELEASE_VERSION -> v0.0.6, for the first CI-built release. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yaml | 112 +++++++++++++++++++++++++++++++++ README.md | 13 ++++ build_defs/postgres.build_defs | 10 +-- release/smoke_test.sh | 52 +++++++++++++++ 4 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release.yaml create mode 100755 release/smoke_test.sh diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..ac04cd2 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,112 @@ +# Builds every PostgreSQL version under binaries/ for every platform we ship, +# smoke-tests each tarball, and on a version tag publishes them all as one +# release. A release is only created when every build and test passed, so a +# release never carries one platform without the other. +# +# Platforms: +# linux_x86_64 ubuntu-24.04 pinned: the binaries link the runner's libicu +# (74) and glibc (2.38), so they run on Ubuntu +# 24.04 or newer; moving the runner moves that +# darwin_arm64 macos-14 Apple Silicon +# +# To release: bump RELEASE_VERSION in build_defs/postgres.build_defs, merge, +# then push the matching tag (git tag v0.0.N && git push origin v0.0.N). +name: Release binaries + +on: + pull_request: + push: + branches: [master] + tags: ["v*"] + workflow_dispatch: + +permissions: + contents: read + +jobs: + versions: + runs-on: ubuntu-24.04 + outputs: + versions: ${{ steps.list.outputs.versions }} + steps: + - uses: actions/checkout@v4 + - id: list + run: echo "versions=$(ls binaries | jq -R . | jq -sc .)" >> "$GITHUB_OUTPUT" + + build: + needs: versions + strategy: + fail-fast: false + matrix: + version: ${{ fromJSON(needs.versions.outputs.versions) }} + runner: [ubuntu-24.04, macos-14] + runs-on: ${{ matrix.runner }} + name: ${{ matrix.version }} (${{ matrix.runner }}) + steps: + - uses: actions/checkout@v4 + + - name: Install build dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y build-essential flex bison perl pkg-config \ + libicu-dev libreadline-dev zlib1g-dev uuid-dev + + - name: Install build dependencies (macOS) + if: runner.os == 'macOS' + run: | + brew install icu4c readline pkgconf + # postgres.build_defs finds them at these paths; fail here, not mid-configure + test -d /opt/homebrew/opt/icu4c/lib/pkgconfig + test -d /opt/homebrew/opt/readline/lib/pkgconfig + + - name: Build + run: ./pleasew build //binaries/${{ matrix.version }}/... + + - name: Smoke test + run: | + set -euo pipefail + for tarball in $(./pleasew query output //binaries/${{ matrix.version }}/... | grep 'psql-.*\.tar\.gz$'); do + case "$(basename "$tarball")" in + psql-ai-*) variant=ai ;; + *) variant=base ;; + esac + release/smoke_test.sh "$tarball" "$variant" + cp "$tarball" "$RUNNER_TEMP/" + done + + - uses: actions/upload-artifact@v4 + with: + name: psql-${{ matrix.version }}-${{ matrix.runner }} + path: ${{ runner.temp }}/psql-*.tar.gz + if-no-files-found: error + + release: + if: startsWith(github.ref, 'refs/tags/v') + needs: build + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Check the tag matches RELEASE_VERSION + run: | + RELEASE_VERSION=$(sed -n 's/^RELEASE_VERSION = "\(.*\)"$/\1/p' build_defs/postgres.build_defs) + if [[ "$RELEASE_VERSION" != "$GITHUB_REF_NAME" ]]; then + echo "Tag $GITHUB_REF_NAME does not match RELEASE_VERSION $RELEASE_VERSION in build_defs/postgres.build_defs" + exit 1 + fi + + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Publish + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "$GITHUB_REF_NAME" dist/psql-*.tar.gz \ + --title "PostgreSQL Release $GITHUB_REF_NAME" \ + --notes "PostgreSQL binaries for linux_x86_64 and darwin_arm64, built and smoke-tested by the Release binaries workflow." diff --git a/README.md b/README.md index b54c44f..f364d40 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,19 @@ You may need to pass the `-L /path/to/psql/share` flag to `initdb`. You can find all available versions and corresponding binaries on the [releases page](https://github.com/becomeliminal/postgres/releases). +Releases are built by CI, never from a laptop. The +[Release binaries](.github/workflows/release.yaml) workflow builds every +version under `binaries/` on `ubuntu-24.04` (linux_x86_64) and `macos-14` +(darwin_arm64), and smoke-tests each tarball with `release/smoke_test.sh` +(starts postgres, creates the extensions it ships). It runs on every pull +request and push to master. To cut a release: + +1. Bump `RELEASE_VERSION` in `build_defs/postgres.build_defs` and merge. +2. Push the matching tag: `git tag v0.0.N && git push origin v0.0.N`. + +The workflow publishes the release only if every build and smoke test passed, +and refuses a tag that doesn't match `RELEASE_VERSION`. + --- ## Need Additional Pre-Built Binaries? diff --git a/build_defs/postgres.build_defs b/build_defs/postgres.build_defs index 26b7831..1a61ff4 100644 --- a/build_defs/postgres.build_defs +++ b/build_defs/postgres.build_defs @@ -66,7 +66,7 @@ def postgres(name:str, version:str, os=None, arch=None, hashes:list=[], visibili f'mkdir -p {target}/psql', 'cd $SRC', env_prefix + f'./configure --prefix "$HOME/{target}/psql" --with-uuid=e2fs ' + ' '.join(configure_flags), - 'make world-bin', + 'make -j"$(getconf _NPROCESSORS_ONLN)" world-bin', 'make install-world-bin', ] # On macOS, rewrite dylib paths from absolute to relative using @loader_path @@ -98,7 +98,7 @@ def postgres(name:str, version:str, os=None, arch=None, hashes:list=[], visibili "share": [f'{target}/psql/share'], }, cmd = ' && '.join(install_cmds), - build_timeout = 1200, + build_timeout = 3600, building_description = f'Installing PostgreSQL v{version} for {target}...', ) if _compress: @@ -209,7 +209,7 @@ def postgres_ai(name:str, version:str, pgvector_version:str="0.8.0", age_version 'tar -xzf $SRCS_PG', f'cd postgresql-{version}', env_prefix + f'./configure --prefix "{psql_dir}" --with-uuid=e2fs ' + ' '.join(configure_flags), - 'make world-bin', + 'make -j"$(getconf _NPROCESSORS_ONLN)" world-bin', 'make install-world-bin', 'cd $HOME', # Build and install pgvector @@ -256,7 +256,7 @@ def postgres_ai(name:str, version:str, pgvector_version:str="0.8.0", age_version "share": [f'{out_dir}/psql/share'], }, cmd = ' && '.join(install_cmds), - build_timeout = 1200, + build_timeout = 3600, building_description = f'Installing PostgreSQL AI v{version} for {target}...', ) if _compress: @@ -291,7 +291,7 @@ def postgres_ai(name:str, version:str, pgvector_version:str="0.8.0", age_version ) -RELEASE_VERSION = "v0.0.5" +RELEASE_VERSION = "v0.0.6" def _resolve_target(): diff --git a/release/smoke_test.sh b/release/smoke_test.sh new file mode 100755 index 0000000..81f4ab5 --- /dev/null +++ b/release/smoke_test.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Proves a release tarball works on the machine it was built for: unpack it, +# start postgres from it, and create the extensions it claims to ship. +# +# release/smoke_test.sh +set -euo pipefail + +TARBALL="$1" +VARIANT="$2" + +WORK=$(mktemp -d) +PGDATA="$WORK/data" +cleanup() { + "$WORK/psql/bin/pg_ctl" -D "$PGDATA" stop -m immediate > /dev/null 2>&1 || true + rm -rf "$WORK" +} +trap cleanup EXIT + +mkdir -p "$WORK/psql" +tar -xzf "$TARBALL" -C "$WORK/psql" +BIN="$WORK/psql/bin" +# Linux binaries carry no rpath: consumers point the loader at lib/, as this +# does. (macOS binaries find libpq via @executable_path instead.) +export LD_LIBRARY_PATH="$WORK/psql/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + +"$BIN/initdb" -D "$PGDATA" --no-locale --encoding=UTF8 > /dev/null +PRELOAD="pg_stat_statements" +if [[ "$VARIANT" == "ai" ]]; then + PRELOAD="pg_stat_statements,age" +fi +cat >> "$PGDATA/postgresql.conf" < /dev/null || { + cat "$WORK/postgres.log" + exit 1 +} + +psql() { "$BIN/psql" -h "$WORK" -d postgres --no-psqlrc -v ON_ERROR_STOP=1 -t -A "$@"; } + +psql -c 'CREATE EXTENSION "uuid-ossp";' -c 'SELECT uuid_generate_v4();' +psql -c 'CREATE EXTENSION pg_stat_statements;' -c 'SELECT count(*) FROM pg_stat_statements;' +if [[ "$VARIANT" == "ai" ]]; then + psql -c 'CREATE EXTENSION vector;' -c "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector;" + psql -c 'CREATE EXTENSION age;' -c "LOAD 'age';" \ + -c 'SET search_path = ag_catalog, "$user", public;' \ + -c "SELECT create_graph('smoke');" +fi + +echo "PASS: $(basename "$TARBALL")" From 484ceef9b9f0083fd5082f9714b9351c878eda02 Mon Sep 17 00:00:00 2001 From: Jack Marsh Date: Wed, 7 Oct 2026 15:50:26 +0100 Subject: [PATCH 2/3] fix: darwin AI builds for PG16+, and AGE-first smoke test The macOS post-install loop linked each extension *.so to a .dylib with `[ -f "$f" ] && ln ...`. Since PG16 extensions build as .dylib on macOS, so the glob matches nothing, the test fails, the loop exits 1 and the whole install step fails: why no darwin AI build exists after 15.x. The smoke test now creates age before anything else: with age preloaded, AGE 1.5 on PG13 fails other queries ("schema ag_catalog does not exist") until CREATE EXTENSION age has run, as a real consumer does first. A new push to a PR now cancels its previous run. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yaml | 6 ++++++ build_defs/postgres.build_defs | 6 ++++-- release/smoke_test.sh | 9 ++++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ac04cd2..13b6d8a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -23,6 +23,12 @@ on: permissions: contents: read +# A new push to a PR supersedes its previous run (macOS runners are scarce); +# master and tag runs always finish. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: versions: runs-on: ubuntu-24.04 diff --git a/build_defs/postgres.build_defs b/build_defs/postgres.build_defs index 1a61ff4..a528d72 100644 --- a/build_defs/postgres.build_defs +++ b/build_defs/postgres.build_defs @@ -238,8 +238,10 @@ def postgres_ai(name:str, version:str, pgvector_version:str="0.8.0", age_version f'"$bin"; ' f'fi; ' f'done', - # Create .dylib symlinks for extension .so files - f'cd {psql_dir}/lib && for f in *.so; do [ -f "$f" ] && ln -sf "$f" "${{f%.so}}.dylib"; done', + # Create .dylib symlinks for extension .so files. PG16+ builds them + # as .dylib on macOS already, so there may be no *.so: the loop + # must not fail when the glob matches nothing. + f'cd {psql_dir}/lib && for f in *.so; do if [ -f "$f" ]; then ln -sf "$f" "${{f%.so}}.dylib"; fi; done', ] install = build_rule( diff --git a/release/smoke_test.sh b/release/smoke_test.sh index 81f4ab5..2af0386 100755 --- a/release/smoke_test.sh +++ b/release/smoke_test.sh @@ -40,13 +40,16 @@ EOF psql() { "$BIN/psql" -h "$WORK" -d postgres --no-psqlrc -v ON_ERROR_STOP=1 -t -A "$@"; } -psql -c 'CREATE EXTENSION "uuid-ossp";' -c 'SELECT uuid_generate_v4();' -psql -c 'CREATE EXTENSION pg_stat_statements;' -c 'SELECT count(*) FROM pg_stat_statements;' +# AI first: with age preloaded, older AGE releases (1.5 on PG13) fail other +# queries until CREATE EXTENSION age has run, as a real consumer does at once. if [[ "$VARIANT" == "ai" ]]; then - psql -c 'CREATE EXTENSION vector;' -c "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector;" psql -c 'CREATE EXTENSION age;' -c "LOAD 'age';" \ -c 'SET search_path = ag_catalog, "$user", public;' \ -c "SELECT create_graph('smoke');" + psql -c 'CREATE EXTENSION vector;' -c "SELECT '[1,2,3]'::vector <-> '[1,2,4]'::vector;" fi +psql -c 'CREATE EXTENSION "uuid-ossp";' -c 'SELECT uuid_generate_v4();' +psql -c 'CREATE EXTENSION pg_stat_statements;' -c 'SELECT count(*) FROM pg_stat_statements;' + echo "PASS: $(basename "$TARBALL")" From 559874859aecc1ff6b20e04cd31eb07d8044c22d Mon Sep 17 00:00:00 2001 From: Jack Marsh Date: Wed, 7 Oct 2026 15:59:26 +0100 Subject: [PATCH 3/3] ci: retry builds that fail on a network error 40+ jobs downloading plz's tools from GitHub at once sometimes exhaust plz's own retries ("giving up after 5 attempt(s)"). The build step now retries up to three times, with backoff, only when the log shows a network failure; a compile error or a 404 still fails at once. plz-out keeps what already built, so a retry only redoes what failed. apt gets its own retries too. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yaml | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 13b6d8a..8b0fb1f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -54,8 +54,8 @@ jobs: - name: Install build dependencies (Linux) if: runner.os == 'Linux' run: | - sudo apt-get update - sudo apt-get install -y build-essential flex bison perl pkg-config \ + sudo apt-get -o Acquire::Retries=3 update + sudo apt-get -o Acquire::Retries=3 install -y build-essential flex bison perl pkg-config \ libicu-dev libreadline-dev zlib1g-dev uuid-dev - name: Install build dependencies (macOS) @@ -67,7 +67,27 @@ jobs: test -d /opt/homebrew/opt/readline/lib/pkgconfig - name: Build - run: ./pleasew build //binaries/${{ matrix.version }}/... + shell: bash + run: | + # Retries only network failures (40+ jobs downloading at once from + # GitHub sometimes exhaust plz's own retries); a compile error or a + # 404 fails at once. plz-out keeps every target that already built, + # so a retry only redoes what failed. + TRANSIENT='giving up after [0-9]+ attempt|connection reset by peer|TLS handshake timeout|i/o timeout|unexpected EOF|Could not resolve host' + for attempt in 1 2 3; do + set +e + ./pleasew build //binaries/${{ matrix.version }}/... 2>&1 | tee "$RUNNER_TEMP/build.log" + status=${PIPESTATUS[0]} + set -e + if [[ "$status" -eq 0 ]]; then + exit 0 + fi + if [[ "$attempt" -eq 3 ]] || ! grep -qE "$TRANSIENT" "$RUNNER_TEMP/build.log"; then + exit "$status" + fi + echo "::warning::Build attempt $attempt failed on a network error; retrying in $((attempt * 30))s" + sleep $((attempt * 30)) + done - name: Smoke test run: |