# .github/workflows/release.yml
#
# Build and publish release artifacts when a version tag (v*) is pushed.
# Produces per-PG-version tarballs containing the compiled extension
# (.so, .control, .sql) and attaches them to a GitHub Release.

name: release

on:
  push:
    tags:
      - "v*"

permissions:
  contents: write

env:
  CARGO_TERM_COLOR: always
  CARGO_INCREMENTAL: "0"
  PGRX_VERSION: "0.17.0"
  RUST_TOOLCHAIN: "1.90.0"

jobs:
  # The mirror will replay v0.14.0 and pg_mentat's old v1.2.1..v1.6.1 tags to
  # GitHub; each would otherwise start a release build. Gate: only tags
  # >= v1.7.0 (the first merged-repo release) proceed.
  gate:
    name: gate tag >= v1.7.0
    runs-on: ubuntu-latest
    steps:
      - name: Check tag version
        run: |
          TAG="${GITHUB_REF_NAME}"
          echo "tag: ${TAG}"
          case "${TAG}" in
            v0.*|v1.[0-6].*|v1.[0-6])
              echo "::notice::${TAG} is a pre-merge tag; skipping release build."
              exit 1
              ;;
          esac
          # Belt-and-suspenders numeric check: TAG must sort >= v1.7.0.
          MIN="v1.7.0"
          LOWEST="$(printf '%s\n%s\n' "${TAG}" "${MIN}" | sort -V | head -1)"
          if [ "${LOWEST}" != "${MIN}" ] && [ "${TAG}" != "${MIN}" ]; then
            echo "::notice::${TAG} sorts below ${MIN}; skipping."
            exit 1
          fi
          echo "${TAG} >= ${MIN}: proceeding."

  build:
    name: Build pg${{ matrix.pg_version }}
    needs: gate
    runs-on: ubuntu-latest
    timeout-minutes: 20
    strategy:
      fail-fast: false
      matrix:
        pg_version: [15, 16, 17]

    steps:
      - uses: actions/checkout@v4

      - name: Install PostgreSQL ${{ matrix.pg_version }}
        run: |
          # The default runner apt repos only carry one PG major; add PGDG so
          # pg15/16/17 are all installable.
          sudo apt-get install -y --no-install-recommends curl ca-certificates gnupg lsb-release
          sudo install -d /usr/share/postgresql-common/pgdg
          sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
            --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
          echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://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_version }} \
              postgresql-server-dev-${{ matrix.pg_version }} \
              postgresql-client-${{ matrix.pg_version }} \
              libpq-dev build-essential pkg-config libclang-dev clang

      - name: Install Rust ${{ env.RUST_TOOLCHAIN }}
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: ${{ env.RUST_TOOLCHAIN }}

      - name: Cache cargo
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: release-pg${{ matrix.pg_version }}-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: release-pg${{ matrix.pg_version }}-${{ runner.os }}-

      - name: Install cargo-pgrx ${{ env.PGRX_VERSION }}
        run: cargo install --locked cargo-pgrx --version "${PGRX_VERSION}"

      - name: pgrx init (system PG${{ matrix.pg_version }})
        run: |
          cargo pgrx init --pg${{ matrix.pg_version }} \
            $(command -v /usr/lib/postgresql/${{ matrix.pg_version }}/bin/pg_config || which pg_config)

      - name: cargo pgrx package
        working-directory: crates/pg/pg_mentat
        run: |
          cargo pgrx package --no-default-features \
            --features pg${{ matrix.pg_version }} \
            --pg-config /usr/lib/postgresql/${{ matrix.pg_version }}/bin/pg_config

      - name: Create release tarball
        run: |
          # cargo pgrx package writes to the WORKSPACE target dir, which is at
          # the repo root (target/), not pg_mentat/target/, even though the
          # package command runs with working-directory: pg_mentat.
          STAGING="pg_mentat-pg${{ matrix.pg_version }}"
          mkdir -p "${STAGING}"

          # Copy the compiled extension files from the pgrx package output.
          find target -name "pg_mentat.so" -path "*release*" -exec cp {} "${STAGING}/" \;
          find target -name "pg_mentat--*.sql" -exec cp {} "${STAGING}/" \; 2>/dev/null || true
          find target -name "pg_mentat.control" -exec cp {} "${STAGING}/" \; 2>/dev/null || true

          # Fall back to the source control file if the packaged one wasn't found.
          if [ ! -f "${STAGING}/pg_mentat.control" ]; then
            cp crates/pg/pg_mentat/pg_mentat.control "${STAGING}/"
          fi

          # Fail loud if the shared library is missing (empty tarball is useless).
          if [ ! -f "${STAGING}/pg_mentat.so" ]; then
            echo "ERROR: pg_mentat.so not found under target/ after packaging" >&2
            find target -name 'pg_mentat.so' 2>/dev/null || true
            exit 1
          fi

          tar czf "pg_mentat-pg${{ matrix.pg_version }}.tar.gz" "${STAGING}"

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: pg_mentat-pg${{ matrix.pg_version }}
          path: pg_mentat-pg${{ matrix.pg_version }}.tar.gz
          retention-days: 5

  # Build the embedded SQLite CLI (crates/sqlite/cli) as a plain Linux binary,
  # attached to the release alongside the per-PG extension tarballs.
  build-cli:
    name: Build mentat-cli (Linux x86_64)
    needs: gate
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4

      - name: Install build deps
        run: |
          sudo apt-get update
          sudo apt-get install -y --no-install-recommends \
              build-essential pkg-config libsqlite3-dev libssl-dev

      - name: Install Rust ${{ env.RUST_TOOLCHAIN }}
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: ${{ env.RUST_TOOLCHAIN }}

      - name: Cache cargo
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: release-cli-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: release-cli-${{ runner.os }}-

      - name: Build mentat-cli
        run: cargo build --release --locked -p mentat_cli

      - name: Package CLI tarball
        run: |
          BIN="$(ls target/release/mentat_cli target/release/mentat-cli 2>/dev/null | head -1)"
          if [ -z "${BIN}" ]; then echo "ERROR: mentat-cli binary not found" >&2; exit 1; fi
          STAGING="mentat-cli-linux-x86_64"
          mkdir -p "${STAGING}"
          cp "${BIN}" "${STAGING}/mentat-cli"
          tar czf "mentat-cli-linux-x86_64.tar.gz" "${STAGING}"

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: mentat-cli-linux-x86_64
          path: mentat-cli-linux-x86_64.tar.gz
          retention-days: 5

  # SQLite loadable extension (crates/sqlite/ext) -> libmentat_sqlite.so.
  build-sqlite-ext:
    name: Build SQLite extension (Linux x86_64)
    needs: gate
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
      - name: Install build deps
        run: |
          sudo apt-get update
          sudo apt-get install -y --no-install-recommends build-essential pkg-config libssl-dev sqlite3
      - name: Install Rust ${{ env.RUST_TOOLCHAIN }}
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: ${{ env.RUST_TOOLCHAIN }}
      - name: Build + smoke test
        run: |
          cargo build --release --locked -p mentat_sqlite_ext
          (cd crates/sqlite/ext && EXT="$GITHUB_WORKSPACE/target/release/libmentat_sqlite" bash test/smoke.sh)
      - name: Package
        run: |
          STAGING="mentat-sqlite-ext-linux-x86_64"
          mkdir -p "${STAGING}"
          cp target/release/libmentat_sqlite.so crates/sqlite/ext/README.md "${STAGING}/"
          tar czf "${STAGING}.tar.gz" "${STAGING}"
      - uses: actions/upload-artifact@v4
        with:
          name: mentat-sqlite-ext-linux-x86_64
          path: mentat-sqlite-ext-linux-x86_64.tar.gz
          retention-days: 5

  # DuckDB loadable extension (crates/duckdb) -> mentat.duckdb_extension
  # (unsigned; the signed build comes from the DuckDB Community Extensions
  # registry once the descriptor PR is merged).
  build-duckdb-ext:
    name: Build DuckDB extension (Linux x86_64, DuckDB v1.5.5)
    needs: gate
    runs-on: ubuntu-latest
    timeout-minutes: 40
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - name: Install build deps
        run: |
          sudo apt-get update
          sudo apt-get install -y --no-install-recommends build-essential pkg-config libssl-dev unzip python3 python3-venv
      - name: Install Rust ${{ env.RUST_TOOLCHAIN }}
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: ${{ env.RUST_TOOLCHAIN }}
      - name: Build (release) + smoke test
        working-directory: crates/duckdb
        run: |
          make configure
          make release
          curl -sSL https://github.com/duckdb/duckdb/releases/download/v1.5.5/duckdb_cli-linux-amd64.zip -o /tmp/duckdb.zip
          unzip -o /tmp/duckdb.zip -d /tmp/duckdb-cli
          EXT="$PWD/build/release/mentat.duckdb_extension" DUCKDB=/tmp/duckdb-cli/duckdb bash test/smoke.sh
      - name: Package
        run: |
          STAGING="mentat-duckdb-v1.5.5-linux_amd64"
          mkdir -p "${STAGING}"
          cp crates/duckdb/build/release/mentat.duckdb_extension crates/duckdb/README.md "${STAGING}/"
          tar czf "${STAGING}.tar.gz" "${STAGING}"
      - uses: actions/upload-artifact@v4
        with:
          name: mentat-duckdb-v1.5.5-linux_amd64
          path: mentat-duckdb-v1.5.5-linux_amd64.tar.gz
          retention-days: 5

  release:
    name: Create GitHub Release
    needs: [build, build-cli, build-sqlite-ext, build-duckdb-ext]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts

      - name: Gather tarballs
        run: |
          mkdir -p release-assets
          find artifacts -name "*.tar.gz" -exec mv {} release-assets/ \;
          ls -la release-assets/

      - name: Create release
        uses: softprops/action-gh-release@v2
        with:
          generate_release_notes: true
          files: release-assets/*.tar.gz
          fail_on_unmatched_files: true
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  # ------------------------------------------------------------------------
  # Registry publishing (see docs/registry-publishing.md).
  # ------------------------------------------------------------------------

  # PGXN: fully automated. Bundles the repo per META.json and uploads to the
  # PGXN Manager. One-time setup: create + get a PGXN account approved and add
  # the PGXN_USERNAME / PGXN_PASSWORD repo secrets. If the secrets are absent
  # (e.g. before the account exists) the job no-ops instead of failing.
  pgxn-release:
    name: Publish to PGXN
    needs: [gate, release]
    runs-on: ubuntu-latest
    container: pgxn/pgxn-tools
    steps:
      - name: Skip if PGXN secrets are not configured
        id: check
        env:
          PGXN_USERNAME: ${{ secrets.PGXN_USERNAME }}
          PGXN_PASSWORD: ${{ secrets.PGXN_PASSWORD }}
        run: |
          if [ -z "${PGXN_USERNAME}" ] || [ -z "${PGXN_PASSWORD}" ]; then
            echo "::notice::PGXN_USERNAME/PGXN_PASSWORD not set; skipping PGXN release."
            echo "skip=true" >> "$GITHUB_OUTPUT"
          fi
      - uses: actions/checkout@v4
        if: steps.check.outputs.skip != 'true'
        with:
          submodules: false
      - name: Verify META.json version matches the tag
        if: steps.check.outputs.skip != 'true'
        run: |
          VER="${GITHUB_REF_NAME#v}"
          MJSON_VER="$(perl -MJSON::PP -E 'say decode_json(join q{}, <>)->{version}' META.json)"
          if [ "$MJSON_VER" != "$VER" ]; then
            echo "ERROR: META.json version ($MJSON_VER) != tag ($VER). Bump META.json." >&2
            exit 1
          fi
      - name: Bundle
        if: steps.check.outputs.skip != 'true'
        run: pgxn-bundle
      - name: Release on PGXN
        if: steps.check.outputs.skip != 'true'
        env:
          PGXN_USERNAME: ${{ secrets.PGXN_USERNAME }}
          PGXN_PASSWORD: ${{ secrets.PGXN_PASSWORD }}
        run: pgxn-release

  # DuckDB Community Extensions: opens/updates a PR to
  # duckdb/community-extensions bumping extensions/mentat/description.yml to
  # this tag's version + commit SHA. The upstream merge is a human gate; the
  # FIRST submission is done by hand (add the descriptor there once). One-time
  # setup: a maintainer fork gburd/community-extensions and a COMMUNITY_EXT_PAT
  # secret (repo + pull-requests scope). No-ops if the PAT is absent.
  duckdb-registry-pr:
    name: PR to duckdb/community-extensions
    needs: [gate, release]
    runs-on: ubuntu-latest
    steps:
      - name: Skip if the community-extensions PAT is not configured
        id: check
        env:
          COMMUNITY_EXT_PAT: ${{ secrets.COMMUNITY_EXT_PAT }}
        run: |
          if [ -z "${COMMUNITY_EXT_PAT}" ]; then
            echo "::notice::COMMUNITY_EXT_PAT not set; skipping DuckDB registry PR."
            echo "skip=true" >> "$GITHUB_OUTPUT"
          fi
      - uses: actions/checkout@v4
        if: steps.check.outputs.skip != 'true'
        with:
          path: mentat
      - name: Check out the maintainer's fork of community-extensions
        if: steps.check.outputs.skip != 'true'
        uses: actions/checkout@v4
        with:
          repository: gburd/community-extensions
          token: ${{ secrets.COMMUNITY_EXT_PAT }}
          path: community-extensions
      - name: Update the mentat descriptor to this tag
        if: steps.check.outputs.skip != 'true'
        run: |
          VER="${GITHUB_REF_NAME#v}"
          SHA="${GITHUB_SHA}"
          mkdir -p community-extensions/extensions/mentat
          sed -e "s/^  version:.*/  version: ${VER}/" \
              -e "s/^  ref:.*/  ref: ${SHA}/" \
              mentat/crates/duckdb/community-extensions/description.yml \
              > community-extensions/extensions/mentat/description.yml
      - name: Open/update the upstream PR
        if: steps.check.outputs.skip != 'true'
        uses: peter-evans/create-pull-request@v6
        with:
          token: ${{ secrets.COMMUNITY_EXT_PAT }}
          path: community-extensions
          push-to-fork: gburd/community-extensions
          branch: mentat-${{ github.ref_name }}
          commit-message: "mentat ${{ github.ref_name }}"
          title: "Update mentat to ${{ github.ref_name }}"
          body: |
            Automated descriptor bump for mentat ${{ github.ref_name }}
            (commit ${{ github.sha }}). Built against DuckDB v1.5.5.
