diff --git a/.docker/Dockerfile b/.docker/Dockerfile deleted file mode 100644 index 77be8b0..0000000 --- a/.docker/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -ARG POSTGRES_VERSION=17 - -FROM mirror.gcr.io/golang:1.26-alpine AS builder - -WORKDIR /app - -COPY go.mod go.sum* ./ -RUN go mod download 2>/dev/null || true - -COPY . . - -RUN go build -o main . - -FROM mirror.gcr.io/postgres:${POSTGRES_VERSION} - -WORKDIR /opt/app - -COPY --from=builder /app/main ./ - -RUN ./main && rm main diff --git a/.docker/Dockerfile.test b/.docker/Dockerfile.test deleted file mode 100644 index a25f918..0000000 --- a/.docker/Dockerfile.test +++ /dev/null @@ -1,18 +0,0 @@ -FROM docker.io/golang:1.26-alpine AS builder - -WORKDIR /app - -COPY go.mod go.sum* ./ -RUN go mod download 2>/dev/null || true - -COPY . . - -RUN go build -o main . - -FROM docker.io/postgres:13 - -WORKDIR /opt/app - -COPY --from=builder /app/main ./ - -RUN ./main && rm main diff --git a/.docker/runtime-test.sh b/.docker/runtime-test.sh deleted file mode 100644 index a3fb2b5..0000000 --- a/.docker/runtime-test.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -POSTGRES_VERSION="${POSTGRES_VERSION:-18}" -IMAGE="${IMAGE:-postgres-extensor-runtime-test}" -CONTAINER_NAME="${CONTAINER_NAME:-postgres-extensor-runtime-test-$$}" - -trap 'docker rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true' EXIT - -if [ "${SKIP_BUILD:-false}" != "true" ]; then - docker build -f .docker/Dockerfile -t "${IMAGE}" --build-arg POSTGRES_VERSION="${POSTGRES_VERSION}" . -fi - -docker run -d --rm --name "${CONTAINER_NAME}" -e POSTGRES_PASSWORD=postgres "${IMAGE}" >/dev/null - -echo "Waiting for PostgreSQL to start..." -until docker exec "${CONTAINER_NAME}" pg_isready -U postgres >/dev/null 2>&1; do - sleep 1 -done - -echo "Running SQL tests..." -docker exec -i "${CONTAINER_NAME}" psql -v ON_ERROR_STOP=1 -U postgres < .docker/runtime-test.sql -echo "Tests passed successfully!" diff --git a/.docker/runtime-test.sql b/.docker/runtime-test.sql deleted file mode 100644 index af4ec3c..0000000 --- a/.docker/runtime-test.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE EXTENSION IF NOT EXISTS vector; -CREATE EXTENSION IF NOT EXISTS pgmq; -CREATE EXTENSION IF NOT EXISTS postgis; - -SELECT extname FROM pg_extension WHERE extname IN ('vector','pgmq','postgis'); -SELECT name FROM pg_available_extensions WHERE name IN ('pg_stat_statements'); diff --git a/.docs/extensions.es.md b/.docs/extensions.es.md new file mode 100644 index 0000000..744a8ad --- /dev/null +++ b/.docs/extensions.es.md @@ -0,0 +1,107 @@ +# Extensiones + +> [Read in English](./extensions.md) + +Cada extensión tiene dos partes: + +1. **Cómo se instala**, en el [`Dockerfile`](../Dockerfile). +2. **Cómo se prueba**, en `extensions//test.sql`. + +No hay más código que tocar. + +## Añadir una extensión + +### Opción A: está empaquetada en PGDG (preferida) + +Busca `postgresql--` en [apt.postgresql.org](https://apt.postgresql.org/pub/repos/apt/pool/main/). Tiene que existir para todas las versiones soportadas. + +Declara su versión arriba del todo en el Dockerfile. El comentario `# renovate:` le indica a Renovate el índice de PGDG, para que abra un PR cuando se empaquete una versión nueva: + +```dockerfile +# renovate: datasource=deb depName=postgresql-18-mi-extension registryUrl=https://apt.postgresql.org/pub/repos/apt?suite=trixie-pgdg&components=main&binaryArch=amd64 +ARG MI_EXTENSION_VERSION=1.2.3 +``` + +`suite` es la versión de Debian de la imagen base (hoy `trixie`) seguida de `-pgdg`. Puedes comprobarla con `docker run --rm postgres:18 cat /etc/os-release`. + +Después, añádela fijada al `apt-get install` de la etapa final, y a los labels: + +```dockerfile +ARG MI_EXTENSION_VERSION +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + "postgresql-${PG_MAJOR}-postgis-3=${POSTGIS_VERSION}+*" \ + "postgresql-${PG_MAJOR}-mi-extension=${MI_EXTENSION_VERSION}*" \ + && rm -rf /var/lib/apt/lists/* +LABEL ... \ + io.github.ruxwez.postgres.mi-extension.version="${MI_EXTENSION_VERSION}" +``` + +El `*` final cubre la revisión de Debian (`1.2.3-1.pgdg13+1`). Usa `+*` cuando la versión del paquete lleve `+dfsg` o algo parecido, como pasa con PostGIS. Puedes ver las versiones disponibles con: + +```sh +docker run --rm postgres:18 bash -c 'apt-get update -qq && apt-cache madison postgresql-18-mi-extension' +``` + +PGDG suele mantener varias versiones anteriores en su índice, así que la versión fijada sigue funcionando un tiempo después de que salga una nueva. Cuando una versión desaparece del índice, el build falla con `Version '…' was not found` hasta que se hace merge del PR de Renovate. + +La imagen oficial base define `PG_MAJOR`. + +### Opción B: compilarla desde el código fuente + +Declara su versión arriba del todo en el Dockerfile, junto a las demás: + +```dockerfile +# renovate: datasource=github-tags depName=owner/mi-extension +ARG MI_EXTENSION_VERSION=1.2.3 +``` + +El comentario `# renovate:` justo encima del `ARG` permite que Renovate abra un PR cada vez que salga un tag nuevo. Mantén el mismo formato. + +Añade una etapa encima de `build-base`. Tiene que instalar en `/out` (con `DESTDIR`): + +```dockerfile +FROM build-base AS mi-extension +ARG MI_EXTENSION_VERSION +RUN git clone --depth 1 --branch "v${MI_EXTENSION_VERSION}" https://github.com/owner/mi-extension.git /src \ + && make -C /src \ + && make -C /src install DESTDIR=/out +``` + +En la etapa final, cópiala y añade su versión a los labels: + +```dockerfile +ARG MI_EXTENSION_VERSION +COPY --from=mi-extension /out/ / +LABEL ... \ + io.github.ruxwez.postgres.mi-extension.version="${MI_EXTENSION_VERSION}" +``` + +Si el build necesita más paquetes, añádelos a `build-base`. No llegan a la imagen final. + +### Después, en ambos casos + +Crea `extensions//test.sql`. Tiene que **usar** la extensión, no solo crearla, y lanzar un error si el resultado es incorrecto: + +```sql +CREATE EXTENSION mi_extension; + +DO $$ +BEGIN + IF mi_funcion(1) <> 2 THEN + RAISE EXCEPTION 'mi_extension: resultado inesperado'; + END IF; +END $$; +``` + +`tests/run.sh` lo detecta automáticamente. Por último, añade la extensión a la tabla del README. + +## Actualizar una extensión + +Cambia su `ARG *_VERSION` arriba del todo en el Dockerfile. Renovate abre estos PRs por ti, tanto para los paquetes apt como para las extensiones que se compilan. + +Una vez hecho el merge, el cambio solo llega a `18` y `latest`. Para llevarlo a `18-v1`, saca una release (en [Releases](./releases.es.md) se explica qué número subir). + +## Añadir o quitar una versión de PostgreSQL + +Edita la lista `major` en [`docker-bake.hcl`](../docker-bake.hcl). Para mover `:latest` a una versión nueva, cambia también `LATEST`. El CI construye y prueba todas las versiones de esa lista, así que si alguna extensión no está disponible para una versión, el build falla. diff --git a/.docs/extensions.md b/.docs/extensions.md new file mode 100644 index 0000000..d5fea3e --- /dev/null +++ b/.docs/extensions.md @@ -0,0 +1,107 @@ +# Extensions + +> [Leer en español](./extensions.es.md) + +Every extension has two parts: + +1. **How it's installed**, in the [`Dockerfile`](../Dockerfile). +2. **How it's tested**, in `extensions//test.sql`. + +There is no other code to touch. + +## Adding an extension + +### Option A: it's packaged in PGDG (preferred) + +Look for `postgresql--` on [apt.postgresql.org](https://apt.postgresql.org/pub/repos/apt/pool/main/). It has to exist for every supported version. + +Declare its version at the top of the Dockerfile. The `# renovate:` comment points Renovate at the PGDG index, so it opens a PR when a new version is packaged: + +```dockerfile +# renovate: datasource=deb depName=postgresql-18-my-extension registryUrl=https://apt.postgresql.org/pub/repos/apt?suite=trixie-pgdg&components=main&binaryArch=amd64 +ARG MY_EXTENSION_VERSION=1.2.3 +``` + +`suite` is the Debian release of the base image (`trixie` today) followed by `-pgdg`. Check it with `docker run --rm postgres:18 cat /etc/os-release`. + +Then add it to the `apt-get install` in the final stage, pinned, and to the labels: + +```dockerfile +ARG MY_EXTENSION_VERSION +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + "postgresql-${PG_MAJOR}-postgis-3=${POSTGIS_VERSION}+*" \ + "postgresql-${PG_MAJOR}-my-extension=${MY_EXTENSION_VERSION}*" \ + && rm -rf /var/lib/apt/lists/* +LABEL ... \ + io.github.ruxwez.postgres.my-extension.version="${MY_EXTENSION_VERSION}" +``` + +The trailing `*` matches the Debian revision (`1.2.3-1.pgdg13+1`). Use `+*` when the package version contains `+dfsg` or similar, as PostGIS does. You can list the available versions with: + +```sh +docker run --rm postgres:18 bash -c 'apt-get update -qq && apt-cache madison postgresql-18-my-extension' +``` + +PGDG usually keeps a few previous versions in its index, so the pin keeps working for a while after a new release. Once a version is dropped from the index, the build fails with `Version '…' was not found` until the Renovate PR is merged. + +`PG_MAJOR` is set by the official base image. + +### Option B: build it from source + +Declare its version at the top of the Dockerfile, next to the others: + +```dockerfile +# renovate: datasource=github-tags depName=owner/my-extension +ARG MY_EXTENSION_VERSION=1.2.3 +``` + +The `# renovate:` comment right above the `ARG` lets Renovate open a PR whenever a new tag is released. Keep the same format. + +Add a stage on top of `build-base`. It must install into `/out` (via `DESTDIR`): + +```dockerfile +FROM build-base AS my-extension +ARG MY_EXTENSION_VERSION +RUN git clone --depth 1 --branch "v${MY_EXTENSION_VERSION}" https://github.com/owner/my-extension.git /src \ + && make -C /src \ + && make -C /src install DESTDIR=/out +``` + +In the final stage, copy it over and add its version to the labels: + +```dockerfile +ARG MY_EXTENSION_VERSION +COPY --from=my-extension /out/ / +LABEL ... \ + io.github.ruxwez.postgres.my-extension.version="${MY_EXTENSION_VERSION}" +``` + +If the build needs extra packages, add them to `build-base`. They won't reach the final image. + +### Then, in both cases + +Create `extensions//test.sql`. It should **use** the extension, not only create it, and raise an error if the result is wrong: + +```sql +CREATE EXTENSION my_extension; + +DO $$ +BEGIN + IF my_function(1) <> 2 THEN + RAISE EXCEPTION 'my_extension: unexpected result'; + END IF; +END $$; +``` + +`tests/run.sh` picks it up automatically. Finally, add the extension to the table in the README. + +## Upgrading an extension + +Change its `ARG *_VERSION` at the top of the Dockerfile. Renovate opens these PRs for you, for both apt packages and extensions built from source. + +Once merged, the change only reaches `18` and `latest`. To ship it to `18-v1`, cut a release (see [Releases](./releases.md) to pick the version bump). + +## Adding or dropping a PostgreSQL version + +Edit the `major` list in [`docker-bake.hcl`](../docker-bake.hcl). To move `:latest` to a new major, change `LATEST` too. CI builds and tests every version in that list, so a version where some extension isn't available fails the build. diff --git a/.docs/playgrounds.es.md b/.docs/playgrounds.es.md deleted file mode 100644 index 5a000ef..0000000 --- a/.docs/playgrounds.es.md +++ /dev/null @@ -1,170 +0,0 @@ -# Playgrounds - -> [Read in English](./playgrounds.md) - -Un **playground** es la unidad fundamental de instalación de extensiones. Cada playground representa un paso en el pipeline que transforma una imagen base `postgres:N` en una imagen con extensiones preinstaladas. - ---- - -## Arquitectura - -Cada playground se define en el paquete `extensions/` y se registra en la variable global `Playgrounds` (`extensions/extensions.go`), que es un slice ordenado de `[]*plyd.Playground`. - -### Estructura de un Playground - -```go -type Playground struct { - Name string - handlers HandlersList -} -``` - -Cada playground contiene una lista de **handlers**, donde cada handler asocia un **rango de versiones semver** con una **función ejecutora**: - -```go -type Handler struct { - Range string // Ej: ">=14.0.0, <19.0.0" - Runner HandlerFunc // func(ins *instance.InstanceInfo) error -} -``` - -### Ejecución (`Run`) - -```go -func (p *Playground) Run(ins *instance.InstanceInfo) { - handler := p.matchHandler(ins.PostgreSQL.Version) - if handler == nil { - log.Printf("[WARNING] No handler for PG %s in playground '%s'", ...) - return // No es fatal — el playground simplemente se salta - } - handler.Runner(ins) -} -``` - ---- - -## Versionado - -El versionado usa **semver 2.0** mediante la librería `github.com/Masterminds/semver/v3`. - -### Flujo de matching - -1. Se detecta la versión de PostgreSQL ejecutando `postgres --version`. -2. Se normaliza a semver canónico (ej: `"17"` → `"17.0.0"`). -3. `matchHandler` recorre los handlers en orden: - - Primero busca coincidencia exacta de string. - - Luego evalúa cada rango como `semver.Constraint`. -4. Retorna el primer handler que coincida. - -### Orden de handlers - -Los handlers se ordenan automáticamente en `NewHandlersList`: -- **Versiones exactas primero** (ej: `"17.2.0"`), ascendente. -- **Rangos después** (ej: `">=14.0.0, <19.0.0"`), orden alfabético. - -Esto permite tener overrides específicos por versión antes de caer en rangos genéricos. - -### Ejemplo - -```go -var miExtension = plyd.New("mi-extension", - plyd.NewHandlersList( - plyd.NewHandler(">=16.0.0", func(ins *instance.InstanceInfo) error { - return plyd.Run("apt-get install -y mi-extension") - }), - plyd.NewHandler(">=14.0.0, <16.0.0", func(ins *instance.InstanceInfo) error { - return plyd.Run("apt-get install -y mi-extension-old") - }), - ), -) -``` - ---- - -## Playgrounds por defecto (orden de ejecución) - -El orden en `extensions/extensions.go` es crítico: - -| # | Playground | Archivo | Propósito | -|---|-----------|---------|-----------| -| 1 | `build-deps-install` | `builddeps_install.go` | Instala `git`, `make`, `gcc`, `postgresql-server-dev-MAYOR` | -| 2 | `pgmq` | `pgmq.go` | Clona, compila e instala [PGMQ](https://github.com/pgmq/pgmq) v1.11.1 | -| 3 | `pgvector` | `pgvector.go` | Clona, compila e instala [pgvector](https://github.com/pgvector/pgvector) v0.8.2 | -| 4 | `postgis` | `postgis.go` | Instala `postgresql-MAYOR-postgis-3` vía apt | -| 5 | `postgres-contrib` | `postgrescontrib.go` | Instala `postgresql-MAYOR` (contrib) vía apt | -| 6 | `build-deps-remove` | `builddeps_remove.go` | Desinstala dependencias de build y limpia apt | - -### ¿Por qué este orden? - -1. **Primero** se instalan las herramientas de build (necesarias para compilar pgmq y pgvector desde fuente). -2. **Luego** se compilan e instalan las extensiones que requieren build desde fuente (pgmq, pgvector). -3. **Después** se instalan las extensiones vía apt (postgis, contrib), que necesitan `postgresql-server-dev-MAYOR` ya presente. -4. **Al final** se eliminan las herramientas de build para mantener la imagen final ligera. - ---- - -## Versiones PostgreSQL soportadas - -Actualmente: **14, 15, 16, 17, 18** (y `latest` que apunta a la más reciente). - -Todos los playgrounds usan el rango `>=14.0.0, <19.0.0`. Para ampliar el soporte (ej: PG 19), basta con actualizar los rangos. - ---- - -## Cómo crear un nuevo playground - -### 1. Crear el archivo de extensión - -En `extensions/`, crea un archivo como `miextension.go`: - -```go -package extensions - -import ( - "postgres-extensor/instance" - "postgres-extensor/plyd" -) - -var miExtension = plyd.New("mi-extension", - plyd.NewHandlersList( - plyd.NewHandler(">=14.0.0, <19.0.0", func(ins *instance.InstanceInfo) error { - return plyd.Run("apt-get install -y postgresql-" + ins.PostgreSQL.Mayor + "-mi-extension") - }), - ), -) -``` - -### 2. Registrarlo en el orden correcto - -Edita `extensions/extensions.go` y agrégalo al slice `Playgrounds` en la posición adecuada: - -```go -var Playgrounds = []*plyd.Playground{ - buildDepsInstall, - pgmq, - pgvector, - miExtension, // <-- Nuevo - postgis, - postgresContrib, - buildDepsRemove, -} -``` - -### 3. Verificar la instalación - -Tu handler debería verificar que la extensión quedó instalada: - -```go -verification := "test -f $(pg_config --sharedir)/extension/mi_extension.control" -``` - -### 4. Agregar cobertura en tests - -Edita `.docker/runtime-test.sql` para incluir la nueva extensión. - -### 5. Build y test - -```sh -make build PG_VERSION=18 -make runtime-test PG_VERSION=18 -``` diff --git a/.docs/playgrounds.md b/.docs/playgrounds.md deleted file mode 100644 index 2dcad1c..0000000 --- a/.docs/playgrounds.md +++ /dev/null @@ -1,170 +0,0 @@ -# Playgrounds - -> [Leer en español](./playgrounds.es.md) - -A **playground** is the fundamental unit of extension installation. Each playground represents a step in the pipeline that transforms a `postgres:N` base image into an image with pre-installed extensions. - ---- - -## Architecture - -Each playground is defined in the `extensions/` package and registered in the global `Playgrounds` slice (`extensions/extensions.go`), an ordered `[]*plyd.Playground`. - -### Playground struct - -```go -type Playground struct { - Name string - handlers HandlersList -} -``` - -Each playground contains a list of **handlers**, where each handler associates a **semver version range** with an **executor function**: - -```go -type Handler struct { - Range string // e.g. ">=14.0.0, <19.0.0" - Runner HandlerFunc // func(ins *instance.InstanceInfo) error -} -``` - -### Execution (`Run`) - -```go -func (p *Playground) Run(ins *instance.InstanceInfo) { - handler := p.matchHandler(ins.PostgreSQL.Version) - if handler == nil { - log.Printf("[WARNING] No handler for PG %s in playground '%s'", ...) - return // Non-fatal — the playground simply skips - } - handler.Runner(ins) -} -``` - ---- - -## Versioning - -Versioning uses **semver 2.0** via `github.com/Masterminds/semver/v3`. - -### Matching flow - -1. The PostgreSQL version is detected by running `postgres --version`. -2. It is normalized to canonical semver (e.g. `"17"` → `"17.0.0"`). -3. `matchHandler` iterates over handlers: - - First tries exact string match. - - Then evaluates each range as a `semver.Constraint`. -4. Returns the first matching handler. - -### Handler ordering - -Handlers are automatically sorted in `NewHandlersList`: -- **Exact versions first** (e.g. `"17.2.0"`), ascending. -- **Ranges after** (e.g. `">=14.0.0, <19.0.0"`), alphabetical order. - -This allows version-specific overrides before falling back to generic ranges. - -### Example - -```go -var myExtension = plyd.New("my-extension", - plyd.NewHandlersList( - plyd.NewHandler(">=16.0.0", func(ins *instance.InstanceInfo) error { - return plyd.Run("apt-get install -y my-extension") - }), - plyd.NewHandler(">=14.0.0, <16.0.0", func(ins *instance.InstanceInfo) error { - return plyd.Run("apt-get install -y my-extension-old") - }), - ), -) -``` - ---- - -## Default playgrounds (execution order) - -The order in `extensions/extensions.go` is critical: - -| # | Playground | File | Purpose | -|---|-----------|------|---------| -| 1 | `build-deps-install` | `builddeps_install.go` | Installs `git`, `make`, `gcc`, `postgresql-server-dev-MAJOR` | -| 2 | `pgmq` | `pgmq.go` | Clones, compiles and installs [PGMQ](https://github.com/pgmq/pgmq) v1.11.1 | -| 3 | `pgvector` | `pgvector.go` | Clones, compiles and installs [pgvector](https://github.com/pgvector/pgvector) v0.8.2 | -| 4 | `postgis` | `postgis.go` | Installs `postgresql-MAJOR-postgis-3` via apt | -| 5 | `postgres-contrib` | `postgrescontrib.go` | Installs `postgresql-MAJOR` (contrib) via apt | -| 6 | `build-deps-remove` | `builddeps_remove.go` | Removes build dependencies and cleans apt | - -### Why this order? - -1. **First** install build tools (needed to compile pgmq and pgvector from source). -2. **Then** compile and install extensions that require source builds (pgmq, pgvector). -3. **After that** install extensions via apt (postgis, contrib), which need `postgresql-server-dev-MAJOR` present. -4. **Finally** remove build tools to keep the final image lightweight. - ---- - -## Supported PostgreSQL versions - -Currently: **14, 15, 16, 17, 18** (and `latest`, which points to the newest). - -All playgrounds use the range `>=14.0.0, <19.0.0`. To extend support (e.g., PG 19), just update the ranges. - ---- - -## How to create a new playground - -### 1. Create the extension file - -In `extensions/`, create a file like `myextension.go`: - -```go -package extensions - -import ( - "postgres-extensor/instance" - "postgres-extensor/plyd" -) - -var myExtension = plyd.New("my-extension", - plyd.NewHandlersList( - plyd.NewHandler(">=14.0.0, <19.0.0", func(ins *instance.InstanceInfo) error { - return plyd.Run("apt-get install -y postgresql-" + ins.PostgreSQL.Mayor + "-my-extension") - }), - ), -) -``` - -### 2. Register it in the correct order - -Edit `extensions/extensions.go` and add it to the `Playgrounds` slice at the right position: - -```go -var Playgrounds = []*plyd.Playground{ - buildDepsInstall, - pgmq, - pgvector, - myExtension, // <-- New - postgis, - postgresContrib, - buildDepsRemove, -} -``` - -### 3. Verify installation - -Your handler should verify the extension was installed: - -```go -verification := "test -f $(pg_config --sharedir)/extension/my_extension.control" -``` - -### 4. Add test coverage - -Edit `.docker/runtime-test.sql` to include the new extension. - -### 5. Build and test - -```sh -make build PG_VERSION=18 -make runtime-test PG_VERSION=18 -``` diff --git a/.docs/releases.es.md b/.docs/releases.es.md new file mode 100644 index 0000000..e406015 --- /dev/null +++ b/.docs/releases.es.md @@ -0,0 +1,61 @@ +# Releases + +> [Read in English](./releases.md) + +## Tags + +Cada build publica los tags móviles. Una release publica además los que llevan versión: + +| Tag | Lo publica | ¿Cambia? | +|-----|------------|----------| +| `18-v1.4.0` | la release `v1.4.0` | Nunca. El CI se niega a publicar una release que ya existe. | +| `18-v1` | cada release `v1.x.y` | Pasa a la release `v1` más reciente. | +| `18`, `latest` | cada build (push a `main`, semanal, release) | Siempre el último build. | + +El rebuild semanal (parches de seguridad de la imagen base) solo mueve `18` y `latest`. Los tags con versión solo cambian cuando sacas una release. + +## Versionado + +La versión describe **lo que contiene la imagen**, siguiendo semver desde el punto de vista de quien la usa: + +| Subida | Cuándo | Ejemplos | +|--------|--------|----------| +| **patch** `v1.4.0 → v1.4.1` | Mismas versiones de extensiones, reconstruidas o con un arreglo | Llevar a `18-v1` los parches de seguridad de la imagen base o una nueva versión menor de PostgreSQL; arreglar algo del build | +| **minor** `v1.4.x → v1.5.0` | Cambios compatibles | Una extensión recibe una actualización compatible (pgvector 0.8.2 → 0.8.7); se añade una extensión; se añade una versión mayor de PostgreSQL | +| **major** `v1.x.y → v2.0.0` | Algo puede romperse | Una extensión sube de versión mayor (pgmq 1.x → 2.x); se quita una extensión o una versión mayor de PostgreSQL | + +Si no tienes claro si una actualización es compatible, lee el changelog de la extensión. Ante la duda, sube la major: una actualización nunca debería romperle nada a quien usa `-v1`. + +## Publicar una release + +Desde `main` actualizado: + +```sh +git tag v1.4.0 +git push origin v1.4.0 +``` + +O crea la release desde la interfaz de GitHub con un tag nuevo. Después, el CI: + +1. Comprueba que el tag tiene la forma `vX.Y.Z` y que es el tag de release **más reciente**. Publicar una versión antigua después de una más nueva haría retroceder `18-v1` y `latest`. +2. Comprueba que ninguno de los tags `-vX.Y.Z` existe todavía en Docker Hub. +3. Construye y prueba todas las versiones de PostgreSQL en ambas arquitecturas. No se publica nada si alguna falla. +4. Publica `-vX.Y.Z`, `-vX`, `` y `latest`. +5. Crea la release de GitHub con el changelog automático y una tabla con las versiones exactas de cada imagen. Si escribiste tú las notas de la release, la tabla se añade al final. + +Si un job de publicación falla a medias (por ejemplo, por un error de Docker Hub), usa **Re-run failed jobs**. Si relanzas todo el workflow, se detendrá en el paso 2, porque algunos tags ya existen. + +## Labels + +Cada imagen lleva sus versiones como labels: + +```sh +docker inspect ruxwez/postgres:18-v1 --format '{{json .Config.Labels}}' +``` + +| Label | Valor | +|-------|-------| +| `org.opencontainers.image.version` | release del proyecto (`1.4.0`); vacío fuera de las releases | +| `org.opencontainers.image.revision` | commit de git | +| `io.github.ruxwez.postgres.postgresql.version` | versión del paquete de PostgreSQL | +| `io.github.ruxwez.postgres..version` | versión de cada extensión (pgvector, pgmq, postgis) | diff --git a/.docs/releases.md b/.docs/releases.md new file mode 100644 index 0000000..2afb985 --- /dev/null +++ b/.docs/releases.md @@ -0,0 +1,61 @@ +# Releases + +> [Leer en español](./releases.es.md) + +## Tags + +Every build publishes the floating tags. A release also publishes the versioned ones: + +| Tag | Published by | Changes? | +|-----|--------------|----------| +| `18-v1.4.0` | release `v1.4.0` | Never. CI refuses to publish a release that already exists. | +| `18-v1` | every `v1.x.y` release | Moves to the newest `v1` release. | +| `18`, `latest` | every build (push to `main`, weekly, release) | Always the latest build. | + +The weekly rebuild (security fixes in the base image) only moves `18` and `latest`. Versioned tags change only when you cut a release. + +## Versioning + +The version describes **what the image contains**, following semver from the user's point of view: + +| Bump | When | Examples | +|------|------|----------| +| **patch** `v1.4.0 → v1.4.1` | Same extension versions, rebuilt or fixed | Ship the base image's security fixes or a new PostgreSQL minor to `18-v1`; fix something in the build | +| **minor** `v1.4.x → v1.5.0` | Compatible changes | An extension gets a compatible update (pgvector 0.8.2 → 0.8.7); a new extension is added; a new PostgreSQL major is added | +| **major** `v1.x.y → v2.0.0` | Something can break | An extension gets a major update (pgmq 1.x → 2.x); an extension or a PostgreSQL major is removed | + +If you're unsure whether an upgrade is compatible, read the extension's changelog. When in doubt, bump the major: a `-v1` user should never be broken by an update. + +## Publishing a release + +From an up-to-date `main`: + +```sh +git tag v1.4.0 +git push origin v1.4.0 +``` + +Or create the release from the GitHub UI with a new tag. CI then: + +1. Checks the tag looks like `vX.Y.Z` and is the **newest** release tag. Releasing an older version after a newer one would move `18-v1` and `latest` backwards. +2. Checks that none of the `-vX.Y.Z` tags exist on Docker Hub yet. +3. Builds and tests every PostgreSQL version on both architectures. Nothing is published if any fails. +4. Publishes `-vX.Y.Z`, `-vX`, `` and `latest`. +5. Creates the GitHub release with the automatic changelog and a table of the exact versions in each image. If you wrote release notes yourself, the table is appended to them. + +If a publish job fails halfway (e.g. a Docker Hub error), use **Re-run failed jobs**. A full re-run would stop at step 2, because some tags already exist. + +## Labels + +Every image carries its versions as labels: + +```sh +docker inspect ruxwez/postgres:18-v1 --format '{{json .Config.Labels}}' +``` + +| Label | Value | +|-------|-------| +| `org.opencontainers.image.version` | project release (`1.4.0`), empty outside releases | +| `org.opencontainers.image.revision` | git commit | +| `io.github.ruxwez.postgres.postgresql.version` | PostgreSQL package version | +| `io.github.ruxwez.postgres..version` | version of each extension (pgvector, pgmq, postgis) | diff --git a/.docs/tests.es.md b/.docs/tests.es.md index cac21b1..b13b4cc 100644 --- a/.docs/tests.es.md +++ b/.docs/tests.es.md @@ -2,83 +2,40 @@ > [Read in English](./tests.md) -Este proyecto utiliza **tests de integración** en lugar de tests unitarios de Go (`_test.go`). La razón es que el propósito del binario es ejecutarse dentro de una imagen Docker en tiempo de build, interactuando directamente con el sistema operativo (apt, git, make, pg_config, etc.). No hay lógica de negocio aislable que justifique tests unitarios tradicionales. +Los tests comprueban la **imagen final**: arrancan un contenedor y usan cada extensión dentro de un PostgreSQL real. ---- - -## `make test` — Build test +## Cómo ejecutarlos ```sh -make test -``` - -Construye la imagen Docker usando `.docker/Dockerfile.test`. Este Dockerfile ejecuta el binario `./main` dentro de un contenedor `postgres:13` y verifica que todo el pipeline de instalación de extensiones compile y se ejecute sin errores. Al finalizar, la imagen se elimina. - -- **Propósito:** Validación rápida de que el código compila y los playgrounds no fallan. -- **Limitación:** Usa PostgreSQL 13 fijo, independientemente de `PG_VERSION`. - ---- - -## `make runtime-test` — Runtime test +make test # construye PG 18 para tu plataforma y lo prueba +make test PG_VERSION=14 -```sh -make runtime-test # PG_VERSION=18 por defecto -PG_VERSION=17 make runtime-test +./tests/run.sh # prueba una imagen ya construida ``` -Ejecuta el script `.docker/runtime-test.sh`, que: - -1. Construye la imagen Docker de producción (`.docker/Dockerfile`). -2. Inicia un contenedor con `POSTGRES_PASSWORD=postgres`. -3. Espera a que PostgreSQL esté listo (`pg_isready`). -4. Ejecuta `.docker/runtime-test.sql` contra la base de datos vía `psql`. - -El script SQL verifica: +## Qué hace `tests/run.sh` -```sql -CREATE EXTENSION IF NOT EXISTS vector; -CREATE EXTENSION IF NOT EXISTS pgmq; -CREATE EXTENSION IF NOT EXISTS postgis; +1. Arranca la imagen con `POSTGRES_PASSWORD=postgres`. +2. Espera a que PostgreSQL acepte conexiones TCP. El servidor temporal de inicialización del entrypoint solo escucha en el socket unix, así que la espera no termina antes de tiempo. +3. Para cada `extensions/*/test.sql`, crea una base de datos nueva, ejecuta el archivo con `ON_ERROR_STOP=1` e imprime `PASS` (con las versiones instaladas) o `FAIL` (con el error). +4. Termina con un código distinto de cero si algún test falla, después de ejecutarlos todos. --- Confirma que las extensiones están instaladas -SELECT * FROM pg_extension WHERE extname IN ('vector', 'pgmq', 'postgis'); - --- Confirma que contrib está disponible -SELECT * FROM pg_available_extensions WHERE name = 'pg_stat_statements'; ``` - -- **Propósito:** Validación real de que todas las extensiones se instalan correctamente y son funcionales dentro de PostgreSQL. -- **Versiones:** Se ejecuta contra la versión de PostgreSQL indicada por `PG_VERSION`. - ---- - -## CI — GitHub Actions - -En `pr-test.yml`, cada Pull Request ejecuta la runtime test matrix para todas las versiones soportadas (`14`, `15`, `16`, `17`, `18`, `latest`) en `linux/amd64`. Esto asegura que cualquier cambio funcione en todo el rango de versiones objetivo. - ---- - -## Cómo ampliar los tests - -### Agregar verificación de una extensión nueva - -Edita `.docker/runtime-test.sql`: - -```sql -CREATE EXTENSION IF NOT EXISTS mi_extension; -SELECT * FROM pg_extension WHERE extname = 'mi_extension'; +PASS contrib (pg_stat_statements 1.12, pg_trgm 1.6) +PASS pgmq (pgmq 1.11.1) +PASS pgvector (vector 0.8.2) +PASS postgis (postgis 3.6.4) +All tests passed. ``` -### Agregar un nuevo paso en runtime-test.sh - -Si necesitas lógica adicional antes o después de los tests SQL (por ejemplo, copiar archivos, verificar permisos), edita `.docker/runtime-test.sh`. - -### Tests unitarios (futuro) +Para probar una extensión nueva, añade su `test.sql` (ver [Extensiones](./extensions.es.md)). No hace falta registrarlo en ningún sitio. -Si en el futuro se extrae lógica reutilizable (por ejemplo, el parseo de versiones o el matching de handlers), se pueden agregar archivos `_test.go` en los paquetes correspondientes y ejecutarlos con: +## CI -```sh -go test ./... -``` +[`.github/workflows/ci.yml`](../.github/workflows/ci.yml) se ejecuta en cada PR, en `main`, en los tags `vX.Y.Z`, cada semana y manualmente: -Actualmente no existen porque la lógica del proyecto está fuertemente acoplada al entorno Docker. +1. **Prepare:** lee las versiones de `docker-bake.hcl`. Si es un tag de release, comprueba además que la release es válida y que no está publicada todavía (ver [Releases](./releases.es.md)). +2. **Build y test** de cada versión en `amd64` y `arm64`, con runners nativos (sin emulación). +3. **All builds passed:** un único check, para la protección de rama. +4. **Publish** (no en PRs): sube exactamente las imágenes que se probaron y crea los tags como manifests multi-arquitectura. No se publica nada si alguna versión falla en cualquiera de las dos arquitecturas. +5. **GitHub release** (solo en tags de release): crea la release con las versiones que reportan los tests. diff --git a/.docs/tests.md b/.docs/tests.md index eac2b2f..1ff573c 100644 --- a/.docs/tests.md +++ b/.docs/tests.md @@ -2,83 +2,40 @@ > [Leer en español](./tests.es.md) -This project uses **integration tests** rather than Go unit tests (`_test.go`). The binary runs inside a Docker image at build time, interacting directly with the OS (apt, git, make, pg_config, etc.). There is no isolatable business logic that would benefit from traditional unit tests. +The tests check the **final image**: they start a container and use each extension inside a real PostgreSQL instance. ---- - -## `make test` — Build test +## Running them ```sh -make test -``` - -Builds the Docker image using `.docker/Dockerfile.test`. This Dockerfile runs `./main` inside a `postgres:13` container and verifies the entire extension installation pipeline compiles and executes without errors. The image is removed afterwards. - -- **Purpose:** Fast validation that the code compiles and playgrounds don't crash. -- **Limitation:** Uses hardcoded PostgreSQL 13, regardless of `PG_VERSION`. - ---- - -## `make runtime-test` — Runtime test +make test # build PG 18 for your platform and test it +make test PG_VERSION=14 -```sh -make runtime-test # PG_VERSION=18 by default -PG_VERSION=17 make runtime-test +./tests/run.sh # test an image that's already built ``` -Runs the `.docker/runtime-test.sh` script, which: - -1. Builds the production Docker image (`.docker/Dockerfile`). -2. Starts a container with `POSTGRES_PASSWORD=postgres`. -3. Waits for PostgreSQL readiness (`pg_isready`). -4. Executes `.docker/runtime-test.sql` against the database via `psql`. - -The SQL script verifies: +## What `tests/run.sh` does -```sql -CREATE EXTENSION IF NOT EXISTS vector; -CREATE EXTENSION IF NOT EXISTS pgmq; -CREATE EXTENSION IF NOT EXISTS postgis; +1. Starts the image with `POSTGRES_PASSWORD=postgres`. +2. Waits until PostgreSQL accepts TCP connections. The entrypoint's temporary init server only listens on the unix socket, so this check doesn't stop too early. +3. For every `extensions/*/test.sql`, it creates a fresh database, runs the file with `ON_ERROR_STOP=1`, and prints `PASS` (with the installed extension versions) or `FAIL` (with the error). +4. Exits non-zero if any test failed, after running all of them. --- Confirms extensions are installed -SELECT * FROM pg_extension WHERE extname IN ('vector', 'pgmq', 'postgis'); - --- Confirms contrib is available -SELECT * FROM pg_available_extensions WHERE name = 'pg_stat_statements'; ``` - -- **Purpose:** Real validation that all extensions install correctly and work inside PostgreSQL. -- **Versions:** Runs against the PostgreSQL version specified by `PG_VERSION`. - ---- - -## CI — GitHub Actions - -The `pr-test.yml` workflow runs the runtime test matrix for all supported versions (`14`, `15`, `16`, `17`, `18`, `latest`) on `linux/amd64` for every Pull Request. This ensures changes work across the entire supported version range. - ---- - -## How to extend the tests - -### Add verification for a new extension - -Edit `.docker/runtime-test.sql`: - -```sql -CREATE EXTENSION IF NOT EXISTS my_extension; -SELECT * FROM pg_extension WHERE extname = 'my_extension'; +PASS contrib (pg_stat_statements 1.12, pg_trgm 1.6) +PASS pgmq (pgmq 1.11.1) +PASS pgvector (vector 0.8.2) +PASS postgis (postgis 3.6.4) +All tests passed. ``` -### Add a new step in runtime-test.sh - -If you need additional logic before or after the SQL tests (e.g., copy files, check permissions), edit `.docker/runtime-test.sh`. - -### Unit tests (future) +To test a new extension, add its `test.sql` (see [Extensions](./extensions.md)). You don't need to register it anywhere. -If reusable logic is extracted in the future (e.g., version parsing or handler matching), add `_test.go` files in the corresponding packages and run them with: +## CI -```sh -go test ./... -``` +[`.github/workflows/ci.yml`](../.github/workflows/ci.yml) runs on every PR, on `main`, on `vX.Y.Z` tags, weekly and on manual dispatch: -Currently none exist because the project's logic is tightly coupled to the Docker environment. +1. **Prepare:** reads the versions from `docker-bake.hcl`. For a release tag, it also checks the release is valid and not published yet (see [Releases](./releases.md)). +2. **Build and test** every version on `amd64` and `arm64`, using native runners (no emulation). +3. **All builds passed:** a single status check, for branch protection. +4. **Publish** (not on PRs): pushes the exact images that were tested and creates the tags as multi-arch manifests. Nothing is published unless every version passes on both architectures. +5. **GitHub release** (release tags only): creates the release with the versions reported by the tests. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..891c9fc --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Scripts run inside Linux containers and CI; keep LF even on Windows checkouts. +*.sh text eol=lf diff --git a/.github/scripts/release-notes.sh b/.github/scripts/release-notes.sh new file mode 100755 index 0000000..db87cb4 --- /dev/null +++ b/.github/scripts/release-notes.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# +# Usage: release-notes.sh RELEASE SUMMARY_DIR +# +# Prints a Markdown table with what each image of RELEASE contains. SUMMARY_DIR +# holds one file per PostgreSQL major, named after its bake target (pg-18, ...), +# as written by tests/run.sh with SUMMARY_FILE. + +set -euo pipefail + +RELEASE="${1:?usage: $0 RELEASE SUMMARY_DIR}" +DIR="${2:?usage: $0 RELEASE SUMMARY_DIR}" +IMAGE="${IMAGE:-ruxwez/postgres}" + +echo "| Image | PostgreSQL | Extensions |" +echo "|---|---|---|" +# Newest major first. +find "$DIR" -maxdepth 1 -type f -name 'pg-*' -printf '%f\n' | sort -t- -k2 -rn | while read -r file; do + major="${file#pg-}" + pg_version="$(awk '$1 == "postgresql" { print $2 }' "$DIR/$file")" + extensions="$(awk '$1 != "postgresql" { printf "%s%s %s", sep, $1, $2; sep = " · " }' "$DIR/$file")" + echo "| \`$IMAGE:$major-v$RELEASE\` | $pg_version | $extensions |" +done diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..60eb512 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,261 @@ +name: CI + +# Pull requests: build and test every version on amd64 and arm64. +# main / weekly / manual: same, then publish the floating tags (18, latest...). +# vX.Y.Z git tag: same, then also publish the release tags (18-vX.Y.Z, 18-vX) +# and create the GitHub release. + +on: + pull_request: + push: + branches: [main] + tags: ["v*"] + schedule: + # Weekly rebuild to pick up security fixes in the postgres base images. + - cron: "0 4 * * 1" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + # Read by docker-bake.hcl. + IMAGE: ruxwez/postgres + +jobs: + prepare: + name: Prepare + runs-on: ubuntu-latest + outputs: + targets: ${{ steps.targets.outputs.targets }} + release: ${{ steps.release.outputs.release }} + publish: ${{ steps.release.outputs.publish }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: docker/setup-buildx-action@v3 + + - id: release + name: Check release + run: | + publish=false + release= + case "$GITHUB_REF" in + refs/heads/main) + [ "$GITHUB_EVENT_NAME" != pull_request ] && publish=true ;; + refs/tags/*) + publish=true + if [[ ! "$GITHUB_REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Release tags must look like v1.2.3, got $GITHUB_REF_NAME" + exit 1 + fi + # Releases move the floating tags (18-v1, 18, latest), so an older + # version must never be released after a newer one. + newest="$(git tag --list 'v*' --sort=-v:refname | head -n1)" + if [ "$newest" != "$GITHUB_REF_NAME" ]; then + echo "::error::$GITHUB_REF_NAME is not the newest release tag ($newest is)" + exit 1 + fi + release="${GITHUB_REF_NAME#v}" ;; + esac + echo "release=$release" >> "$GITHUB_OUTPUT" + echo "publish=$publish" >> "$GITHUB_OUTPUT" + + - id: targets + run: echo "targets=$(docker buildx bake --print | jq -c '.target | keys')" >> "$GITHUB_OUTPUT" + + # 18-v1.2.3 tags are immutable: refuse to build a release that already + # exists, before anything gets pushed. + - name: Check release tags are new + if: steps.release.outputs.release != '' + env: + RELEASE: ${{ steps.release.outputs.release }} + run: | + existing=() + for tag in $(docker buildx bake --print | jq -r --arg r "-v$RELEASE" '.target[].tags[] | select(endswith($r))'); do + if docker buildx imagetools inspect "$tag" >/dev/null 2>&1; then + existing+=("$tag") + fi + done + if [ "${#existing[@]}" -gt 0 ]; then + echo "::error::Already published, release tags are never overwritten: ${existing[*]}" + exit 1 + fi + + build: + name: ${{ matrix.target }} (${{ matrix.arch }}) + needs: prepare + runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} + strategy: + fail-fast: false + matrix: + target: ${{ fromJSON(needs.prepare.outputs.targets) }} + arch: [amd64, arm64] + env: + # Read by docker-bake.hcl. + RELEASE: ${{ needs.prepare.outputs.release }} + REVISION: ${{ github.sha }} + PUBLISH: ${{ needs.prepare.outputs.publish }} + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + + - name: Build + uses: docker/bake-action@v6 + with: + source: . + targets: ${{ matrix.target }} + load: true + set: | + *.platform=linux/${{ matrix.arch }} + *.tags=postgres-extensor:test + *.cache-from=type=gha,scope=${{ matrix.target }}-${{ matrix.arch }} + *.cache-to=type=gha,mode=max,scope=${{ matrix.target }}-${{ matrix.arch }} + + - name: Test + env: + SUMMARY_FILE: /tmp/summary/${{ matrix.target }} + run: | + mkdir -p /tmp/summary + ./tests/run.sh postgres-extensor:test + + # Versions are the same on both architectures; one copy is enough for + # the release notes. + - uses: actions/upload-artifact@v4 + if: env.RELEASE != '' && matrix.arch == 'amd64' + with: + name: summary-${{ matrix.target }} + path: /tmp/summary/${{ matrix.target }} + retention-days: 1 + + - name: Log in to Docker Hub + if: env.PUBLISH == 'true' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PERSONAL_KEY }} + + # Same build as above (served from the builder cache), pushed untagged. + # The publish job stitches the per-arch digests into tagged manifests. + - name: Push by digest + id: push + if: env.PUBLISH == 'true' + uses: docker/bake-action@v6 + with: + source: . + targets: ${{ matrix.target }} + set: | + *.platform=linux/${{ matrix.arch }} + *.tags=${{ env.IMAGE }} + *.output=type=image,push-by-digest=true,name-canonical=true,push=true + *.cache-from=type=gha,scope=${{ matrix.target }}-${{ matrix.arch }} + + - name: Export digest + if: env.PUBLISH == 'true' + env: + METADATA: ${{ steps.push.outputs.metadata }} + run: | + mkdir -p /tmp/digests + digest="$(jq -r '."${{ matrix.target }}"."containerimage.digest"' <<<"$METADATA")" + touch "/tmp/digests/${digest#sha256:}" + + - uses: actions/upload-artifact@v4 + if: env.PUBLISH == 'true' + with: + name: digest-${{ matrix.target }}-${{ matrix.arch }} + path: /tmp/digests/* + retention-days: 1 + + # Single required check for branch protection. + all-builds: + name: All builds passed + needs: build + if: always() + runs-on: ubuntu-latest + steps: + - if: needs.build.result != 'success' + run: exit 1 + + publish: + # No matrix expression here: GitHub appends "(pg-18)" itself, and a skipped + # job (pull requests) shows a clean "Publish" instead of the raw expression. + name: Publish + needs: [prepare, build] + if: needs.prepare.outputs.publish == 'true' + runs-on: ubuntu-latest + # A release and a push to main can finish at the same time; push the tags + # of each version one run at a time. + concurrency: + group: publish-${{ matrix.target }} + strategy: + matrix: + target: ${{ fromJSON(needs.prepare.outputs.targets) }} + env: + RELEASE: ${{ needs.prepare.outputs.release }} + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + + - uses: actions/download-artifact@v4 + with: + pattern: digest-${{ matrix.target }}-* + path: /tmp/digests + merge-multiple: true + + - uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PERSONAL_KEY }} + + - name: Create tagged manifest + run: | + mapfile -t tags < <(docker buildx bake --print "${{ matrix.target }}" | jq -r '.target[].tags[]') + args=() + for tag in "${tags[@]}"; do args+=(--tag "$tag"); done + for digest in /tmp/digests/*; do args+=("$IMAGE@sha256:$(basename "$digest")"); done + docker buildx imagetools create "${args[@]}" + docker buildx imagetools inspect "${tags[0]}" + + release: + name: GitHub release + needs: [prepare, publish] + if: needs.prepare.outputs.release != '' + runs-on: ubuntu-latest + permissions: + contents: write + env: + RELEASE: ${{ needs.prepare.outputs.release }} + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + pattern: summary-* + path: /tmp/summary + merge-multiple: true + + - name: Create or update release + run: | + { + echo "## Images" + echo + .github/scripts/release-notes.sh "$RELEASE" /tmp/summary + echo + echo "Pin a digest (\`$IMAGE@sha256:...\`) if you need byte-for-byte reproducibility." + } > /tmp/images.md + + # If the release was created by hand (e.g. from the GitHub UI), keep + # its text and append the image table. + if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then + { gh release view "$GITHUB_REF_NAME" --json body -q .body; echo; cat /tmp/images.md; } > /tmp/notes.md + gh release edit "$GITHUB_REF_NAME" --notes-file /tmp/notes.md + else + gh release create "$GITHUB_REF_NAME" --verify-tag --title "$GITHUB_REF_NAME" \ + --generate-notes --notes-file /tmp/images.md + fi diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml deleted file mode 100644 index 8d0c349..0000000 --- a/.github/workflows/docker-build.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Build and Push Docker Images - -on: - push: - branches: - - main - workflow_dispatch: - -jobs: - build-and-push: - runs-on: ubuntu-latest - strategy: - matrix: - postgres_version: ["latest", "18", "17", "16", "15", "14"] - fail-fast: false - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PERSONAL_KEY }} - - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: . - file: .docker/Dockerfile - platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} - tags: | - ${{ secrets.DOCKER_USERNAME }}/postgres:${{ matrix.postgres_version }} - build-args: | - POSTGRES_VERSION=${{ matrix.postgres_version }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml deleted file mode 100644 index 71b39c7..0000000 --- a/.github/workflows/pr-test.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: PR - Build (amd64, arm64) - -on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - -permissions: - contents: read - pull-requests: read - -concurrency: - group: pr-build-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - build: - name: Build (no push) — PostgreSQL ${{ matrix.postgres_version }} / ${{ matrix.platform }} - runs-on: ubuntu-latest - strategy: - matrix: - postgres_version: ["latest", "18", "17", "16", "15", "14"] - platform: ["linux/amd64"] - fail-fast: false - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up QEMU (for multi-arch builds) - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - install: true - - - name: Build image for ${{ matrix.platform }} (no push) - uses: docker/build-push-action@v5 - with: - context: . - file: .docker/Dockerfile - platforms: ${{ matrix.platform }} - push: false - load: true - tags: | - pr-test/${{ github.repository }}:postgres-${{ matrix.postgres_version }}-pr${{ github.event.pull_request.number }} - build-args: | - POSTGRES_VERSION=${{ matrix.postgres_version }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Output build metadata - run: | - echo "✅ Build completed successfully" - echo "Built (multi-arch) for POSTGRES_VERSION=${{ matrix.postgres_version }} PLATFORM=${{ matrix.platform }}" - echo "Tag used: pr-test/${{ github.repository }}:postgres-${{ matrix.postgres_version }}-pr${{ github.event.pull_request.number }}" - - - name: Run runtime test - env: - SKIP_BUILD: "true" - IMAGE: pr-test/${{ github.repository }}:postgres-${{ matrix.postgres_version }}-pr${{ github.event.pull_request.number }} - run: bash .docker/runtime-test.sh - - # Job resumen que agrupa todos los builds de la matriz - build-complete: - name: All builds passed - runs-on: ubuntu-latest - needs: build - if: always() - - steps: - - name: Check build matrix status - if: needs.build.result != 'success' - run: | - echo "❌ One or more builds failed" - exit 1 - - - name: All builds passed - run: | - echo "✅ All PostgreSQL versions built successfully" - echo "Ready to merge!" diff --git a/CONTRIBUTING.es.md b/CONTRIBUTING.es.md index 90b0b17..7bcec9f 100644 --- a/CONTRIBUTING.es.md +++ b/CONTRIBUTING.es.md @@ -1,51 +1,46 @@ -# Contribuyendo +# Contribuir > [Read in English](./CONTRIBUTING.md) Gracias por tu interés en contribuir a PostgreSQL Extensor. -## Reportar issues +## Reportar problemas -Usa las plantillas en `.github/ISSUE_TEMPLATE/`: +Usa las plantillas de `.github/ISSUE_TEMPLATE/`: -- **Bug report** — para errores o comportamientos inesperados. -- **Feature request** — para sugerir nuevas extensiones o funcionalidades. -- **Custom** — para cualquier otro caso. +- **Bug report:** para errores o comportamientos inesperados. +- **Feature request:** para proponer nuevas extensiones o funcionalidades. +- **Custom:** para cualquier otra cosa. ## Pull Requests -1. Crea un fork del repositorio. +1. Haz un fork del repositorio. 2. Crea una rama con un nombre descriptivo: `feat/pg-partman`, `fix/pgvector-build`. -3. Asegúrate de que los tests pasen para todas las versiones de PostgreSQL: +3. Ejecuta los tests de al menos una versión en local: ```sh - make test - make runtime-test PG_VERSION=18 + make test PG_VERSION=18 ``` -4. Si agregas una nueva extensión, incluye: - - El playground en `extensions/`. - - El registro en `extensions/extensions.go`. - - La verificación en `.docker/runtime-test.sql`. - - Actualiza la documentación relevante en `.docs/`. +4. Si añades una extensión, sigue [Extensiones](./.docs/extensions.es.md). Se reduce a unas líneas en el `Dockerfile`, un `extensions//test.sql` y una fila en la tabla del README. +5. Abre el PR contra `main`. El CI construye y prueba todas las versiones en amd64 y arm64. -5. Abre el PR contra `main`. Los workflows de CI ejecutarán los tests automáticamente para todas las versiones soportadas. +Al hacer merge solo se actualizan los tags móviles (`18`, `latest`). Los mantenedores sacan releases para actualizar los tags con versión (ver [Releases](./.docs/releases.es.md)). -## Guías +## Pautas -- Sigue el estilo del código existente. -- No agregues dependencias innecesarias a `go.mod`. -- Los playgrounds deben ser idempotentes y limpiar los archivos temporales. -- Documenta los rangos de versiones semver claramente. +- Usa paquetes apt de PGDG antes que compilar desde el código fuente. +- Fija la versión de todas las extensiones, también las de apt, con un `ARG` y un comentario `# renovate:`. +- Las herramientas de compilación van en `build-base`, nunca en la etapa final. ## Estructura del proyecto ``` -.docker/ — Dockerfiles y scripts de test -.docs/ — Documentación -.github/ — Workflows CI/CD y templates -extensions/ — Definición de playgrounds -instance/ — Detección del entorno (OS, versión PG) -plyd/ — Motor de playgrounds (core framework) -utils/ — Utilidades de versionado semver +Dockerfile Definición de la imagen (una etapa por extensión compilada) +docker-bake.hcl Versiones de PostgreSQL y tags +extensions// test.sql de cada extensión +tests/run.sh Ejecuta todos los test.sql contra una imagen +.github/workflows/ CI: build, test, publicación, release +.github/scripts/ Scripts que usa el CI (release notes) +.docs/ Documentación ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6fbaad5..e95089b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,44 +8,39 @@ Thank you for your interest in contributing to PostgreSQL Extensor. Use the templates in `.github/ISSUE_TEMPLATE/`: -- **Bug report** — for errors or unexpected behavior. -- **Feature request** — to suggest new extensions or features. -- **Custom** — for any other case. +- **Bug report:** for errors or unexpected behavior. +- **Feature request:** to suggest new extensions or features. +- **Custom:** for anything else. ## Pull Requests 1. Fork the repository. 2. Create a branch with a descriptive name: `feat/pg-partman`, `fix/pgvector-build`. -3. Make sure tests pass for all PostgreSQL versions: +3. Run the tests for at least one version locally: ```sh - make test - make runtime-test PG_VERSION=18 + make test PG_VERSION=18 ``` -4. If adding a new extension, include: - - The playground in `extensions/`. - - The registration in `extensions/extensions.go`. - - The verification in `.docker/runtime-test.sql`. - - Update the relevant documentation in `.docs/`. +4. If you're adding an extension, follow [Extensions](./.docs/extensions.md). It comes down to a few lines in the `Dockerfile`, an `extensions//test.sql`, and a row in the README table. +5. Open the PR against `main`. CI builds and tests every version on amd64 and arm64. -5. Open the PR against `main`. CI workflows will run tests automatically for all supported versions. +Merging only updates the floating tags (`18`, `latest`). Maintainers cut releases to update the versioned tags (see [Releases](./.docs/releases.md)). ## Guidelines -- Follow the style of existing code. -- Don't add unnecessary dependencies to `go.mod`. -- Playgrounds should be idempotent and clean up temporary files. -- Document semver version ranges clearly. +- Prefer PGDG apt packages over building from source. +- Pin every extension version, apt packages included, using an `ARG` with a `# renovate:` comment. +- Build tools go in `build-base`, never in the final stage. ## Project structure ``` -.docker/ — Dockerfiles and test scripts -.docs/ — Documentation -.github/ — CI/CD workflows and templates -extensions/ — Playground definitions -instance/ — Environment detection (OS, PG version) -plyd/ — Playground engine (core framework) -utils/ — Semver versioning utilities +Dockerfile Image definition (one stage per extension built from source) +docker-bake.hcl PostgreSQL versions and tags +extensions// test.sql for each extension +tests/run.sh Runs every test.sql against an image +.github/workflows/ CI: build, test, publish, release +.github/scripts/ Helpers used by CI (release notes) +.docs/ Documentation ``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1d4c634 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,67 @@ +# syntax=docker/dockerfile:1 + +# Builds one image: postgres: plus the extensions below. +# The list of versions/tags lives in docker-bake.hcl. + +ARG POSTGRES_VERSION=18 + +# Extension versions. Renovate bumps them using the comment right above each +# one. +# renovate: datasource=github-tags depName=pgvector/pgvector +ARG PGVECTOR_VERSION=0.8.2 +# renovate: datasource=github-tags depName=pgmq/pgmq +ARG PGMQ_VERSION=1.11.1 +# PGDG package, same version for every PostgreSQL major. PGDG keeps a few +# previous versions in its index, so the pin keeps working for a while after a +# new one comes out. +# renovate: datasource=deb depName=postgresql-18-postgis-3 registryUrl=https://apt.postgresql.org/pub/repos/apt?suite=trixie-pgdg&components=main&binaryArch=amd64 +ARG POSTGIS_VERSION=3.6.4 + +# --------------------------------------------------------------------------- +# Toolchain for extensions compiled from source. Nothing from this stage ends +# up in the final image: each extension stage installs into /out and only that +# directory is copied over. +# --------------------------------------------------------------------------- +FROM postgres:${POSTGRES_VERSION} AS build-base +RUN apt-get update \ + && apt-get install -y ca-certificates git make gcc "postgresql-server-dev-${PG_MAJOR}" + +FROM build-base AS pgvector +ARG PGVECTOR_VERSION +# OPTFLAGS="" avoids -march=native, so the binary runs on any CPU and not just +# the one that built it. +RUN git clone --depth 1 --branch "v${PGVECTOR_VERSION}" https://github.com/pgvector/pgvector.git /src \ + && make -C /src OPTFLAGS="" \ + && make -C /src install OPTFLAGS="" DESTDIR=/out + +FROM build-base AS pgmq +ARG PGMQ_VERSION +RUN git clone --depth 1 --branch "v${PGMQ_VERSION}" https://github.com/pgmq/pgmq.git /src \ + && make -C /src/pgmq-extension \ + && make -C /src/pgmq-extension install DESTDIR=/out + +# --------------------------------------------------------------------------- +# Final image +# --------------------------------------------------------------------------- +FROM postgres:${POSTGRES_VERSION} +ARG PGVECTOR_VERSION +ARG PGMQ_VERSION +ARG POSTGIS_VERSION + +# Extensions available as PGDG packages. contrib (pg_stat_statements, +# pg_trgm, ...) already ships with the base image. "+*" matches any Debian +# revision of the pinned upstream version (3.6.4+dfsg-2.pgdg13+1, ...). +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + "postgresql-${PG_MAJOR}-postgis-3=${POSTGIS_VERSION}+*" \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=pgvector /out/ / +COPY --from=pgmq /out/ / + +# What's inside, readable with `docker inspect` without starting the image. +# PG_VERSION is the Debian package version set by the base image. +LABEL io.github.ruxwez.postgres.postgresql.version="${PG_VERSION}" \ + io.github.ruxwez.postgres.pgvector.version="${PGVECTOR_VERSION}" \ + io.github.ruxwez.postgres.pgmq.version="${PGMQ_VERSION}" \ + io.github.ruxwez.postgres.postgis.version="${POSTGIS_VERSION}" diff --git a/Makefile b/Makefile index 1cc22a0..6af2304 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,16 @@ PG_VERSION ?= 18 +IMAGE ?= ruxwez/postgres -.PHONY: test build runtime-test - -test: - docker build -f .docker/Dockerfile.test -t postgres-extensor-test . - -docker rmi postgres-extensor-test > /dev/null +.PHONY: build test build-all +# Build one version for the local platform. build: - docker build -f .docker/Dockerfile -t ruxwez/postgres:$(PG_VERSION) --build-arg POSTGRES_VERSION=$(PG_VERSION) . + docker buildx bake pg-$(PG_VERSION) --load + +# Build one version and run every extensions/*/test.sql against it. +test: build + ./tests/run.sh $(IMAGE):$(PG_VERSION) -runtime-test: - POSTGRES_VERSION=$(PG_VERSION) bash .docker/runtime-test.sh +# Build every version listed in docker-bake.hcl. +build-all: + docker buildx bake --load diff --git a/README.es.md b/README.es.md index 64094da..a851988 100644 --- a/README.es.md +++ b/README.es.md @@ -6,45 +6,72 @@ Imágenes Docker de PostgreSQL con extensiones populares preinstaladas. ## Propósito -Las imágenes oficiales de `postgres` no incluyen extensiones como pgvector, PGMQ o PostGIS. Instalarlas manualmente en cada despliegue es tedioso y lento. Este proyecto resuelve el problema construyendo imágenes Docker listas para usar, con las extensiones ya compiladas e instaladas. - -El binario Go se ejecuta en tiempo de build de la imagen Docker, detecta automáticamente la versión de PostgreSQL, instala cada extensión y luego se elimina a sí mismo. El resultado es una imagen limpia y optimizada. +Las imágenes oficiales de `postgres` no incluyen extensiones como pgvector, PGMQ o PostGIS. Instalarlas a mano en cada despliegue es tedioso y lento. Este proyecto publica las imágenes oficiales con esas extensiones ya instaladas, sin cambiar nada más. ## Imágenes Docker Disponibles en [Docker Hub](https://hub.docker.com/r/ruxwez/postgres): +PostgreSQL 14, 15, 16, 17 y 18, para `linux/amd64` y `linux/arm64`. + +### Tags + +Elige el tag según cuántos cambios estés dispuesto a aceptar: + +| Tag | Ejemplo | ¿Cambia? | +|-----|---------|----------| +| `-v` | `18-v1.0.0` | **Nunca.** Se publica una vez por release y no se sobrescribe. | +| `-v` | `18-v1` | Con cada release `v1.x.y`: solo cambios compatibles. | +| `` | `18` | En cada build, incluidos los rebuilds semanales y los cambios incompatibles. | +| `latest` | | Igual que el `` más reciente (ahora `18`). | + +La versión `vX.Y.Z` es la de este proyecto, no la de PostgreSQL: cambia cuando cambia una extensión. Cada [release](https://github.com/ruxwez/postgres/releases) indica las versiones exactas de PostgreSQL y de las extensiones de cada imagen. En [Releases](./.docs/releases.es.md) se explica qué significa cada tipo de subida. + +```sh +docker pull ruxwez/postgres:18-v1 # recomendado para producción +``` + +Las versiones también están en los labels de la imagen: + ```sh -docker pull ruxwez/postgres:18 -docker pull ruxwez/postgres:17 -docker pull ruxwez/postgres:16 -docker pull ruxwez/postgres:15 -docker pull ruxwez/postgres:14 -docker pull ruxwez/postgres:latest +docker inspect ruxwez/postgres:18-v1 --format '{{json .Config.Labels}}' ``` -Soporte multi-arquitectura: `linux/amd64` y `linux/arm64`. +Para una reproducibilidad exacta, fija un digest: `ruxwez/postgres@sha256:…`. ### Extensiones incluidas -| Extensión | Versión | Instalación | -|-----------|---------|-------------| -| [pgvector](https://github.com/pgvector/pgvector) | 0.8.2 | Compilación desde fuente | -| [PGMQ](https://github.com/pgmq/pgmq) | 1.11.1 | Compilación desde fuente | -| [PostGIS](https://postgis.net/) | 3 | Paquete apt (`postgresql-N-postgis-3`) | -| [postgresql-contrib](https://www.postgresql.org/docs/current/contrib.html) | — | Paquete apt (`pg_stat_statements`, etc.) | +| Extensión | Versión | Origen | +|-----------|---------|--------| +| [pgvector](https://github.com/pgvector/pgvector) | 0.8.2 | Compilada desde el código fuente | +| [PGMQ](https://github.com/pgmq/pgmq) | 1.11.1 | Compilada desde el código fuente | +| [PostGIS](https://postgis.net/) | 3.6.4 | Paquete apt de PGDG (`postgresql-N-postgis-3`) | +| [contrib](https://www.postgresql.org/docs/current/contrib.html) | — | Incluida en la imagen base (`pg_stat_statements`, `pg_trgm`, …) | + +## Cómo funciona + +Todo está en dos archivos: + +- [`Dockerfile`](./Dockerfile): parte de `postgres:N`, instala las extensiones que hay en apt y copia las que se compilan en etapas de build separadas. Los compiladores y headers nunca llegan a la imagen final. +- [`docker-bake.hcl`](./docker-bake.hcl): la lista de versiones de PostgreSQL y sus tags. El CI también la lee. + +## Uso local -## Build local +Requiere Docker con Buildx. ```sh -make build PG_VERSION=18 +make test # construye PG 18 y ejecuta los tests +make test PG_VERSION=16 # otra versión +make build-all # construye todas las versiones ``` ## Documentación -- [Tests](./.docs/tests.es.md) — Cómo funcionan los tests y cómo ampliarlos. -- [Playgrounds](./.docs/playgrounds.es.md) — Arquitectura, versionado y creación de nuevos playgrounds. +- [Extensiones](./.docs/extensions.es.md): añadir o actualizar una extensión o una versión de PostgreSQL. +- [Tests](./.docs/tests.es.md): cómo funcionan los tests y el CI. +- [Releases](./.docs/releases.es.md): versionado y cómo publicar una release. +- [Contribuir](./CONTRIBUTING.es.md) ## Licencia -MIT — ver [LICENSE](./LICENSE). +MIT, ver [LICENSE](./LICENSE). diff --git a/README.md b/README.md index 916a4d7..70029a6 100644 --- a/README.md +++ b/README.md @@ -6,45 +6,72 @@ Pre-built PostgreSQL Docker images with popular extensions. ## Purpose -Official `postgres` images don't include extensions like pgvector, PGMQ, or PostGIS. Installing them manually in every deployment is tedious and slow. This project solves that by building ready-to-use Docker images with extensions already compiled and installed. +Official `postgres` images don't include extensions like pgvector, PGMQ or PostGIS. Installing them by hand in every deployment is tedious and slow. This project publishes the official images with those extensions already installed, and nothing else changed. -The Go binary runs at Docker build time, auto-detects the PostgreSQL version, installs each extension, and then deletes itself. The result is a clean, optimized image. - -## Docker Images +## Docker images Available on [Docker Hub](https://hub.docker.com/r/ruxwez/postgres): +PostgreSQL 14, 15, 16, 17 and 18, for `linux/amd64` and `linux/arm64`. + +### Tags + +Pick a tag depending on how much change you're willing to accept: + +| Tag | Example | Changes? | +|-----|---------|----------| +| `-v` | `18-v1.0.0` | **Never.** Published once per release, never overwritten. | +| `-v` | `18-v1` | With every `v1.x.y` release: compatible updates only. | +| `` | `18` | On every build, including weekly rebuilds and breaking changes. | +| `latest` | | Same as the newest `` (currently `18`). | + +The `vX.Y.Z` version belongs to this project, not to PostgreSQL: it changes when an extension changes. Each [release](https://github.com/ruxwez/postgres/releases) lists the exact PostgreSQL and extension versions of every image. See [Releases](./.docs/releases.md) for what each kind of bump means. + +```sh +docker pull ruxwez/postgres:18-v1 # recommended for production +``` + +The versions are also in the image labels: + ```sh -docker pull ruxwez/postgres:18 -docker pull ruxwez/postgres:17 -docker pull ruxwez/postgres:16 -docker pull ruxwez/postgres:15 -docker pull ruxwez/postgres:14 -docker pull ruxwez/postgres:latest +docker inspect ruxwez/postgres:18-v1 --format '{{json .Config.Labels}}' ``` -Multi-architecture support: `linux/amd64` and `linux/arm64`. +For byte-for-byte reproducibility, pin a digest: `ruxwez/postgres@sha256:…`. ### Included extensions -| Extension | Version | Installation | -|-----------|---------|-------------| -| [pgvector](https://github.com/pgvector/pgvector) | 0.8.2 | Build from source | -| [PGMQ](https://github.com/pgmq/pgmq) | 1.11.1 | Build from source | -| [PostGIS](https://postgis.net/) | 3 | apt package (`postgresql-N-postgis-3`) | -| [postgresql-contrib](https://www.postgresql.org/docs/current/contrib.html) | — | apt package (`pg_stat_statements`, etc.) | +| Extension | Version | Source | +|-----------|---------|--------| +| [pgvector](https://github.com/pgvector/pgvector) | 0.8.2 | Built from source | +| [PGMQ](https://github.com/pgmq/pgmq) | 1.11.1 | Built from source | +| [PostGIS](https://postgis.net/) | 3.6.4 | PGDG apt package (`postgresql-N-postgis-3`) | +| [contrib](https://www.postgresql.org/docs/current/contrib.html) | — | Included in the base image (`pg_stat_statements`, `pg_trgm`, …) | + +## How it works + +Everything is in two files: + +- [`Dockerfile`](./Dockerfile): starts from `postgres:N`, installs the apt-packaged extensions, and copies in the ones compiled in separate build stages. Compilers and headers never reach the final image. +- [`docker-bake.hcl`](./docker-bake.hcl): the list of PostgreSQL versions and their tags. CI reads it too. + +## Local usage -## Local build +Requires Docker with Buildx. ```sh -make build PG_VERSION=18 +make test # build PG 18 and run the tests +make test PG_VERSION=16 # another version +make build-all # build every version ``` ## Documentation -- [Tests](./.docs/tests.md) — How tests work and how to extend them. -- [Playgrounds](./.docs/playgrounds.md) — Architecture, versioning, and creating new playgrounds. +- [Extensions](./.docs/extensions.md): adding or upgrading an extension or a PostgreSQL version. +- [Tests](./.docs/tests.md): how the tests and CI work. +- [Releases](./.docs/releases.md): versioning and how to publish a release. +- [Contributing](./CONTRIBUTING.md) ## License -MIT — see [LICENSE](./LICENSE). +MIT, see [LICENSE](./LICENSE). diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 0000000..62ebb24 --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,57 @@ +# Single source of truth for which images are built and how they're tagged. +# +# docker buildx bake --print # show every target +# docker buildx bake pg-17 # build one version +# RELEASE=1.4.0 docker buildx bake --print # tags of a release build + +variable "IMAGE" { + default = "ruxwez/postgres" +} + +# Newest supported major. It is also published as :latest. +variable "LATEST" { + default = "18" +} + +# Project version (semver, without the leading "v"). Set by CI when a vX.Y.Z +# git tag is pushed; empty for regular builds. +variable "RELEASE" { + default = "" +} + +# Upstream source, revision and version in the standard OCI labels. +variable "REVISION" { + default = "" +} + +function "release_tags" { + params = [major] + result = RELEASE == "" ? [] : [ + "${IMAGE}:${major}-v${RELEASE}", # immutable + "${IMAGE}:${major}-v${split(".", RELEASE)[0]}", # compatible updates + ] +} + +target "pg" { + name = "pg-${major}" + matrix = { + major = ["14", "15", "16", "17", "18"] + } + args = { + POSTGRES_VERSION = major + } + tags = concat( + ["${IMAGE}:${major}"], + major == LATEST ? ["${IMAGE}:latest"] : [], + release_tags(major), + ) + labels = { + "org.opencontainers.image.source" = "https://github.com/ruxwez/postgres" + "org.opencontainers.image.version" = RELEASE + "org.opencontainers.image.revision" = REVISION + } +} + +group "default" { + targets = ["pg"] +} diff --git a/extensions/builddeps_install.go b/extensions/builddeps_install.go deleted file mode 100644 index 1045a40..0000000 --- a/extensions/builddeps_install.go +++ /dev/null @@ -1,23 +0,0 @@ -package extensions - -import ( - "fmt" - - "postgres-extensor/instance" - "postgres-extensor/plyd" -) - -var buildDepsInstall = plyd.New( - "build-deps-install", - plyd.NewHandlersList( - plyd.NewHandler(">=14.0.0, <19.0.0", func(ins *instance.InstanceInfo) error { - if err := plyd.Run("apt-get update -qq"); err != nil { - return err - } - return plyd.Run(fmt.Sprintf( - "apt-get install -y -qq git make gcc postgresql-server-dev-%s", - ins.PostgreSQL.Mayor, - )) - }), - ), -) diff --git a/extensions/builddeps_remove.go b/extensions/builddeps_remove.go deleted file mode 100644 index 16c2247..0000000 --- a/extensions/builddeps_remove.go +++ /dev/null @@ -1,23 +0,0 @@ -package extensions - -import ( - "fmt" - - "postgres-extensor/instance" - "postgres-extensor/plyd" -) - -var buildDepsRemove = plyd.New( - "build-deps-remove", - plyd.NewHandlersList( - plyd.NewHandler(">=14.0.0, <19.0.0", func(ins *instance.InstanceInfo) error { - if err := plyd.Run(fmt.Sprintf( - "apt-get remove -y -qq git make gcc postgresql-server-dev-%s", - ins.PostgreSQL.Mayor, - )); err != nil { - return err - } - return plyd.Run("apt-get autoremove -y -qq && apt-get clean && rm -rf /var/lib/apt/lists/*") - }), - ), -) diff --git a/extensions/contrib/test.sql b/extensions/contrib/test.sql new file mode 100644 index 0000000..62cd09c --- /dev/null +++ b/extensions/contrib/test.sql @@ -0,0 +1,9 @@ +CREATE EXTENSION pg_stat_statements; +CREATE EXTENSION pg_trgm; + +DO $$ +BEGIN + IF similarity('postgres', 'postgis') <= 0 THEN + RAISE EXCEPTION 'contrib: pg_trgm similarity returned 0'; + END IF; +END $$; diff --git a/extensions/extensions.go b/extensions/extensions.go deleted file mode 100644 index 46797f9..0000000 --- a/extensions/extensions.go +++ /dev/null @@ -1,12 +0,0 @@ -package extensions - -import "postgres-extensor/plyd" - -var Playgrounds = []*plyd.Playground{ - buildDepsInstall, - pgmq, - pgvector, - postgis, - postgresContrib, - buildDepsRemove, -} diff --git a/extensions/pgmq.go b/extensions/pgmq.go deleted file mode 100644 index e1b2315..0000000 --- a/extensions/pgmq.go +++ /dev/null @@ -1,34 +0,0 @@ -package extensions - -import ( - "fmt" - - "postgres-extensor/instance" - "postgres-extensor/plyd" -) - -var pgmq = plyd.New( - "pgmq", - plyd.NewHandlersList( - plyd.NewHandler(">=14.0.0, <19.0.0", func(ins *instance.InstanceInfo) error { - err := plyd.Run( - "git clone --branch v1.11.1 --depth 1 " + - "https://github.com/pgmq/pgmq.git /tmp/pgmq", - ) - if err != nil { - return err - } - err = plyd.Run("cd /tmp/pgmq/pgmq-extension && make && make install") - if err != nil { - return err - } - - // Verify installation - if err := plyd.Run("ls $(pg_config --sharedir)/extension/pgmq.control > /dev/null"); err != nil { - return fmt.Errorf("failed to verify pgmq installation: %w", err) - } - - return plyd.Run("rm -rf /tmp/pgmq") - }), - ), -) diff --git a/extensions/pgmq/test.sql b/extensions/pgmq/test.sql new file mode 100644 index 0000000..0b4b4df --- /dev/null +++ b/extensions/pgmq/test.sql @@ -0,0 +1,14 @@ +CREATE EXTENSION pgmq; + +SELECT pgmq.create('test_queue'); +SELECT pgmq.send(queue_name => 'test_queue', msg => '{"hello": "world"}'); + +DO $$ +DECLARE + msg jsonb; +BEGIN + SELECT message INTO msg FROM pgmq.read(queue_name => 'test_queue', vt => 30, qty => 1); + IF msg->>'hello' IS DISTINCT FROM 'world' THEN + RAISE EXCEPTION 'pgmq: round-trip failed, got %', msg; + END IF; +END $$; diff --git a/extensions/pgvector.go b/extensions/pgvector.go deleted file mode 100644 index 35b33f1..0000000 --- a/extensions/pgvector.go +++ /dev/null @@ -1,34 +0,0 @@ -package extensions - -import ( - "fmt" - - "postgres-extensor/instance" - "postgres-extensor/plyd" -) - -var pgvector = plyd.New( - "pgvector", - plyd.NewHandlersList( - plyd.NewHandler(">=14.0.0, <19.0.0", func(ins *instance.InstanceInfo) error { - err := plyd.Run( - "git clone --branch v0.8.2 --depth 1 " + - "https://github.com/pgvector/pgvector.git /tmp/pgvector", - ) - if err != nil { - return err - } - err = plyd.Run("cd /tmp/pgvector && make && make install") - if err != nil { - return err - } - - // Verify installation - if err := plyd.Run("ls $(pg_config --sharedir)/extension/vector.control > /dev/null"); err != nil { - return fmt.Errorf("failed to verify pgvector installation: %w", err) - } - - return plyd.Run("rm -rf /tmp/pgvector") - }), - ), -) diff --git a/extensions/pgvector/test.sql b/extensions/pgvector/test.sql new file mode 100644 index 0000000..adf5742 --- /dev/null +++ b/extensions/pgvector/test.sql @@ -0,0 +1,12 @@ +CREATE EXTENSION vector; + +CREATE TABLE items (id bigserial PRIMARY KEY, embedding vector(3)); +INSERT INTO items (embedding) VALUES ('[1,2,3]'), ('[4,5,6]'); +CREATE INDEX ON items USING hnsw (embedding vector_l2_ops); + +DO $$ +BEGIN + IF (SELECT id FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 1) <> 1 THEN + RAISE EXCEPTION 'pgvector: unexpected nearest neighbour'; + END IF; +END $$; diff --git a/extensions/postgis.go b/extensions/postgis.go deleted file mode 100644 index fd1d522..0000000 --- a/extensions/postgis.go +++ /dev/null @@ -1,27 +0,0 @@ -package extensions - -import ( - "fmt" - - "postgres-extensor/instance" - "postgres-extensor/plyd" -) - -var postgis = plyd.New( - "postgis", - plyd.NewHandlersList( - plyd.NewHandler(">=14.0.0, <19.0.0", func(ins *instance.InstanceInfo) error { - pkg := fmt.Sprintf("postgresql-%s-postgis-3", ins.PostgreSQL.Mayor) - if err := plyd.Run(fmt.Sprintf("apt-get install -y -qq %s", pkg)); err != nil { - return err - } - - // Verify installation - if err := plyd.Run("ls $(pg_config --sharedir)/extension/postgis.control > /dev/null"); err != nil { - return fmt.Errorf("failed to verify postgis installation: %w", err) - } - - return nil - }), - ), -) diff --git a/extensions/postgis/test.sql b/extensions/postgis/test.sql new file mode 100644 index 0000000..5a25125 --- /dev/null +++ b/extensions/postgis/test.sql @@ -0,0 +1,8 @@ +CREATE EXTENSION postgis; + +DO $$ +BEGIN + IF ST_Distance('POINT(0 0)'::geometry, 'POINT(3 4)'::geometry) <> 5 THEN + RAISE EXCEPTION 'postgis: unexpected distance'; + END IF; +END $$; diff --git a/extensions/postgrescontrib.go b/extensions/postgrescontrib.go deleted file mode 100644 index 10b74f0..0000000 --- a/extensions/postgrescontrib.go +++ /dev/null @@ -1,28 +0,0 @@ -package extensions - -import ( - "fmt" - - "postgres-extensor/instance" - "postgres-extensor/plyd" -) - -var postgresContrib = plyd.New( - "postgres-contrib", - plyd.NewHandlersList( - plyd.NewHandler(">=14.0.0, <19.0.0", func(ins *instance.InstanceInfo) error { - // In Debian/Ubuntu pgdg distributions, contrib modules are bundled within the main postgresql package. - pkg := fmt.Sprintf("postgresql-%s", ins.PostgreSQL.Mayor) - if err := plyd.Run(fmt.Sprintf("apt-get install -y -qq %s", pkg)); err != nil { - return err - } - - // Verify installation using a standard contrib module like pg_stat_statements - if err := plyd.Run("ls $(pg_config --sharedir)/extension/pg_stat_statements.control > /dev/null"); err != nil { - return fmt.Errorf("failed to verify postgres-contrib installation: %w", err) - } - - return nil - }), - ), -) diff --git a/go.mod b/go.mod deleted file mode 100644 index 741a2d9..0000000 --- a/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module postgres-extensor - -go 1.26.3 - -require github.com/Masterminds/semver/v3 v3.5.0 diff --git a/go.sum b/go.sum deleted file mode 100644 index f1a7a9a..0000000 --- a/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= -github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= diff --git a/instance/instance.go b/instance/instance.go deleted file mode 100644 index 0d6957e..0000000 --- a/instance/instance.go +++ /dev/null @@ -1,58 +0,0 @@ -package instance - -import ( - "os/exec" - "regexp" - "runtime" - "strings" - - "postgres-extensor/utils" -) - -type PGVersion string - -type InstanceInfo struct { - OS string - Arch string - PostgreSQL PGInstance -} - -type PGInstance struct { - Version PGVersion - Mayor string -} - -func detectPGVersion() PGVersion { - out, err := exec.Command("postgres", "--version").Output() - if err != nil { - panic("Failed to detect PostgreSQL version: " + err.Error()) - } - - re := regexp.MustCompile(`([\d.]+)`) - match := re.FindString(strings.TrimSpace(string(out))) - if match == "" { - panic("Failed to parse PostgreSQL version from: " + string(out)) - } - - normalized, err := utils.NormalizeVersion(match) - if err != nil { - panic("Failed to normalize PostgreSQL version from: " + string(out) + ": " + err.Error()) - } - - return PGVersion(normalized) -} - -func New() *InstanceInfo { - if runtime.GOOS != "linux" { - panic("This software only runs on Linux") - } - - return &InstanceInfo{ - PostgreSQL: PGInstance{ - Version: detectPGVersion(), - Mayor: strings.Split(string(detectPGVersion()), ".")[0], - }, - OS: runtime.GOOS, - Arch: runtime.GOARCH, - } -} diff --git a/main.go b/main.go deleted file mode 100644 index a3c9c4a..0000000 --- a/main.go +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "postgres-extensor/extensions" - "postgres-extensor/instance" -) - -func main() { - - var ins = instance.New() - - for _, playground := range extensions.Playgrounds { - playground.Run(ins) - } -} diff --git a/plyd/handlers.go b/plyd/handlers.go deleted file mode 100644 index d3c4ccf..0000000 --- a/plyd/handlers.go +++ /dev/null @@ -1,39 +0,0 @@ -package plyd - -import ( - "sort" - - "postgres-extensor/instance" - "postgres-extensor/utils" -) - -type HandlerFunc func(ins *instance.InstanceInfo) error - -type Handler struct { - Range string - Runner HandlerFunc -} - -// NewHandler creates a new Handler with the specified version range and runner function. -func NewHandler(rng string, runner HandlerFunc) Handler { - if err := utils.ValidateVersionRange(rng); err != nil { - panic("Invalid handler range: " + err.Error()) - } - - return Handler{ - Range: rng, - Runner: runner, - } -} - -type HandlersList []Handler - -// NewHandlersList sorts handlers with specific versions first, ranges last, ascending. -func NewHandlersList(handlers ...Handler) HandlersList { - - sort.Slice(handlers, func(i, j int) bool { - return utils.VersionLess(handlers[i].Range, handlers[j].Range) - }) - - return handlers -} diff --git a/plyd/playground.go b/plyd/playground.go deleted file mode 100644 index baec084..0000000 --- a/plyd/playground.go +++ /dev/null @@ -1,70 +0,0 @@ -package plyd - -import ( - "fmt" - "log" - - "postgres-extensor/instance" - - "github.com/Masterminds/semver/v3" -) - -type Playground struct { - Name string - handlers HandlersList -} - -func New(name string, handlers HandlersList) *Playground { - return &Playground{ - Name: name, - handlers: handlers, - } -} - -// Match the instance's PostgreSQL version to the appropriate handler -func (p *Playground) matchHandler(v string) (HandlerFunc, error) { - - for _, h := range p.handlers { - if h.Range == v { - return h.Runner, nil - } - } - - version, err := semver.NewVersion(v) - if err != nil { - return nil, fmt.Errorf("Invalid version: %s", v) - } - - for _, h := range p.handlers { - constraint, err := semver.NewConstraint(h.Range) - if err != nil { - continue - } - if constraint.Check(version) { - return h.Runner, nil - } - } - - return nil, fmt.Errorf("No handler found for version %s", v) -} - -// Execute the playground with the given instance information -func (p *Playground) Run(ins *instance.InstanceInfo) { - - log.Printf("[PLYD]: Installing '%s' for PostgreSQL '%s'", p.Name, ins.PostgreSQL.Version) - - handler, err := p.matchHandler(string(ins.PostgreSQL.Version)) - if err != nil { - log.Printf("[PLYD]: The playground '%s' is not compatible with PostgreSQL version '%s'", p.Name, ins.PostgreSQL.Version) - log.Println(err) - return - } - - err = handler(ins) - if err != nil { - log.Fatalf("[PLYD]: Error installing '%s' for PostgreSQL '%s'. Error: %v", p.Name, ins.PostgreSQL.Version, err) - return - } - - log.Printf("[PLYD]: Successfully installed '%s' for PostgreSQL '%s'", p.Name, ins.PostgreSQL.Version) -} diff --git a/plyd/run.go b/plyd/run.go deleted file mode 100644 index 69dffac..0000000 --- a/plyd/run.go +++ /dev/null @@ -1,13 +0,0 @@ -package plyd - -import ( - "os" - "os/exec" -) - -func Run(cmd string) error { - c := exec.Command("sh", "-c", cmd) - c.Stdout = os.Stdout - c.Stderr = os.Stderr - return c.Run() -} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..66e1c73 --- /dev/null +++ b/renovate.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "customManagers": [ + { + "customType": "regex", + "description": "Extension versions (see the `# renovate:` comments in the Dockerfile)", + "managerFilePatterns": ["/^Dockerfile$/"], + "matchStrings": [ + "# renovate: datasource=(?\\S+) depName=(?\\S+)(?: registryUrl=(?\\S+))?\\s+ARG \\S+_VERSION=(?\\S+)" + ], + "extractVersionTemplate": "^v?(?[^+]+)" + } + ] +} diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..753e727 --- /dev/null +++ b/tests/run.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# +# Usage: tests/run.sh IMAGE +# +# Starts IMAGE and runs every extensions/*/test.sql against it, each one in a +# fresh database. Fails if any test fails. +# +# If SUMMARY_FILE is set, writes there the PostgreSQL version and the version +# of every extension the tests created, one "name version" pair per line. + +set -euo pipefail + +IMAGE="${1:?usage: $0 IMAGE}" +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +CONTAINER="postgres-extensor-test-$$" + +trap 'docker rm -f "$CONTAINER" >/dev/null 2>&1 || true' EXIT + +docker run -d --name "$CONTAINER" -e POSTGRES_PASSWORD=postgres "$IMAGE" >/dev/null + +# Connect over TCP: during first-boot initialisation the entrypoint runs a +# temporary server on the unix socket only, so this waits for the real one. +pg() { + docker exec -i -e PGPASSWORD=postgres "$CONTAINER" psql -h 127.0.0.1 -U postgres -X -q -v ON_ERROR_STOP=1 "$@" +} + +echo "Waiting for PostgreSQL..." +for _ in $(seq 60); do + docker exec "$CONTAINER" pg_isready -h 127.0.0.1 -U postgres -q && break + sleep 1 +done +if ! docker exec "$CONTAINER" pg_isready -h 127.0.0.1 -U postgres -q; then + docker logs "$CONTAINER" + echo "PostgreSQL did not start" >&2 + exit 1 +fi +pg -At -c 'SELECT version()' + +summary="postgresql $(pg -At -c "SELECT split_part(current_setting('server_version'), ' ', 1)")"$'\n' + +failed=() +for sql in "$ROOT"/extensions/*/test.sql; do + name="$(basename "$(dirname "$sql")")" + db="test_${name//-/_}" + pg -c "CREATE DATABASE $db" >/dev/null + if output="$(pg -d "$db" <"$sql" 2>&1)"; then + versions="$(pg -d "$db" -At -F ' ' -c "SELECT extname, extversion FROM pg_extension WHERE extname <> 'plpgsql' ORDER BY extname")" + summary+="$versions"$'\n' + echo "PASS $name (${versions//$'\n'/, })" + else + echo "FAIL $name" + echo "$output" | sed 's/^/ /' + failed+=("$name") + fi +done + +if [ -n "${SUMMARY_FILE:-}" ]; then + printf '%s' "$summary" >"$SUMMARY_FILE" +fi + +if [ "${#failed[@]}" -gt 0 ]; then + echo "Failed: ${failed[*]}" >&2 + exit 1 +fi +echo "All tests passed." diff --git a/utils/version.go b/utils/version.go deleted file mode 100644 index f0f03eb..0000000 --- a/utils/version.go +++ /dev/null @@ -1,38 +0,0 @@ -package utils - -import ( - "fmt" - - "github.com/Masterminds/semver/v3" -) - -func NormalizeVersion(v string) (string, error) { - ver, err := semver.NewVersion(v) - if err != nil { - return "", fmt.Errorf("Invalid version: %s", v) - } - return ver.String(), nil -} - -func ValidateVersionRange(rng string) error { - _, err := semver.NewConstraint(rng) - return err -} - -// VersionLess returns true if a should be sorted before b. -// Specific versions come before ranges; both groups are sorted ascending. -func VersionLess(a, b string) bool { - va, erra := semver.NewVersion(a) - vb, errb := semver.NewVersion(b) - - switch { - case erra == nil && errb == nil: - return va.LessThan(vb) - case erra == nil: - return true - case errb == nil: - return false - default: - return a < b - } -}