name: CI

on:
  push:
    branches: [ main, master, develop ]
  pull_request:
    branches: [ main, master ]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # Linux - PostgreSQL 12-18 via PGDG packages, plus a PostgreSQL 19
  # source-build job below. Regression and TAP tests are executed.
  # PostgreSQL 12 and 13 are out of support upstream: PGDG still builds them,
  # but only for older Ubuntu releases, so those two jobs stay on 22.04.
  linux:
    name: Linux / PostgreSQL ${{ matrix.pg_version }}
    runs-on: ${{ matrix.os || 'ubuntu-latest' }}

    strategy:
      fail-fast: false
      matrix:
        pg_version: [14, 15, 16, 17, 18]
        include:
          - pg_version: 12
            os: ubuntu-22.04
          - pg_version: 13
            os: ubuntu-22.04

    env:
      PG_CONFIG: /usr/lib/postgresql/${{ matrix.pg_version }}/bin/pg_config

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install PostgreSQL, build dependencies, and Perl modules
        run: |
          sudo apt-get update
          sudo apt-get install -y ca-certificates postgresql-common build-essential libipc-run-perl
          sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
          sudo apt-get update
          sudo apt-get install -y postgresql-${{ matrix.pg_version }} postgresql-server-dev-${{ matrix.pg_version }}

      - name: Build extension
        run: make PG_CONFIG="$PG_CONFIG" with_llvm=no

      - name: Install extension (into system PostgreSQL)
        run: sudo make PG_CONFIG="$PG_CONFIG" install with_llvm=no

      - name: Setup test cluster
        run: |
          sudo pg_createcluster ${{ matrix.pg_version }} testcluster --start --port 5433
          echo "local all all trust" | sudo tee -a /etc/postgresql/${{ matrix.pg_version }}/testcluster/pg_hba.conf
          sudo -u postgres psql -p 5433 -c "ALTER SYSTEM SET autovacuum = off;"
          sudo pg_ctlcluster ${{ matrix.pg_version }} testcluster reload
          sudo -u postgres psql -p 5433 -c "CREATE USER runner WITH SUPERUSER;"

      - name: Run regression tests and TAP tests
        run: |
          # The TAP framework shells out to initdb/pg_ctl/psql, but Debian
          # keeps the versioned binaries outside the default PATH.
          export PATH="/usr/lib/postgresql/${{ matrix.pg_version }}/bin:$PATH"
          make PG_CONFIG="$PG_CONFIG" installcheck enable_tap_tests=yes PROVE=prove
        env:
          PGPORT: 5433

      - name: Show regression.diffs on failure
        if: failure()
        run: |
          if [ -f regression.diffs ]; then cat regression.diffs; else echo "regression.diffs not found"; fi

      - name: Show TAP test log on failure
        if: failure()
        run: cat tmp_check/log/regress_log_* 2>/dev/null || true

      - name: Cleanup cluster
        if: always()
        run: sudo pg_dropcluster ${{ matrix.pg_version }} testcluster --stop || true

  # PostgreSQL 19 is newer than the PGDG apt repository for Ubuntu 24.04.
  # Build it from source here; the normal Linux matrix covers 12-18.
  linux-pg19:
    name: Linux / PostgreSQL 19 (source)
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install build dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y build-essential bison flex curl ca-certificates libipc-run-perl

      - name: Build PostgreSQL 19 from source
        run: |
          curl -L -o pg.tar.gz \
            "https://github.com/postgres/postgres/archive/refs/heads/REL_19_STABLE.tar.gz"
          tar -xzf pg.tar.gz
          cd postgres-REL_19_STABLE
          ./configure \
            --prefix="$GITHUB_WORKSPACE/pg19" \
            --enable-tap-tests \
            --disable-nls \
            --without-icu \
            --without-readline \
            --without-zlib \
            --without-llvm
          make -j"$(nproc)"
          make install
          echo "PG_CONFIG=$GITHUB_WORKSPACE/pg19/bin/pg_config" >> "$GITHUB_ENV"
          echo "PATH=$GITHUB_WORKSPACE/pg19/bin:$PATH" >> "$GITHUB_ENV"
          echo "TAP_PERL_DIR=$GITHUB_WORKSPACE/postgres-REL_19_STABLE/src/test/perl" >> "$GITHUB_ENV"

      - name: Build extension
        run: make PG_CONFIG="$PG_CONFIG" with_llvm=no

      - name: Install extension
        run: make PG_CONFIG="$PG_CONFIG" install with_llvm=no

      - name: Setup test cluster
        run: |
          PGBIN="$(dirname "$PG_CONFIG")"
          PGDATA="$RUNNER_TEMP/pgdata"
          rm -rf "$PGDATA"
          "$PGBIN/initdb" -D "$PGDATA" -U postgres -A trust --no-sync
          "$PGBIN/pg_ctl" -D "$PGDATA" -l "$PGDATA/server.log" -o "-p 5433" -w start
          echo "PGDATA=$PGDATA" >> "$GITHUB_ENV"

      - name: Run regression tests and TAP tests
        run: |
          make PG_CONFIG="$PG_CONFIG" installcheck enable_tap_tests=yes PROVE=prove PROVE_FLAGS="-I $TAP_PERL_DIR"
        env:
          PGPORT: 5433
          PGHOST: 127.0.0.1
          PGUSER: postgres

      - name: Show regression.diffs on failure
        if: failure()
        run: cat regression.diffs 2>/dev/null || true

      - name: Show TAP test log on failure
        if: failure()
        run: cat tmp_check/log/regress_log_* 2>/dev/null || true

      - name: Stop test cluster
        if: always()
        run: |
          if [ -n "$PGDATA" ]; then
            PGBIN="$(dirname "$PG_CONFIG")"
            "$PGBIN/pg_ctl" -D "$PGDATA" -m immediate stop || true
          fi

  # macOS - full PostgreSQL version matrix (12 - 19).
  # Homebrew ships bottles for 14-18. PostgreSQL 12, 13, and 19 are compiled
  # from source (the matching source tree is downloaded anyway for its TAP
  # Perl modules). Homebrew's PostgreSQL is built without --enable-tap-tests, so the
  # TAP Perl modules are not installed. We download the matching PostgreSQL
  # source's src/test/perl tree and force the PGXS TAP target on, so that TAP
  # tests run here as well.
  macos:
    name: macOS / PostgreSQL ${{ matrix.pg_version }}
    runs-on: macos-latest

    strategy:
      fail-fast: false
      matrix:
        pg_version: [12, 13, 14, 15, 16, 17, 18, 19]

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install Perl, bison and flex
        run: |
          # bison/flex are needed to build PostgreSQL 12/13 from the source
          # archive below (it does not ship the generated parser files); the
          # Homebrew versions are keg-only, hence the PATH updates.
          brew install perl bison flex
          echo "$(brew --prefix perl)/bin" >> "$GITHUB_PATH"
          echo "$(brew --prefix bison)/bin" >> "$GITHUB_PATH"
          echo "$(brew --prefix flex)/bin" >> "$GITHUB_PATH"

      - name: Install PostgreSQL ${{ matrix.pg_version }} (Homebrew, 14-18)
        if: matrix.pg_version >= 14 && matrix.pg_version <= 18
        run: |
          brew install postgresql@${{ matrix.pg_version }}
          echo "PG_CONFIG=$(brew --prefix postgresql@${{ matrix.pg_version }})/bin/pg_config" >> "$GITHUB_ENV"

      - name: Install TAP prerequisites and fetch PostgreSQL ${{ matrix.pg_version }} sources
        run: |
          curl -L -o "$RUNNER_TEMP/ipc-run.tar.gz" \
            "https://cpan.metacpan.org/authors/id/T/TO/TODDR/IPC-Run-20231003.0.tar.gz"
          tar -xzf "$RUNNER_TEMP/ipc-run.tar.gz" -C "$RUNNER_TEMP"
          echo "PERL5LIB=$RUNNER_TEMP/IPC-Run-20231003.0/lib" >> "$GITHUB_ENV"
          curl -L -o "$RUNNER_TEMP/pg-src.tar.gz" \
            "https://github.com/postgres/postgres/archive/refs/heads/REL_${{ matrix.pg_version }}_STABLE.tar.gz"
          tar -xzf "$RUNNER_TEMP/pg-src.tar.gz" -C "$RUNNER_TEMP"
          echo "PG_SRC=$RUNNER_TEMP/postgres-REL_${{ matrix.pg_version }}_STABLE" >> "$GITHUB_ENV"
          echo "TAP_PERL_DIR=$RUNNER_TEMP/postgres-REL_${{ matrix.pg_version }}_STABLE/src/test/perl" >> "$GITHUB_ENV"

      - name: Build and install PostgreSQL ${{ matrix.pg_version }} from source (12, 13, 19)
        if: matrix.pg_version < 14 || matrix.pg_version == 19
        run: |
          PREFIX="$RUNNER_TEMP/pg-${{ matrix.pg_version }}"
          cd "$PG_SRC"
          ./configure \
            --prefix="$PREFIX" \
            --disable-nls \
            --without-icu \
            --without-readline \
            --without-zlib
          make -j"$(sysctl -n hw.ncpu)"
          make install
          echo "PG_CONFIG=$PREFIX/bin/pg_config" >> "$GITHUB_ENV"
          echo "$PREFIX/bin" >> "$GITHUB_PATH"

      - name: Build extension
        run: make PG_CONFIG="$PG_CONFIG" with_llvm=no

      - name: Install extension
        run: make PG_CONFIG="$PG_CONFIG" with_llvm=no install

      - name: Setup test cluster
        run: |
          PGBIN="$(dirname "$PG_CONFIG")"
          PGDATA="$RUNNER_TEMP/pgdata"
          rm -rf "$PGDATA"
          "$PGBIN/initdb" -D "$PGDATA" -U postgres -A trust --no-sync
          "$PGBIN/pg_ctl" -D "$PGDATA" -l "$PGDATA/server.log" -o "-p 5433" -w start
          echo "PGDATA=$PGDATA" >> "$GITHUB_ENV"

      - name: Run regression tests and TAP tests
        run: make PG_CONFIG="$PG_CONFIG" with_llvm=no installcheck enable_tap_tests=yes PROVE=prove PROVE_FLAGS="-I $TAP_PERL_DIR"
        env:
          PGPORT: 5433
          PGHOST: 127.0.0.1
          PGUSER: postgres

      - name: Show regression.diffs on failure
        if: failure()
        run: |
          if [ -f regression.diffs ]; then cat regression.diffs; else echo "regression.diffs not found"; fi

      - name: Show TAP test log on failure
        if: failure()
        run: cat tmp_check/log/regress_log_* 2>/dev/null || true

      - name: Show cluster log on failure
        if: failure()
        run: cat "$PGDATA/server.log" 2>/dev/null || true

      - name: Stop test cluster
        if: always()
        run: |
          PGBIN="$(dirname "$PG_CONFIG")"
          "$PGBIN/pg_ctl" -D "$PGDATA" -m immediate stop || true

  # Windows - full PostgreSQL version matrix (12 - 19).
  # The official PostgreSQL binaries are built with MSVC and do not ship PGXS,
  # so PostgreSQL is compiled from source with MinGW-w64 inside MSYS2 (the same
  # way MSYS2 packages PostgreSQL). The resulting install is cached between runs,
  # so only the first run pays the compilation cost.
  # TAP tests are driven by the MSYS Perl (osname=msys), which is already
  # supported by PostgreSQL's test framework.
  windows:
    name: Windows / PostgreSQL ${{ matrix.pg_version }}
    runs-on: windows-latest

    strategy:
      fail-fast: false
      matrix:
        pg_version: [12, 13, 14, 15, 16, 17, 18, 19]

    defaults:
      run:
        shell: msys2 {0}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup MSYS2 with MinGW-w64 toolchain and both Perl runtimes
        uses: msys2/setup-msys2@v2
        with:
          msystem: MINGW64
          update: true
          install: >-
            git
            make
            tar
            curl
            bison
            flex
            diffutils
            perl
            mingw-w64-x86_64-gcc
            mingw-w64-x86_64-perl

      # The TAP tests are driven by the MSYS (not MinGW) Perl: it reports
      # osname=msys, so IPC::Run uses its Unix code path and Win32API::File is
      # already provided by the MSYS perl package. This avoids the MinGW/CPAN
      # mess around Win32::Process / Win32::ShellQuote.
      - name: Install PostgreSQL TAP prerequisite (IPC::Run for MSYS Perl)
        run: |
          curl -L -o ipc-run.tar.gz \
            "https://cpan.metacpan.org/authors/id/T/TO/TODDR/IPC-Run-20231003.0.tar.gz"
          tar -xzf ipc-run.tar.gz
          cd IPC-Run-20231003.0
          /usr/bin/perl.exe Makefile.PL
          make
          make install

      - name: Fetch PostgreSQL ${{ matrix.pg_version }} sources and Perl test modules
        run: |
          curl -L -o pg-src.tar.gz \
            "https://github.com/postgres/postgres/archive/refs/heads/REL_${{ matrix.pg_version }}_STABLE.tar.gz"
          tar -xzf pg-src.tar.gz
          TAP_PERL_DIR="$(pwd)/postgres-REL_${{ matrix.pg_version }}_STABLE/src/test/perl"
          echo "TAP_PERL_DIR=$TAP_PERL_DIR" >> "$GITHUB_ENV"
          echo "PROVE_BIN=/usr/bin/perl.exe /usr/bin/core_perl/prove" >> "$GITHUB_ENV"
          # MSYS2's Perl is Cygwin-based (osname=cygwin), but it drives a native
          # Windows PostgreSQL. Two adjustments are needed:
          #  1. native Postgres cannot use the Cygwin Unix-socket directory, so
          #     force TCP connections for the test clusters;
          #  2. native psql writes CRLF to pipes and Cygwin Perl does not strip it
          #     (the test framework only normalizes when it detects Windows), so
          #     normalize line endings before the usual chomp().
          for f in "$TAP_PERL_DIR/PostgreSQL/Test/Utils.pm" "$TAP_PERL_DIR/TestLib.pm"; do
            if [ -f "$f" ] && grep -q 'use_unix_sockets' "$f"; then
              printf '%s\n' '' "\$use_unix_sockets = 0 if \$Config{osname} eq 'cygwin';" '1;' >> "$f"
            fi
          done
          # PostgreSQL 12 has no $use_unix_sockets knob: it picks TCP vs Unix
          # sockets from $TestLib::windows_os, which is false for the Cygwin
          # based MSYS Perl. Force TCP in PostgresNode's INIT block instead.
          if [ -f "$TAP_PERL_DIR/PostgresNode.pm" ] &&
             grep -q '\$use_tcp[[:space:]]*=[[:space:]]*\$TestLib::windows_os;' "$TAP_PERL_DIR/PostgresNode.pm"; then
            sed -i 's/^\([[:space:]]*\$use_tcp[[:space:]]*=[[:space:]]*\)\$TestLib::windows_os;/\1 1;/' "$TAP_PERL_DIR/PostgresNode.pm"
          fi
          for f in "$TAP_PERL_DIR/PostgreSQL/Test/Cluster.pm" "$TAP_PERL_DIR/PostgresNode.pm"; do
            if [ -f "$f" ]; then
              sed -i 's/chomp \$\$stdout;/$$stdout =~ s\/\\r\\n\/\\n\/g; chomp $$stdout;/g; s/chomp \$\$stderr;/$$stderr =~ s\/\\r\\n\/\\n\/g; chomp $$stderr;/g' "$f"
            fi
          done

      - name: Cache PostgreSQL ${{ matrix.pg_version }} build
        id: pg-cache
        uses: actions/cache@v4
        with:
          path: pg-${{ matrix.pg_version }}
          key: ${{ runner.os }}-pg-${{ matrix.pg_version }}-v2
          restore-keys: |
            ${{ runner.os }}-pg-${{ matrix.pg_version }}-

      - name: Build PostgreSQL ${{ matrix.pg_version }} from source
        if: steps.pg-cache.outputs.cache-hit != 'true'
        run: |
          set -e
          PREFIX="$(pwd)/pg-${{ matrix.pg_version }}"
          cd "postgres-REL_${{ matrix.pg_version }}_STABLE"
          # GCC 14 turned several formerly warning-only diagnostics (implicit
          # function declarations, incompatible pointer types, ...) into errors
          # by default. PostgreSQL 12 and 13 predate that change, so keep them
          # as warnings for the legacy branches.
          case "${{ matrix.pg_version }}" in
            12|13)
              export CFLAGS="-Wno-error=implicit-function-declaration -Wno-error=incompatible-pointer-types -Wno-error=int-conversion"
              ;;
          esac
          # PostgreSQL 19 checks libpq with libpq_check.pl, which runs native
          # Perl and needs a Windows path for nm.
          export NM="$(cygpath -w "$(command -v nm)")"
          ./configure \
            --prefix="$PREFIX" \
            --build="$MINGW_CHOST" \
            --host="$MINGW_CHOST" \
            --disable-nls \
            --without-icu \
            --without-readline \
            --without-zlib
          make -j"$(nproc)"
          make install

      - name: Set PG_CONFIG
        run: echo "PG_CONFIG=$(pwd)/pg-${{ matrix.pg_version }}/bin/pg_config" >> "$GITHUB_ENV"

      - name: Show PostgreSQL version
        run: '"$PG_CONFIG" --version'

      - name: Build extension
        run: make PG_CONFIG="$PG_CONFIG" with_llvm=no

      - name: Install extension
        run: make PG_CONFIG="$PG_CONFIG" with_llvm=no install

      - name: Setup test cluster
        run: |
          PGBIN="$(pwd)/pg-${{ matrix.pg_version }}/bin"
          PGDATA="$(pwd)/.pgdata-${{ matrix.pg_version }}"
          rm -rf "$PGDATA"
          "$PGBIN/initdb.exe" -D "$PGDATA" -U postgres -A trust --no-sync
          "$PGBIN/pg_ctl.exe" -D "$PGDATA" -l "$PGDATA/server.log" -o "-p 5433" -w start
          echo "PGDATA=$PGDATA" >> "$GITHUB_ENV"

      - name: Run regression tests and TAP tests
        env:
          PGPORT: 5433
          PGHOST: 127.0.0.1
          PGUSER: postgres
        run: |
          # pg_regress.exe links against libpq.dll, which lives in the
          # PostgreSQL bin directory, so it must be on PATH.  Convert the
          # Windows path with cygpath and append it: putting PostgreSQL's
          # native DLLs before /usr/bin can make MSYS2's make load a
          # conflicting libintl-8.dll and fail with STATUS_DLL_INIT_FAILED.
          PGBIN="$(dirname "$(cygpath -u "$PG_CONFIG")")"
          export PATH="$PATH:$PGBIN"
          make PG_CONFIG="$PG_CONFIG" installcheck enable_tap_tests=yes PROVE="$PROVE_BIN" PROVE_FLAGS="-I $TAP_PERL_DIR"

      - name: Show regression.diffs on failure
        if: failure()
        shell: pwsh
        run: |
          if (Test-Path regression.diffs) { Get-Content regression.diffs }
          else { Write-Host "regression.diffs not found" }

      - name: Show TAP test log on failure
        if: failure()
        shell: pwsh
        run: |
          Get-ChildItem tmp_check/log/regress_log_* -ErrorAction SilentlyContinue |
            ForEach-Object { Get-Content $_.FullName }

      - name: Show cluster log on failure
        if: failure()
        shell: pwsh
        run: |
          if ($env:PGDATA) {
            Get-Content "$env:PGDATA/server.log" -ErrorAction SilentlyContinue
          }

      - name: Upload Windows logs on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: windows-pg${{ matrix.pg_version }}-logs
          path: |
            regression.diffs
            regression.out
            results
            tmp_check
            **/server.log
          if-no-files-found: ignore

      - name: Stop test cluster
        if: always()
        shell: pwsh
        run: |
          if ($env:PGDATA) {
            # PGXS exports PGDATA as an MSYS path (/d/a/...); native
            # pg_ctl.exe needs a Windows path (D:/a/...).
            $pgdata = $env:PGDATA
            if ($pgdata -match '^/([a-zA-Z])/') {
              $pgdata = $Matches[1] + ":" + $pgdata.Substring(2)
            }
            & "$PWD\pg-${{ matrix.pg_version }}\bin\pg_ctl.exe" -D $pgdata -m immediate stop
            if ($LASTEXITCODE -ne 0) { exit 0 }
          }
