diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9a05080 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.github +.dockerignore +.gitignore +.env* +*.md +LICENSE +docker-compose*.yml +versions.json diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4c54c6d..7f7b15d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,33 +7,34 @@ on: env: REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/${{ github.event.repository.name }} jobs: - build-and-push: + load-versions: runs-on: ubuntu-latest + outputs: + build-matrix: ${{ steps.set-matrix.outputs.build_matrix }} + merge-matrix: ${{ steps.set-matrix.outputs.merge_matrix }} + steps: + - uses: actions/checkout@v6 + - id: set-matrix + run: | + RUNNERS='[{"runner":"ubuntu-latest","platform":"linux/amd64"},{"runner":"ubuntu-24.04-arm","platform":"linux/arm64"}]' + BUILD_MATRIX=$(jq -c --argjson runners "$RUNNERS" '{include: [.[] | . as $v | $runners[] | . + $v]}' versions.json) + echo "build_matrix=$BUILD_MATRIX" >> "$GITHUB_OUTPUT" + echo "merge_matrix=$(jq -c '{include: .}' versions.json)" >> "$GITHUB_OUTPUT" + + build: + needs: load-versions + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 permissions: contents: read packages: write strategy: - matrix: - include: - - pg_version: "17.9" - postgis_version: "3.6.2" - pgvector_version: "0.8.2" - latest: true - - pg_version: "16.13" - postgis_version: "3.6.2" - pgvector_version: "0.8.2" - latest: false - - pg_version: "15.17" - postgis_version: "3.6.2" - pgvector_version: "0.8.2" - latest: false - - pg_version: "14.22" - postgis_version: "3.6.2" - pgvector_version: "0.8.2" - latest: false + fail-fast: false + matrix: ${{ fromJSON(needs.load-versions.outputs.build-matrix) }} steps: - name: Checkout repository @@ -53,22 +54,86 @@ jobs: id: meta uses: docker/metadata-action@v6 with: - images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ github.event.repository.name }} - tags: | - type=raw,value=postgres-${{ matrix.pg_version }}-postgis-${{ matrix.postgis_version }}-pgvector-${{ matrix.pgvector_version }} - type=raw,value=latest,enable=${{ matrix.latest }} - type=ref,event=tag + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - name: Build and push Docker image + - name: Build and push by digest + id: build uses: docker/build-push-action@v7 with: context: . - push: true - tags: ${{ steps.meta.outputs.tags }} + platforms: ${{ matrix.platform }} + provenance: false + sbom: false labels: ${{ steps.meta.outputs.labels }} build-args: | PG_VERSION=${{ matrix.pg_version }} POSTGIS_VERSION=${{ matrix.postgis_version }} PGVECTOR_VERSION=${{ matrix.pgvector_version }} - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=gha,scope=${{ matrix.pg_version }}-${{ matrix.runner }} + outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true + + - name: Export digest + env: + DIGEST: ${{ steps.build.outputs.digest }} + run: | + mkdir -p /tmp/digests + touch "/tmp/digests/${DIGEST#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digest-${{ matrix.pg_version }}-${{ matrix.runner }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: [load-versions, build] + permissions: + contents: read + packages: write + + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.load-versions.outputs.merge-matrix) }} + + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + pattern: digest-${{ matrix.pg_version }}-* + merge-multiple: true + path: /tmp/digests + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=postgres-${{ matrix.pg_version }}-postgis-${{ matrix.postgis_version }}-pgvector-${{ matrix.pgvector_version }} + type=raw,value=latest,enable=${{ matrix.latest }} + type=ref,event=tag,enable=${{ matrix.latest }} + + - name: Create multi-arch manifest and push + working-directory: /tmp/digests + env: + REGISTRY: ${{ env.REGISTRY }} + IMAGE: ${{ env.IMAGE_NAME }} + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf "${REGISTRY}/${IMAGE}@sha256:%s " *) + + - name: Verify multi-arch manifest + run: | + docker buildx imagetools inspect $(jq -cr '.tags[0]' <<< "$DOCKER_METADATA_OUTPUT_JSON") diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 99fb817..1f81f80 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,24 +7,28 @@ on: branches: [main] workflow_dispatch: +permissions: + contents: read + jobs: - test: + load-versions: runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - uses: actions/checkout@v6 + - id: set-matrix + run: | + MATRIX=$(jq -c '{include: [.[] | . as $v | {runner: "ubuntu-latest"}, {runner: "ubuntu-24.04-arm"} | . + $v]}' versions.json) + echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT" + + test: + needs: load-versions + runs-on: ${{ matrix.runner }} + timeout-minutes: 15 strategy: - matrix: - include: - - pg_version: "17.9" - postgis_version: "3.6.2" - pgvector_version: "0.8.2" - - pg_version: "16.13" - postgis_version: "3.6.2" - pgvector_version: "0.8.2" - - pg_version: "15.17" - postgis_version: "3.6.2" - pgvector_version: "0.8.2" - - pg_version: "14.22" - postgis_version: "3.6.2" - pgvector_version: "0.8.2" + fail-fast: false + matrix: ${{ fromJSON(needs.load-versions.outputs.matrix) }} steps: - uses: actions/checkout@v6 @@ -42,36 +46,76 @@ jobs: PG_VERSION=${{ matrix.pg_version }} POSTGIS_VERSION=${{ matrix.postgis_version }} PGVECTOR_VERSION=${{ matrix.pgvector_version }} + cache-from: type=gha,scope=${{ matrix.pg_version }}-${{ matrix.runner }} + cache-to: type=gha,mode=max,scope=${{ matrix.pg_version }}-${{ matrix.runner }} - - name: Start PostgreSQL container and test extensions + - name: Start PostgreSQL container + env: + PG_VERSION: ${{ matrix.pg_version }} + RUNNER: ${{ matrix.runner }} run: | - IMAGE_TAG="postgres-test:pg${{ matrix.pg_version }}" - echo "Testing image: $IMAGE_TAG" + IMAGE_TAG="postgres-test:pg${PG_VERSION}" + echo "Testing image: $IMAGE_TAG (runner: $RUNNER)" docker run -d --name test-db \ -e POSTGRES_PASSWORD=test \ -e POSTGRES_USER=test \ -e POSTGRES_DB=test \ - $IMAGE_TAG \ + --health-cmd="pg_isready -U test -d test" \ + --health-interval=2s \ + --health-timeout=5s \ + --health-retries=30 \ + "$IMAGE_TAG" \ postgres -c shared_preload_libraries=vector - echo "Waiting for PostgreSQL to start..." - sleep 15 - - echo "PostgreSQL logs:" - docker logs test-db + echo "Waiting for PostgreSQL to become healthy..." + until [ "$(docker inspect -f '{{.State.Health.Status}}' test-db)" = "healthy" ]; do + if [ "$(docker inspect -f '{{.State.Status}}' test-db)" != "running" ]; then + echo "Container exited unexpectedly!" + docker logs test-db + exit 1 + fi + sleep 1 + done + echo "PostgreSQL is ready." + - name: Test PostGIS extension + run: | echo "Checking PostGIS version..." docker exec test-db psql -U test -d test -c "SELECT postgis_full_version();" + echo "Testing PostGIS spatial functionality..." + docker exec test-db psql -U test -d test -c "SELECT ST_AsText(ST_Point(1, 2));" + + - name: Test pgvector extension + run: | echo "Checking pgvector extension version..." docker exec test-db psql -U test -d test -c "CREATE EXTENSION IF NOT EXISTS vector; SELECT extversion FROM pg_extension WHERE extname = 'vector';" - echo "Attempting to create a table with a vector column and insert data..." + echo "Testing vector operations..." docker exec test-db psql -U test -d test -c "CREATE TABLE items (id bigserial PRIMARY KEY, embedding vector(3)); INSERT INTO items (embedding) VALUES ('[1,2,3]'), ('[4,5,6]'); SELECT COUNT(*) FROM items;" + - name: Verify installed versions match requested + env: + EXPECTED_PGVECTOR: ${{ matrix.pgvector_version }} + EXPECTED_POSTGIS: ${{ matrix.postgis_version }} + run: | + ACTUAL_PGVECTOR=$(docker exec test-db psql -U test -d test -tAc \ + "SELECT extversion FROM pg_extension WHERE extname = 'vector';") + if [ "$ACTUAL_PGVECTOR" != "$EXPECTED_PGVECTOR" ]; then + echo "FAIL: expected pgvector $EXPECTED_PGVECTOR, got $ACTUAL_PGVECTOR" + exit 1 + fi + echo "pgvector version OK: $ACTUAL_PGVECTOR" + + ACTUAL_POSTGIS=$(docker exec test-db psql -U test -d test -tAc \ + "SELECT extversion FROM pg_extension WHERE extname = 'postgis';") + if [ "$ACTUAL_POSTGIS" != "$EXPECTED_POSTGIS" ]; then + echo "FAIL: expected PostGIS $EXPECTED_POSTGIS, got $ACTUAL_POSTGIS" + exit 1 + fi + echo "PostGIS version OK: $ACTUAL_POSTGIS" + - name: Stop and remove container if: always() - run: | - docker stop test-db || true - docker rm test-db || true + run: docker rm -f test-db || true diff --git a/Dockerfile b/Dockerfile index f04061f..8937f75 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,54 +13,51 @@ LABEL maintainer="TypeORM" LABEL description="PostgreSQL with PostGIS and pgvector extensions for TypeORM" LABEL org.opencontainers.image.source="https://github.com/typeorm/docker" -# Install base dependencies, setup PGDG repository, and install build tools +# Install PostGIS, build pgvector from source, then clean up in a single layer # Note: PG_MAJOR is provided by the official postgres base image -RUN apt-get update \ +RUN set -eux \ + && apt-get update \ && apt-get install -y --no-install-recommends \ - lsb-release \ - gnupg \ - ca-certificates \ - wget \ + lsb-release \ + gnupg \ + ca-certificates \ + wget \ && wget --quiet -O /usr/share/keyrings/postgresql-archive-keyring.gpg https://www.postgresql.org/media/keys/ACCC4CF8.asc \ && sh -c 'echo "deb [signed-by=/usr/share/keyrings/postgresql-archive-keyring.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' \ && apt-get update \ && apt-get install -y --no-install-recommends \ - build-essential \ - git \ - make \ - gcc \ - "postgresql-server-dev-${PG_MAJOR}" - -# Install pinned PostGIS version (apt packages use major version in name) -RUN POSTGIS_MAJOR=$(echo "${POSTGIS_VERSION}" | cut -d. -f1) \ - && apt-get update \ + build-essential \ + git \ + make \ + gcc \ + "postgresql-server-dev-${PG_MAJOR}" \ + && POSTGIS_MAJOR=$(echo "${POSTGIS_VERSION}" | cut -d. -f1) \ && apt-get install -y --no-install-recommends \ - "postgis=${POSTGIS_VERSION}+dfsg*" \ - "postgresql-${PG_MAJOR}-postgis-${POSTGIS_MAJOR}=${POSTGIS_VERSION}+dfsg*" \ - "postgresql-${PG_MAJOR}-postgis-${POSTGIS_MAJOR}-scripts=${POSTGIS_VERSION}+dfsg*" - -# Build and install pinned pgvector version from source -RUN apt-get update \ - && apt-get install -y --no-install-recommends git make gcc "postgresql-server-dev-${PG_MAJOR}" \ - && mkdir -p /usr/src/pgvector \ - && git clone --branch "v${PGVECTOR_VERSION}" https://github.com/pgvector/pgvector.git /usr/src/pgvector \ + "postgis=${POSTGIS_VERSION}+dfsg*" \ + "postgresql-${PG_MAJOR}-postgis-${POSTGIS_MAJOR}=${POSTGIS_VERSION}+dfsg*" \ + "postgresql-${PG_MAJOR}-postgis-${POSTGIS_MAJOR}-scripts=${POSTGIS_VERSION}+dfsg*" \ + && git clone --branch "v${PGVECTOR_VERSION}" --depth 1 https://github.com/pgvector/pgvector.git /usr/src/pgvector \ && cd /usr/src/pgvector \ && make \ - && make install - -# Cleanup build dependencies -RUN apt-get purge -y --auto-remove \ - build-essential \ - git \ - make \ - gcc \ - "postgresql-server-dev-${PG_MAJOR}" \ - wget \ + && make install \ + && apt-get purge -y --auto-remove \ + build-essential \ + git \ + make \ + gcc \ + "postgresql-server-dev-${PG_MAJOR}" \ + wget \ + lsb-release \ + gnupg \ && apt-get clean \ - && rm -rf /var/lib/apt/lists/* \ - && rm -rf /usr/src/pgvector + && rm -rf /var/lib/apt/lists/* /usr/src/pgvector \ + /etc/apt/sources.list.d/pgdg.list \ + /usr/share/keyrings/postgresql-archive-keyring.gpg # Copy initialization scripts COPY docker-entrypoint-initdb.d/ /docker-entrypoint-initdb.d/ +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD pg_isready -U postgres + EXPOSE 5432 diff --git a/versions.json b/versions.json new file mode 100644 index 0000000..c88ac01 --- /dev/null +++ b/versions.json @@ -0,0 +1,26 @@ +[ + { + "pg_version": "17.9", + "postgis_version": "3.6.2", + "pgvector_version": "0.8.2", + "latest": true + }, + { + "pg_version": "16.13", + "postgis_version": "3.6.2", + "pgvector_version": "0.8.2", + "latest": false + }, + { + "pg_version": "15.17", + "postgis_version": "3.6.2", + "pgvector_version": "0.8.2", + "latest": false + }, + { + "pg_version": "14.22", + "postgis_version": "3.6.2", + "pgvector_version": "0.8.2", + "latest": false + } +]