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 - every PostgreSQL version on x86_64 and ARM64, built from source.
  linux:
    name: Linux ${{ matrix.arch }} / PostgreSQL ${{ matrix.pg }}
    runs-on: ${{ matrix.os }}
    timeout-minutes: 45
    env:
      PG: ${{ matrix.pg }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, ubuntu-24.04-arm]
        arch: [x86_64, arm64]
        pg: [12, 13, 14, 15, 16, 17, 18, 19]
        exclude:
          - os: ubuntu-latest
            arch: arm64
          - os: ubuntu-24.04-arm
            arch: x86_64

    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 ${{ matrix.pg }} from source
        run: |
          curl -L -o pg.tar.gz \
            "https://github.com/postgres/postgres/archive/refs/heads/REL_${{ matrix.pg }}_STABLE.tar.gz"
          tar -xzf pg.tar.gz
          PG_SRC="$PWD/postgres-REL_$PG"_STABLE
          PREFIX="$PWD/pg$PG"
          cd "$PG_SRC"
          ./configure \
            --prefix="$PREFIX" \
            --enable-tap-tests \
            --disable-nls \
            --without-icu \
            --without-readline \
            --without-zlib \
            --without-llvm
          make -j"$(nproc)"
          make install
          echo "PG_CONFIG=$PREFIX/bin/pg_config" >> "$GITHUB_ENV"
          echo "PATH=$PREFIX/bin:$PATH" >> "$GITHUB_ENV"
          echo "TAP_PERL_DIR=$PG_SRC/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 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 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

  # FreeBSD - every PostgreSQL version on amd64 and arm64.
  freebsd:
    name: FreeBSD ${{ matrix.arch }} / PostgreSQL ${{ matrix.pg }}
    runs-on: ubuntu-latest
    timeout-minutes: 60
    env:
      PG: ${{ matrix.pg }}
    strategy:
      fail-fast: false
      matrix:
        arch: [x86_64, aarch64]
        pg: [12, 13, 14, 15, 16, 17, 18, 19]

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

      - name: Resolve PostgreSQL source checksum
        id: pg-src
        run: |
          curl -L -o pg-src-cache.tar.gz \
            "https://github.com/postgres/postgres/archive/refs/heads/REL_${{ matrix.pg }}_STABLE.tar.gz"
          echo "sha=$(sha256sum pg-src-cache.tar.gz | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"

      - name: Restore FreeBSD PostgreSQL build
        id: pg-cache
        if: matrix.pg == 12 || matrix.pg == 13 || matrix.pg == 19
        uses: actions/cache/restore@v4
        with:
          path: pg-cache
          key: freebsd-14-v1-${{ matrix.arch }}-pg${{ matrix.pg }}-${{ steps.pg-src.outputs.sha }}

      - name: Test in FreeBSD
        uses: vmactions/freebsd-vm@v1
        with:
          release: "14"
          arch: ${{ matrix.arch }}
          mem: 4096
          cpu: 4
          envs: 'PG'
          prepare: |
            pkg update
            pkg install -y gmake bison flex curl p5-IPC-Run
          run: |
            set -e
            PG_SRC="/tmp/pg-src-$PG"
            PREFIX="/usr/local/pg$PG"
            rm -rf "$PG_SRC"
            tar -xzf "$GITHUB_WORKSPACE/pg-src-cache.tar.gz" -C /tmp
            mv "/tmp/postgres-REL_$PG"_STABLE "$PG_SRC"

            CACHE_FILE="$GITHUB_WORKSPACE/pg-cache/pg$PG.tar.gz"
            if [ -f "$CACHE_FILE" ]; then
              tar -xzf "$CACHE_FILE" -C /
              PG_CONFIG="$PREFIX/bin/pg_config"
            elif pkg install -y "postgresql$PG-server" "postgresql$PG-client"; then
              PG_CONFIG=/usr/local/bin/pg_config
            else
              cd "$PG_SRC"
              ./configure \
                --prefix="$PREFIX" \
                --enable-tap-tests \
                --disable-nls \
                --without-icu \
                --without-readline \
                --without-zlib \
                --without-llvm
              gmake -j"$(sysctl -n hw.ncpu)"
              gmake install
              PG_CONFIG="$PREFIX/bin/pg_config"

              mkdir -p "$GITHUB_WORKSPACE/pg-cache"
              tar -czf "$CACHE_FILE" -C / "usr/local/pg$PG"
              chmod 644 "$CACHE_FILE"
            fi

            if ! id postgres >/dev/null 2>&1; then
              pw useradd postgres -m -s /bin/sh
            fi
            chown -R postgres:postgres "$GITHUB_WORKSPACE"

            cd "$GITHUB_WORKSPACE"
            gmake PG_CONFIG="$PG_CONFIG" with_llvm=no
            gmake PG_CONFIG="$PG_CONFIG" install with_llvm=no

            PGBIN="$(dirname "$PG_CONFIG")"
            PGDATA="/tmp/pgdata-$PG"
            rm -rf "$PGDATA"
            su -m postgres -c "$PGBIN/initdb -D $PGDATA -U postgres -A trust --no-sync"
            su -m postgres -c "$PGBIN/pg_ctl -D $PGDATA -l $PGDATA/server.log -o '-p 5433' -w start"

            TAP_ARGS=""
            if command -v prove >/dev/null 2>&1; then
              TAP_ARGS="enable_tap_tests=yes PROVE=prove PROVE_FLAGS='-I $PG_SRC/src/test/perl'"
            fi
            su -m postgres -c "cd '$GITHUB_WORKSPACE' && PATH='$PGBIN:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin' PGHOST=/tmp PGPORT=5433 PGUSER=postgres gmake PG_CONFIG='$PG_CONFIG' installcheck $TAP_ARGS"

            su -m postgres -c "$PGBIN/pg_ctl -D $PGDATA -m immediate stop"

      - name: Save FreeBSD PostgreSQL build
        if: always() && (matrix.pg == 12 || matrix.pg == 13 || matrix.pg == 19) && steps.pg-cache.outputs.cache-hit != 'true' && hashFiles('pg-cache/*.tar.gz') != ''
        continue-on-error: true
        uses: actions/cache/save@v4
        with:
          path: pg-cache
          key: freebsd-14-v1-${{ matrix.arch }}-pg${{ matrix.pg }}-${{ steps.pg-src.outputs.sha }}

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

  # 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 }
          }
