Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# 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

# 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
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 -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)
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
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: |
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."
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
16 changes: 9 additions & 7 deletions build_defs/postgres.build_defs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -256,7 +258,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:
Expand Down Expand Up @@ -291,7 +293,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():
Expand Down
55 changes: 55 additions & 0 deletions release/smoke_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/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 <tarball> <base|ai>
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" <<EOF
listen_addresses = ''
unix_socket_directories = '$WORK'
shared_preload_libraries = '$PRELOAD'
EOF
"$BIN/pg_ctl" -D "$PGDATA" -l "$WORK/postgres.log" start -w > /dev/null || {
cat "$WORK/postgres.log"
exit 1
}

psql() { "$BIN/psql" -h "$WORK" -d postgres --no-psqlrc -v ON_ERROR_STOP=1 -t -A "$@"; }

# 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 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")"
Loading