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 - full PostgreSQL version matrix (12 - 18) using the PGDG packages.
  # Regression tests and TAP tests are both 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"

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

      - 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

  # macOS - full PostgreSQL version matrix (12 - 18).
  # Homebrew ships bottles for 14 and newer. postgresql@12 and postgresql@13
  # are disabled in Homebrew, so those two versions are compiled from source
  # instead (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]

    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+)
        if: matrix.pg_version >= 14
        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)
        if: matrix.pg_version < 14
        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"

      - name: Install extension
        run: make PG_CONFIG="$PG_CONFIG" 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" 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 - 18).
  # 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]

    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
          ./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"

      - name: Install extension
        run: make PG_CONFIG="$PG_CONFIG" 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.
          export PATH="$(dirname "$PG_CONFIG"):$PATH"
          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()
        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: '"$(pwd)/pg-${{ matrix.pg_version }}/bin/pg_ctl.exe" -D "$PGDATA" -m immediate stop || true'
