name: Release PostgreSQL 14-18 Linux artifacts

on:
  workflow_run:
    workflows: ["CI"]
    branches: ["master"]
    types: [completed]
  workflow_dispatch:
    inputs:
      ref:
        description: Exact commit SHA to release
        required: false
        type: string
      ci_run_id:
        description: Successful CI run for the exact release commit
        required: false
        type: string
      publish_pgxn:
        description: Dispatch PGXN publication after release
        required: false
        default: false
        type: boolean

permissions:
  contents: read

concurrency:
  group: pg_local_cache-release
  cancel-in-progress: false

jobs:
  metadata:
    if: >-
      github.event_name == 'workflow_dispatch' ||
      (
        github.event.workflow_run.conclusion == 'success' &&
        (
          github.event.workflow_run.event == 'push' ||
          github.event.workflow_run.event == 'workflow_dispatch'
        ) &&
        github.event.workflow_run.head_branch == 'master' &&
        github.event.workflow_run.head_repository.full_name == github.repository
      )
    runs-on: ubuntu-24.04
    timeout-minutes: 5
    permissions:
      contents: read
      actions: read
    outputs:
      release_ready: ${{ steps.meta.outputs.release_ready }}
      version: ${{ steps.meta.outputs.version }}
      stable_tag: ${{ steps.meta.outputs.stable_tag }}
      commit_tag: ${{ steps.meta.outputs.commit_tag }}
      sha: ${{ steps.meta.outputs.sha }}
      short_sha: ${{ steps.meta.outputs.short_sha }}
      stable_publishable: ${{ steps.meta.outputs.stable_publishable }}
      workflow_run_id: ${{ steps.meta.outputs.workflow_run_id }}
    env:
      TARGET_SHA: >-
        ${{ github.event_name == 'workflow_run'
            && github.event.workflow_run.head_sha
            || inputs.ref
            || github.sha }}
      TRIGGER_RUN_ID: >-
        ${{ github.event_name == 'workflow_run'
            && github.event.workflow_run.id
            || inputs.ci_run_id
            || '' }}
      GH_TOKEN: ${{ github.token }}
    steps:
      - name: Check out the exact successful commit
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: ${{ env.TARGET_SHA }}
          fetch-depth: 0
          persist-credentials: false

      - name: Validate release metadata and immutable tag policy
        id: meta
        shell: bash
        run: |
          set -Eeuo pipefail

          sha="$(git rev-parse HEAD)"
          [[ "$sha" == "$TARGET_SHA" ]]
          master_sha="$(gh api "repos/${GITHUB_REPOSITORY}/commits/master" --jq .sha)"
          if [[ "$sha" != "$master_sha" ]]; then
            echo "::notice::Skipping ${sha}; master advanced to ${master_sha}"
            {
              echo "release_ready=false"
              echo "sha=${sha}"
              echo "workflow_run_id=${TRIGGER_RUN_ID}"
            } >> "$GITHUB_OUTPUT"
            exit 0
          fi

          plan="$(python3 scripts/auto_version.py --json)"
          action="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["action"])' <<<"$plan")"
          reason="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["reason"])' <<<"$plan")"
          if [[ "$action" != "release" ]]; then
            echo "::notice::No release from ${sha}: ${reason}"
            {
              echo "release_ready=false"
              echo "sha=${sha}"
              echo "workflow_run_id=${TRIGGER_RUN_ID}"
            } >> "$GITHUB_OUTPUT"
            exit 0
          fi

          mapfile -t versions < <(
            sed -n \
              "s/^default_version = '\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)'$/\1/p" \
              pg_local_cache.control
          )
          [[ "${#versions[@]}" == 1 ]] || {
            echo "::error::control file needs one strict X.Y.Z default_version"
            exit 1
          }

          version="${versions[0]}"
          stable_tag="v${version}"
          short_sha="${sha:0:12}"
          commit_tag="master-${short_sha}"
          test -f "sql/pg_local_cache--${version}.sql"
          grep -Fq 'pg_local_cache--' Dockerfile
          grep -Fqx "#define PGLC_VERSION \"${version}\"" src/pg_local_cache.h
          python3 scripts/validate_pgxn_meta.py

          if stable_sha="$(
            gh api "repos/${GITHUB_REPOSITORY}/commits/${stable_tag}" \
              --jq .sha 2>/dev/null
          )"; then
            echo "::error::${stable_tag} already points to ${stable_sha}; refusing version reuse"
            exit 1
          fi

          workflow_run_id="$TRIGGER_RUN_ID"
          if [[ -z "$workflow_run_id" ]]; then
            workflow_run_id="$(
              gh api \
                "repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/runs?branch=master&head_sha=${sha}&status=success&per_page=100" \
                --jq '[
                  .workflow_runs[]
                  | select(
                      .event == "push" or
                      .event == "workflow_dispatch"
                    )
                ][0].id // empty'
            )"
          fi

          [[ -n "$workflow_run_id" ]] || {
            echo "::error::No successful master CI run exists for ${sha}"
            exit 1
          }

          {
            echo "release_ready=true"
            echo "version=${version}"
            echo "stable_tag=${stable_tag}"
            echo "commit_tag=${commit_tag}"
            echo "sha=${sha}"
            echo "short_sha=${short_sha}"
            echo "stable_publishable=true"
            echo "workflow_run_id=${workflow_run_id}"
          } >> "$GITHUB_OUTPUT"

  binary:
    needs: metadata
    if: needs.metadata.outputs.release_ready == 'true'
    runs-on: ubuntu-24.04
    timeout-minutes: 25
    strategy:
      fail-fast: false
      matrix:
        postgres_major: [14, 15, 16, 17, 18]
        target:
          - variant: bookworm
            libc: glibc
          - variant: alpine3.23
            libc: musl
    env:
      VERSION: ${{ needs.metadata.outputs.version }}
      RELEASE_SHA: ${{ needs.metadata.outputs.sha }}
      POSTGRES_MAJOR: ${{ matrix.postgres_major }}
      POSTGRES_VARIANT: ${{ matrix.target.variant }}
      LIBC: ${{ matrix.target.libc }}
    steps:
      - name: Check out exact source
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: ${{ env.RELEASE_SHA }}
          fetch-depth: 0
          persist-credentials: false

      - name: Build and verify binary
        shell: bash
        run: |
          set -Eeuo pipefail
          image="pg_local_cache-release:pg${POSTGRES_MAJOR}-${LIBC}-${RELEASE_SHA}"
          docker build \
            --platform linux/amd64 \
            --target extension \
            --build-arg "POSTGRES_MAJOR=${POSTGRES_MAJOR}" \
            --build-arg "POSTGRES_VARIANT=${POSTGRES_VARIANT}" \
            --build-arg "PGLC_BUILD_ID=${RELEASE_SHA}" \
            --tag "$image" \
            .

          pkglibdir="$(
            docker run --rm \
              --entrypoint /bin/sh \
              "$image" \
              -ec 'pg_config --pkglibdir'
          )"

          reported="$(
            docker run --rm \
              --env LD_BIND_NOW=1 \
              --env "LD_PRELOAD=${pkglibdir}/pg_local_cache.so" \
              --entrypoint postgres \
              "$image" \
              --version
          )"
          [[ "$reported" == *" ${POSTGRES_MAJOR}."* ]]

      - name: Assemble deterministic binary archive
        shell: bash
        run: |
          set -Eeuo pipefail
          image="pg_local_cache-release:pg${POSTGRES_MAJOR}-${LIBC}-${RELEASE_SHA}"
          asset="pg_local_cache-pg${POSTGRES_MAJOR}-linux-${LIBC}-amd64"
          root="release-stage/pg_local_cache-${VERSION}-pg${POSTGRES_MAJOR}-linux-${LIBC}-amd64"
          install -d "$root/lib" "$root/share/extension" "$root/docs" extracted

          pkglibdir="$(docker run --rm --entrypoint /bin/sh "$image" -ec 'pg_config --pkglibdir')"
          sharedir="$(docker run --rm --entrypoint /bin/sh "$image" -ec 'pg_config --sharedir')"
          artifact_container="$(docker create "$image")"
          trap 'docker rm -f "$artifact_container" >/dev/null 2>&1 || true' EXIT
          docker cp "${artifact_container}:${pkglibdir}/pg_local_cache.so" "$root/lib/"
          docker cp "${artifact_container}:${sharedir}/extension/." extracted/
          docker rm -f "$artifact_container" >/dev/null
          trap - EXIT
          install -m 0644 extracted/pg_local_cache.control "$root/share/extension/"
          find extracted -maxdepth 1 -type f -name 'pg_local_cache--*.sql' \
            -exec install -m 0644 {} "$root/share/extension/" \;

          install -m 0755 scripts/install-existing.sh "$root/install.sh"
          install -m 0644 README.md LICENSE "$root/"
          printf '%s\n' "$RELEASE_SHA" > "$root/BUILD-ID"
          install -m 0644 \
            docs/INSTALL_EXISTING.md docs/TECHNICAL.md "$root/docs/"
          {
            printf 'format=1\n'
            printf 'version=%s\n' "$VERSION"
            printf 'postgres_major=%s\n' "$POSTGRES_MAJOR"
            printf 'os=linux\n'
            printf 'libc=%s\n' "$LIBC"
            printf 'architecture=amd64\n'
            printf 'commit=%s\n' "$RELEASE_SHA"
            printf 'build_image=postgres:%s-%s\n' "$POSTGRES_MAJOR" "$POSTGRES_VARIANT"
          } > "$root/RELEASE-METADATA"

          epoch="$(git show -s --format=%ct "$RELEASE_SHA")"
          archive="$PWD/dist/${asset}.tar.gz"
          identity="$RUNNER_TEMP/${asset}.identity.json"
          python3 scripts/release_archive.py build \
            --stage release-stage --root "$(basename "$root")" \
            --output "$archive" --epoch "$epoch"
          python3 scripts/release_archive.py inspect \
            --archive "$archive" --root "$(basename "$root")" \
            --extract-dir "$RUNNER_TEMP/${asset}-extracted" \
            --identity-out "$identity" | tee "$RUNNER_TEMP/${asset}.members.txt"

          extracted_root="$RUNNER_TEMP/${asset}-extracted/$(basename "$root")"
          test -x "$extracted_root/install.sh"
          test -r "$extracted_root/README.md"
          test -r "$extracted_root/LICENSE"
          test -r "$extracted_root/BUILD-ID"
          test -r "$extracted_root/RELEASE-METADATA"
          test "$(cat "$extracted_root/BUILD-ID")" = "$RELEASE_SHA"
          bash "$extracted_root/install.sh" --help >/dev/null

          container="$(docker run --detach --network none \
            --env POSTGRES_HOST_AUTH_METHOD=trust "$image")"
          trap 'docker rm -f "$container" >/dev/null 2>&1 || true' EXIT
          for attempt in {1..60}; do
            docker exec "$container" pg_isready --username postgres \
              --dbname postgres >/dev/null 2>&1 && break
            sleep 0.5
          done
          docker exec "$container" pg_isready --username postgres \
            --dbname postgres >/dev/null
          docker cp "$extracted_root/." "$container:/artifact/"
          docker exec --user root "$container" chown -R 0:0 /artifact
          before="$(docker exec "$container" sh -c \
            'sha256sum "$PGDATA/postgresql.auto.conf"')"
          docker exec "$container" /artifact/install.sh preflight \
            --database postgres --postgres-os-user postgres \
            --pg-config pg_config --psql psql --mode sql-only
          docker exec "$container" /artifact/install.sh install --dry-run \
            --database postgres --postgres-os-user postgres \
            --pg-config pg_config --psql psql --mode sql-only
          after="$(docker exec "$container" sh -c \
            'sha256sum "$PGDATA/postgresql.auto.conf"')"
          test "$before" = "$after"
          python3 scripts/release_archive.py verify-identity \
            --archive "$archive" --identity "$identity"
          (cd dist && sha256sum "${asset}.tar.gz" > "${asset}.tar.gz.sha256")

      - name: Upload binary lane
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: binary-pg${{ matrix.postgres_major }}-${{ matrix.target.libc }}
          path: |
            dist/*.tar.gz
            dist/*.tar.gz.sha256
          if-no-files-found: error
          retention-days: 1

  package:
    needs: [metadata, binary]
    runs-on: ubuntu-24.04
    timeout-minutes: 20
    permissions:
      contents: read
      actions: read
    env:
      VERSION: ${{ needs.metadata.outputs.version }}
      RELEASE_SHA: ${{ needs.metadata.outputs.sha }}
      SHORT_SHA: ${{ needs.metadata.outputs.short_sha }}
      TRIGGER_RUN_ID: ${{ needs.metadata.outputs.workflow_run_id }}
      GH_TOKEN: ${{ github.token }}
    steps:
      - name: Check out exact source
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: ${{ env.RELEASE_SHA }}
          fetch-depth: 0
          persist-credentials: false

      - name: Download all binary lanes
        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
        with:
          pattern: binary-*
          path: dist
          merge-multiple: true

      - name: Validate source and create source archive
        shell: bash
        run: |
          set -Eeuo pipefail
          test "$(find dist -maxdepth 1 -name 'pg_local_cache-pg*-linux-*-amd64.tar.gz' | wc -l)" -eq 10
          test "$(find dist -maxdepth 1 -name 'pg_local_cache-pg*-linux-*-amd64.tar.gz.sha256' | wc -l)" -eq 10
          (
            cd dist
            for manifest in pg_local_cache-pg*-linux-*-amd64.tar.gz.sha256; do
              sha256sum --check "$manifest"
            done
          )
          rm dist/pg_local_cache-pg*-linux-*-amd64.tar.gz.sha256
          test -x scripts/install-existing.sh
          bash -n scripts/install-existing.sh
          make source-sanitize

          source_root="pg_local_cache-${VERSION}-source"
          source_stage="$RUNNER_TEMP/pg_local_cache-source"
          install -d "$source_stage/$source_root"
          git archive --format=tar "$RELEASE_SHA" |
            tar -xf - -C "$source_stage/$source_root"
          printf '%s\n' "$RELEASE_SHA" > "$source_stage/$source_root/BUILD-ID"
          epoch="$(git show -s --format=%ct "$RELEASE_SHA")"
          source_archive="$PWD/dist/pg_local_cache-source.tar.gz"
          source_identity="$RUNNER_TEMP/source.identity.json"
          python3 scripts/release_archive.py build \
            --stage "$source_stage" --root "$source_root" \
            --output "$source_archive" --epoch "$epoch"
          python3 scripts/release_archive.py inspect \
            --archive "$source_archive" --root "$source_root" \
            --extract-dir "$RUNNER_TEMP/source-extracted" \
            --identity-out "$source_identity"
          extracted_root="$RUNNER_TEMP/source-extracted/$source_root"
          (
            cd "$extracted_root"
            bash -n scripts/install-existing.sh
            make verify-static source-test pgxn-check
          )
          python3 scripts/release_archive.py verify-identity \
            --archive "$source_archive" --identity "$source_identity"
          install -m 0755 scripts/fetch-release.sh dist/fetch-release.sh
          install -m 0755 scripts/install-latest.sh dist/install-latest.sh
          bash -n dist/fetch-release.sh
          bash -n dist/install-latest.sh
          cmp --silent scripts/fetch-release.sh dist/fetch-release.sh
          cmp --silent scripts/install-latest.sh dist/install-latest.sh

      - name: Create and verify checksums
        shell: bash
        run: |
          set -Eeuo pipefail
          (
            cd dist
            find . -maxdepth 1 -type f ! -name SHA256SUMS -printf '%f\0' |
              LC_ALL=C sort -z | xargs -0 sha256sum > SHA256SUMS
            sha256sum --check SHA256SUMS
          )

      - name: Upload per-commit downloadable artifact
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: pg_local_cache-${{ needs.metadata.outputs.version }}-${{ needs.metadata.outputs.short_sha }}
          path: dist/
          if-no-files-found: error
          retention-days: 90

  publish:
    needs: [metadata, package]
    runs-on: ubuntu-24.04
    timeout-minutes: 15
    permissions:
      contents: write
      actions: write
    env:
      GH_TOKEN: ${{ github.token }}
      GH_REPO: ${{ github.repository }}
      VERSION: ${{ needs.metadata.outputs.version }}
      STABLE_TAG: ${{ needs.metadata.outputs.stable_tag }}
      COMMIT_TAG: ${{ needs.metadata.outputs.commit_tag }}
      RELEASE_SHA: ${{ needs.metadata.outputs.sha }}
      STABLE_PUBLISHABLE: ${{ needs.metadata.outputs.stable_publishable }}
    steps:
      - name: Download verified packages
        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
        with:
          name: pg_local_cache-${{ needs.metadata.outputs.version }}-${{ needs.metadata.outputs.short_sha }}
          path: dist

      - name: Verify checksums
        working-directory: dist
        run: sha256sum --check SHA256SUMS

      - name: Publish immutable commit prerelease and optional stable release
        shell: bash
        run: |
          set -Eeuo pipefail

          create_ref() {
            local tag="$1" existing
            existing=""
            if existing="$(gh api "repos/${GH_REPO}/commits/${tag}" --jq .sha 2>/dev/null)"; then
              [[ "$existing" == "$RELEASE_SHA" ]] || {
                echo "::error::Refusing to move immutable tag ${tag}"
                return 1
              }
              return
            fi
            gh api --method POST "repos/${GH_REPO}/git/refs" \
              -f "ref=refs/tags/${tag}" -f "sha=${RELEASE_SHA}" >/dev/null
          }

          upload_missing_assets() {
            local tag="$1" existing_assets temporary
            existing_assets="$(gh release view "$tag" --json assets --jq '.assets[].name')"
            temporary="$(mktemp -d)"
            for file in dist/*; do
              local name
              name="$(basename "$file")"
              if grep -Fxq "$name" <<<"$existing_assets"; then
                mkdir -p "$temporary/$name"
                gh release download "$tag" --pattern "$name" --dir "$temporary/$name"
                cmp --silent "$file" "$temporary/$name/$name" || {
                  echo "::error::Existing ${tag}/${name} differs; refusing overwrite"
                  return 1
                }
              else
                gh release upload "$tag" "$file"
              fi
            done
          }

          create_ref "$COMMIT_TAG"
          if ! gh release view "$COMMIT_TAG" >/dev/null 2>&1; then
            gh release create "$COMMIT_TAG" \
              --target "$RELEASE_SHA" --draft --prerelease \
              --title "pg_local_cache ${VERSION} — master ${RELEASE_SHA:0:12}" \
              --notes "Successful master build ${RELEASE_SHA}. Includes PostgreSQL 14–18 Linux glibc/musl binaries, checksums and installer."
          fi
          upload_missing_assets "$COMMIT_TAG"
          gh release edit "$COMMIT_TAG" --draft=false --prerelease

          if [[ "$STABLE_PUBLISHABLE" == "true" ]]; then
            create_ref "$STABLE_TAG"
            if ! gh release view "$STABLE_TAG" >/dev/null 2>&1; then
              gh release create "$STABLE_TAG" \
                --target "$RELEASE_SHA" --draft \
                --title "pg_local_cache ${VERSION} — PostgreSQL 14–18" \
                --notes "Immutable pg_local_cache ${VERSION} release for PostgreSQL 14–18 on Linux amd64 (glibc and musl). Read docs/INSTALL_EXISTING.md before installing on an existing cluster."
            fi
            upload_missing_assets "$STABLE_TAG"
            gh release edit "$STABLE_TAG" --draft=false --prerelease=false
          fi

      - name: Dispatch PGXN publication
        if: inputs.publish_pgxn
        shell: bash
        run: |
          gh workflow run pgxn.yml \
            --ref master \
            --raw-field "ref=${RELEASE_SHA}" \
            --field publish=true
