name: ci

on:
  push:
  pull_request:
  schedule:
    # weekly bit-rot check: distro, libcurl, and PG minor updates land
    # without any push to this repo
    - cron: '17 6 * * 1'

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        pg: [15, 16, 17, 18]
        sanitize: [0, 1]
    name: "pg${{ matrix.pg }}${{ matrix.sanitize == 1 && ' (asan+ubsan)' || '' }}"
    steps:
      - uses: actions/checkout@v4

      - name: Install PostgreSQL ${{ matrix.pg }} and libcurl
        run: |
          sudo apt-get update
          sudo apt-get install -y --no-install-recommends curl ca-certificates gnupg
          # pgdg has every major version regardless of the Ubuntu image default
          sudo install -d /usr/share/postgresql-common/pgdg
          sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
            https://www.postgresql.org/media/keys/ACCC4CF8.asc
          echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \
            | sudo tee /etc/apt/sources.list.d/pgdg.list
          sudo apt-get update
          sudo apt-get install -y --no-install-recommends \
            postgresql-${{ matrix.pg }} \
            postgresql-server-dev-${{ matrix.pg }} \
            libcurl4-openssl-dev \
            libipc-run-perl \
            pkg-config \
            gcc \
            make

      - name: Set build flags
        run: |
          if [ "${{ matrix.sanitize }}" = 1 ]; then
            echo "COPT=-Werror -fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer -Og" >> "$GITHUB_ENV"
            LIBASAN=$(gcc -print-file-name=libasan.so)
            LIBUBSAN=$(gcc -print-file-name=libubsan.so)
            # server is uninstrumented: preload sanitizer runtimes into
            # every backend; leak detection off (would flag the server
            # allocator itself)
            echo "SRV_ENV=LD_PRELOAD=$LIBASAN:$LIBUBSAN ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:log_path=/tmp/asan UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1:log_path=/tmp/ubsan" >> "$GITHUB_ENV"
          else
            echo "COPT=-Werror" >> "$GITHUB_ENV"
            echo "SRV_ENV=" >> "$GITHUB_ENV"
          fi

      - name: Build and install extension
        run: |
          make COPT="$COPT" with_llvm=no PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config
          sudo make COPT="$COPT" with_llvm=no PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config install

      - name: Mock tests
        run: |
          # the packaged cluster auto-starts on 5432; remove it, then use a
          # hand-rolled cluster so sanitizer runtimes can be preloaded
          # into every backend (mirrors test/docker_check.sh)
          for v in 14 15 16 17 18; do sudo pg_dropcluster --stop $v main 2>/dev/null || true; done
          sudo -u postgres env $SRV_ENV /usr/lib/postgresql/${{ matrix.pg }}/bin/initdb -D /tmp/data --locale=C
          sudo -u postgres env $SRV_ENV /usr/lib/postgresql/${{ matrix.pg }}/bin/pg_ctl -D /tmp/data -l /tmp/pg.log \
            -o "-k /var/run/postgresql -c listen_addresses=" start
          sudo -u postgres createdb typesafe_ci
          sudo -u postgres env PGHOST=/var/run/postgresql PGUSER=postgres PGDATABASE=typesafe_ci \
            sh test/ci.sh

      - name: Regression + TAP tests
        run: |
          sudo chown -R postgres "$PWD"
          # let the postgres user traverse into the workspace
          sudo chmod o+rx /home/runner /home/runner/work "$(dirname "$PWD")"
          sudo -u postgres env PGHOST=/var/run/postgresql PGUSER=postgres $SRV_ENV \
            make COPT="$COPT" with_llvm=no PG_CONFIG=/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config installcheck \
            || { cat regression.diffs 2>/dev/null; find tmp_check -name '*.log' -exec tail -50 {} + 2>/dev/null; exit 1; }

      - name: Check sanitizer reports
        if: matrix.sanitize == 1
        run: |
          if ls /tmp/asan.* /tmp/ubsan.* 2>/dev/null; then
            echo "=== SANITIZER REPORTS ==="
            cat /tmp/asan.* /tmp/ubsan.* 2>/dev/null
            exit 1
          fi
          echo "no sanitizer reports"

  installer:
    # the one-liner, as a user would run it, on a stock runner: build
    # dependencies come from the package manager, the key is stored via --db
    strategy:
      fail-fast: false
      matrix:
        pg: [15, 18]
    runs-on: ubuntu-latest
    name: "install.sh pg${{ matrix.pg }}"
    steps:
      - uses: actions/checkout@v4

      - name: Install PostgreSQL ${{ matrix.pg }} server only
        run: |
          sudo apt-get update
          sudo apt-get install -y --no-install-recommends curl ca-certificates
          sudo install -d /usr/share/postgresql-common/pgdg
          sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
            https://www.postgresql.org/media/keys/ACCC4CF8.asc
          echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \
            | sudo tee /etc/apt/sources.list.d/pgdg.list
          sudo apt-get update
          sudo apt-get install -y --no-install-recommends postgresql-${{ matrix.pg }}
          for v in 14 15 16 17 18; do sudo pg_dropcluster --stop $v main 2>/dev/null || true; done
          sudo pg_createcluster ${{ matrix.pg }} main --start

      - name: Run install.sh
        run: |
          sudo TYPESAFE_API_KEY=tsk_ci_dummy sh install.sh --source \
            --pg-config /usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config --db postgres

      - name: Verify
        run: |
          q() { sudo -u postgres psql -X -v ON_ERROR_STOP=1 -Atc "$1"; }
          q "SELECT extversion FROM pg_extension WHERE extname = 'typesafe'"
          f=$(q "SHOW typesafe.api_key_file")
          test "$(sudo cat "$f")" = tsk_ci_dummy
          # key is resolved: the call gets as far as the (unreachable) endpoint
          out=$(sudo -u postgres psql -X -Atc "SET typesafe.endpoint = 'http://127.0.0.1:9/'" \
            -c "SELECT typesafe_noul('a', 'b')" 2>&1 || true)
          echo "$out"
          echo "$out" | grep -q "HTTP request failed"
          # the key never reached the server log
          if sudo grep -rq tsk_ci_dummy /var/log/postgresql/; then exit 1; fi

      - name: Prebuilt path (as release.yml packages and install.sh consumes it)
        run: |
          sh scripts/package-binary.sh /usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config dist
          (cd dist && sha256sum *.tar.gz > SHA256SUMS)
          cd /tmp
          sudo TYPESAFE_RELEASE_URL="file://$GITHUB_WORKSPACE/dist" \
            sh "$GITHUB_WORKSPACE/install.sh" --pg-config /usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config \
            --db postgres 2>&1 | tee /tmp/install.log
          grep -q "installed prebuilt" /tmp/install.log

  docker:
    strategy:
      fail-fast: false
      matrix:
        pg: [15, 18]
    runs-on: ubuntu-latest
    name: "docker pg${{ matrix.pg }}"
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: docker build --build-arg PG_MAJOR=${{ matrix.pg }} -t pg_typesafe:ci .
      - name: Extension is enabled out of the box
        run: |
          echo tsk_ci_dummy > /tmp/typesafe.key
          docker run -d --name pg -e POSTGRES_PASSWORD=pw -e POSTGRES_DB=app \
            -v /tmp/typesafe.key:/run/secrets/typesafe_key:ro \
            -e TYPESAFE_API_KEY_FILE=/run/secrets/typesafe_key pg_typesafe:ci
          for i in $(seq 60); do
            docker exec pg pg_isready -U postgres -d app -h 127.0.0.1 >/dev/null 2>&1 && break
            sleep 1
          done
          docker exec pg psql -U postgres -d app -v ON_ERROR_STOP=1 \
            -c "SET typesafe.mock_response = '{\"model\":\"m\",\"answers\":{\"flag\":{\"type\":\"noul\",\"noul\":0.5}},\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}'" \
            -c "SELECT typesafe_noul('a', 'b')"
          docker exec pg psql -U postgres -v ON_ERROR_STOP=1 -c "CREATE DATABASE later" \
            -d postgres
          test "$(docker exec pg psql -U postgres -d later -Atc "SELECT count(*) FROM pg_extension WHERE extname = 'typesafe'")" = 1
          test "$(docker exec pg psql -U postgres -Atc "SHOW typesafe.api_key_file")" = /run/secrets/typesafe_key

  shellcheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: shellcheck -s sh install.sh scripts/package-binary.sh docker/initdb-typesafe.sh test/ci.sh

  test-macos:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4

      - name: Install PostgreSQL 17
        run: |
          brew install postgresql@17
          echo "/opt/homebrew/opt/postgresql@17/bin" >> "$GITHUB_PATH"

      - name: Build and install extension
        run: |
          export PATH=/opt/homebrew/opt/postgresql@17/bin:$PATH
          # pg_config's CFLAGS carry flags from the clang Homebrew built
          # with; the runner's older clang warns about them, so keep
          # -Werror from promoting that mismatch noise
          make COPT="-Werror -Wno-ignored-optimization-argument -Wno-unknown-warning-option"
          make COPT="-Werror -Wno-ignored-optimization-argument -Wno-unknown-warning-option" install

      - name: Regression tests
        run: |
          export PATH=/opt/homebrew/opt/postgresql@17/bin:$PATH LC_ALL=C LANG=C
          initdb -D /tmp/data --locale=C -U postgres
          pg_ctl -D /tmp/data -l /tmp/pg.log -o "-k /tmp -c listen_addresses=" start
          PGHOST=/tmp make installcheck PGUSER=postgres \
            || { cat regression.diffs 2>/dev/null; exit 1; }

      - name: install.sh
        run: |
          PGHOST=/tmp PGUSER=postgres sh install.sh --source \
            --pg-config /opt/homebrew/opt/postgresql@17/bin/pg_config --db postgres
