# Every push to main, every pull request, and a weekly run re-prove the attacks. For a
# security extension, CI is not "the build passes": it is the claim that no guarantee broke,
# re-proved continuously.
#
# The `verify` job is `make clean-machine` -- the exact command anyone can run locally -- for
# one PostgreSQL major: a fresh Debian container, PostgreSQL from PGDG, cargo-pgrx 0.19.2,
# pg_living_assertions pinned, the repository as committed, `make verify` as a non-root user.
# One definition of "verified", not a second one in YAML that can drift from it.
#
# PostgreSQL 19 is a MOVING beta: its job runs but is `continue-on-error`, so a break in the
# beta does not turn the whole run red. Branch protection requires both "verify (PostgreSQL 18)"
# and "end-to-end (contrast + a real MCP client)" to block merges (Settings -> Branches); 19 is
# NOT required, and the weekly schedule is there to catch it drifting early.
#
# Third-party actions are pinned to a full commit SHA (the version is in the comment) so a
# moved tag cannot change what runs. Bump them through Dependabot (.github/dependabot.yml).
name: verify

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
  workflow_dispatch:
  schedule:
    - cron: '0 7 * * 1'   # Mondays 07:00 UTC: catch a moving PostgreSQL 19 beta, run the measurements

permissions:
  contents: read     # the verify job widens this to issues: write for the PG19 cron alert

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  verify:
    name: verify (PostgreSQL ${{ matrix.pg }})
    runs-on: ubuntu-24.04
    timeout-minutes: 60
    permissions:
      contents: read
      issues: write     # open an issue when the PostgreSQL 19 beta breaks on the weekly run
    # PostgreSQL 19 is a beta that moves under us: let it run and report, without failing the run.
    continue-on-error: ${{ matrix.pg == 19 }}
    strategy:
      fail-fast: false
      matrix:
        pg: [18, 19]
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false   # keep the token (issues: write) out of .git/config, where later steps could read it

      - name: clean machine, PostgreSQL ${{ matrix.pg }}
        run: PG_MAJOR=${{ matrix.pg }} ENGINE=docker bash tests/clean-machine/run.sh

      # continue-on-error keeps the run green when the PostgreSQL 19 beta breaks, so the
      # weekly schedule would otherwise say nothing. Open an issue instead -- that is the
      # heads-up the cron exists for. One at a time: skip if one is already open.
      - name: open an issue if the PostgreSQL 19 beta broke on the weekly run
        if: failure() && matrix.pg == 19 && github.event_name == 'schedule'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          OPEN=$(gh issue list --state open --json title \
            --jq '[.[] | select(.title | startswith("PostgreSQL 19 beta broke"))] | length')
          if [ "$OPEN" = "0" ]; then
            gh issue create \
              --title "PostgreSQL 19 beta broke \`make verify\`" \
              --body "The weekly scheduled run of \`verify (PostgreSQL 19)\` failed. The 19 beta moves under us; this is the heads-up to look. Run: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID ($(date -u +%F))."
          fi

      - name: logs
        if: always()
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: verify-logs-pg${{ matrix.pg }}
          path: target/clean-machine-pg${{ matrix.pg }}
          if-no-files-found: warn

  # The things the README shows, re-proved on a fresh cluster -- the same commands anyone
  # runs after a fork:
  #   * `make contrast`: the same statement an ordinary connection runs (gone) and the gate
  #     refuses, and the rich verification the gate returns instead of a bare result.
  #   * `make mcp`: a real MCP client, through gated-mcp (which connects AS THE AGENT ROLE and
  #     holds no power), can only operate the gate -- the official @modelcontextprotocol
  #     client, both protocol eras.
  #   * `make transfer`: a MEASUREMENT, not a pass/fail check, so it runs only on the schedule
  #     and on demand -- what the JSON pipe costs vs a native connection.
  # Every change is checked from outside the gate by a superuser. None depends on the
  # PostgreSQL major, so one is enough to prove them.
  end-to-end:
    name: end-to-end (contrast + a real MCP client)
    runs-on: ubuntu-24.04
    timeout-minutes: 30
    permissions:
      contents: read
      issues: write     # open an issue if the weekly fuzz run breaks (mirrors the PG19 alert)
    env:
      PG_MAJOR: "18"
      PGRX_VERSION: "0.19.2"
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false   # keep the token (issues: write) out of .git/config, where make mcp (npm/bun), cargo and curl | sh could read it

      - name: PostgreSQL ${{ env.PG_MAJOR }} and a build toolchain, from PGDG
        run: |
          sudo install -d /usr/share/postgresql-common/pgdg
          sudo curl -fsSo /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] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main $PG_MAJOR" \
            | sudo tee /etc/apt/sources.list.d/pgdg.list
          sudo apt-get update
          sudo apt-get install -y --no-install-recommends \
            postgresql-$PG_MAJOR postgresql-server-dev-$PG_MAJOR \
            build-essential clang libclang-dev pkg-config
          echo "/usr/lib/postgresql/$PG_MAJOR/bin" >> "$GITHUB_PATH"

      # Caches ~/.cargo (registry, git and the cargo-pgrx binary) and target/, keyed on
      # Cargo.lock -- so cargo-pgrx is not reinstalled and the extension is not rebuilt from
      # scratch on every run.
      - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2

      - name: cargo-pgrx ${{ env.PGRX_VERSION }}
        run: |
          command -v cargo-pgrx || cargo install cargo-pgrx --version "$PGRX_VERSION" --locked
          cargo pgrx init --pg$PG_MAJOR "/usr/lib/postgresql/$PG_MAJOR/bin/pg_config"

      - name: the gate vs an ordinary connection, side by side
        run: make contrast PG_CONFIG=/usr/lib/postgresql/$PG_MAJOR/bin/pg_config

      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
        with:
          node-version: "20"
      - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0

      - name: a real MCP client operates the gate
        run: make mcp PG_CONFIG=/usr/lib/postgresql/$PG_MAJOR/bin/pg_config

      - name: what the JSON pipe costs, measured
        if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
        run: make transfer PG_CONFIG=/usr/lib/postgresql/$PG_MAJOR/bin/pg_config

      # The GENERATIVE fuzz is a weekly campaign: a seed derived from the run id each week, so
      # coverage widens over time (the teeth and the oracle below run every push instead). On a
      # real escape the seed is withheld from the public log; the maintainer recomputes it from the
      # run id with the repo secret (see tests/fuzz.py).
      - name: install uv (pinned) for the fuzzer
        run: |
          curl -LsSf https://astral.sh/uv/0.12.23/install.sh | sh    # pinned, like every other tool here
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"

      # FUZZ_ITERS=0 runs the regression teeth and the differential oracle with no random inputs:
      # the teeth assert each known hole is refused by the RIGHT check -- direct execution, DROP and
      # GRANT, and the amplification class (cascade, trigger, rule, opaque function, the commit
      # backstop and the allow-list) -- and the max_rows / writing-CTE oracle (48 fixed cases) is
      # the exact regression for the 0.2.1-0.2.3 bugs. So it runs on EVERY push and PR, not just the
      # weekly campaign; being deterministic it needs no seed (hence no FUZZ_SEED_KEY here).
      - name: regression teeth + differential oracle (every push)
        if: github.event_name == 'push' || github.event_name == 'pull_request'
        run: FUZZ_ITERS=0 make fuzz PG_CONFIG=/usr/lib/postgresql/$PG_MAJOR/bin/pg_config

      - name: fuzz the gate
        id: fuzz
        if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
        env:
          # The seed is HMAC(this secret, run id); unset -> random. Scoped to THIS step, which runs
          # only Python -- not the `curl | sh` above, nor the npm/bun/cargo steps -- so the secret
          # is never in the environment of third-party tooling.
          FUZZ_SEED_KEY: ${{ secrets.FUZZ_SEED_KEY }}
        run: FUZZ_ITERS=${FUZZ_ITERS:-5000} make fuzz PG_CONFIG=/usr/lib/postgresql/$PG_MAJOR/bin/pg_config

      # A weekly fuzz break must not be silent (the job is not merge-gating). Open ONE issue --
      # public, so no seed and no input, just the run link; the maintainer recomputes the seed
      # from the run id with the repo secret. Mirrors the PG19 beta alert above.
      - name: open an issue if the weekly fuzz run broke
        if: failure() && steps.fuzz.outcome == 'failure' && github.event_name == 'schedule'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          OPEN=$(gh issue list --state open --json title \
            --jq '[.[] | select(.title | startswith("the weekly fuzz run"))] | length')
          if [ "$OPEN" = "0" ]; then
            gh issue create \
              --title "the weekly fuzz run found something in \`make fuzz\`" \
              --body "The weekly scheduled fuzz failed (an escape, a crash, or an unhealthy run). Details are withheld from this public issue; recompute the seed from the run id with the repo's FUZZ_SEED_KEY and re-run \`make fuzz\` locally. Run: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID ($(date -u +%F))."
          fi

      - name: logs
        if: always()
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: end-to-end-gated-mcp-log
          path: gated-mcp.log
          if-no-files-found: warn
