# Manual publication workflow (workflow_dispatch) — the outward, irreversible
# release steps a human triggers AFTER the automated `release.yaml` build has
# produced downloadable assets (and they've been eyeballed as good).
#
# Why manual + separate from release.yaml:
#   * Publishing to a public registry is irreversible (crates.io versions can
#     only be *yanked*, never deleted) — a human pulls this lever, and the
#     registry token never sits on every tag push.
#   * Our release is split (Linux/Windows on the tag, macOS dispatched), so a
#     human is the natural "all assets are up — go" gate. crates.io publish
#     reads SOURCE so it doesn't strictly need the release, but binstall's
#     metadata points at the release assets — hence run this once builds exist.
#
# Structure: each registry is its OWN job with NO inter-job `needs`, so jobs run
# independently and one failing (or a newly-added one) never breaks another.
# Every job is IDEMPOTENT — re-dispatching when a target is already published is
# a clean no-op. Add Scoop / Homebrew / winget as sibling jobs here later.
name: publish
on:
  workflow_dispatch:
    inputs:
      tag:
        description: 'Release tag to publish (e.g. v0.2.0)'
        required: true

jobs:
  crates-io:
    runs-on: ci-public
    container:
      image: git.lazyeval.net/oli/rdbms-playground-ci:latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.tag }}

      - name: publish to crates.io (idempotent)
        shell: bash
        env:
          TAG: ${{ inputs.tag }}
          # A crate-scoped, publish-update crates.io token, stored as a Gitea
          # Actions secret. `cargo publish` reads CARGO_REGISTRY_TOKEN from env.
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          set -euo pipefail

          # Source of truth = the [package] version at the checked-out tag
          # (toolchain-free read; same approach as release.yaml's guard, which
          # avoids the flake devShell's stdout banner corrupting a parse).
          VER=$(grep -m1 '^version = ' Cargo.toml | sed -E 's/^version = "(.*)"/\1/')
          [ -n "$VER" ] || { echo "ERROR: could not read version from Cargo.toml" >&2; exit 1; }
          if [ "$TAG" != "v$VER" ]; then
            echo "ERROR: dispatch tag '$TAG' != 'v$VER' (Cargo.toml at that tag)" >&2
            exit 1
          fi

          # Idempotency: if this version is already on crates.io, no-op.
          # (crates.io requires a descriptive User-Agent per its data policy;
          # without one the API returns 403.) Only an explicit 200 means
          # "already there" — anything else proceeds, and `cargo publish` is the
          # final backstop (it refuses to overwrite an existing version).
          UA="rdbms-playground-release-ci (oliver@sturmnet.org)"
          code=$(curl -sS -o /dev/null -w '%{http_code}' -A "$UA" \
            "https://crates.io/api/v1/crates/rdbms-playground/$VER" || echo 000)
          if [ "$code" = "200" ]; then
            echo "rdbms-playground $VER is already on crates.io — nothing to do."
            exit 0
          fi
          echo "crates.io returned HTTP $code for $VER (not 200) — proceeding to publish."

          echo "publishing rdbms-playground $VER to crates.io ..."
          nix develop -c cargo publish --locked
          echo "published rdbms-playground $VER to crates.io."

  # Update the lazyeval Scoop bucket (Windows). Renders the manifest from the
  # release's .sha256 sidecars and commits it to lazyeval/scoop-bucket. Pushes
  # with the lazyeval-ci bot token (LAZYEVAL_PKG_TOKEN), which is scoped — via
  # the bot's org-team membership — to the lazyeval package repos only, so a
  # leak cannot touch oli/rdbms-playground.
  scoop-bucket:
    runs-on: ci-public
    container:
      image: git.lazyeval.net/oli/rdbms-playground-ci:latest
    steps:
      - uses: actions/checkout@v4   # default ref (main) — current render script

      - name: update the lazyeval Scoop bucket (idempotent)
        shell: bash
        env:
          TAG: ${{ inputs.tag }}
          # Passed via env, never inlined into the script, so the value stays
          # masked in logs; it only materialises in the clone URL at runtime.
          PKG_TOKEN: ${{ secrets.LAZYEVAL_PKG_TOKEN }}
        run: |
          set -euo pipefail
          VER="${TAG#v}"
          echo "scoop: targeting rdbms-playground $VER ($TAG)"

          base="https://git.lazyeval.net/oli/rdbms-playground/releases/download/$TAG"
          fetch_hash() {
            local asset="$1" line
            echo "scoop: fetching $asset.sha256" >&2
            line=$(curl -fsSL "$base/$asset.sha256") \
              || { echo "ERROR: cannot fetch $asset.sha256 — is $TAG released with assets?" >&2; exit 1; }
            # First whitespace-delimited field is the hash. `read` is a bash
            # builtin (no awk, which the slim CI image may lack).
            local hash _
            read -r hash _ <<<"$line"
            printf '%s' "$hash"
          }
          h_x64=$(fetch_hash "rdbms-playground-$TAG-x86_64-pc-windows-gnu.exe")
          h_arm=$(fetch_hash "rdbms-playground-$TAG-aarch64-pc-windows-gnullvm.exe")

          echo "scoop: rendering manifest"
          bash scripts/render-scoop-manifest.sh "$VER" "$h_x64" "$h_arm" > /tmp/rdbms-playground.json
          node -e 'JSON.parse(require("fs").readFileSync("/tmp/rdbms-playground.json","utf8"))' \
            || { echo "ERROR: rendered Scoop manifest is not valid JSON" >&2; exit 1; }

          work=$(mktemp -d)
          echo "scoop: cloning lazyeval/scoop-bucket"
          git clone --depth 1 "https://lazyeval-ci:${PKG_TOKEN}@git.lazyeval.net/lazyeval/scoop-bucket.git" "$work"
          cp /tmp/rdbms-playground.json "$work/rdbms-playground.json"

          cd "$work"
          git config user.name  "lazyeval-ci"
          git config user.email "ci@lazyeval.net"
          git add rdbms-playground.json
          if git diff --cached --quiet; then
            echo "scoop: manifest already at $VER — nothing to commit."
            exit 0
          fi
          git commit -m "rdbms-playground $VER"
          # Push to main explicitly: a freshly-created (empty) repo clone may put
          # the first commit on a differently-named local branch. Assumes the
          # bucket/tap default branch is `main` (Gitea's default for new repos).
          git push origin HEAD:main
          echo "scoop: bucket updated to rdbms-playground $VER."

  # Update the lazyeval Homebrew tap (macOS + Linux). Same shape as scoop-bucket;
  # writes Formula/rdbms-playground.rb into lazyeval/homebrew-tap.
  homebrew-tap:
    runs-on: ci-public
    container:
      image: git.lazyeval.net/oli/rdbms-playground-ci:latest
    steps:
      - uses: actions/checkout@v4

      - name: update the lazyeval Homebrew tap (idempotent)
        shell: bash
        env:
          TAG: ${{ inputs.tag }}
          PKG_TOKEN: ${{ secrets.LAZYEVAL_PKG_TOKEN }}
        run: |
          set -euo pipefail
          VER="${TAG#v}"
          echo "homebrew: targeting rdbms-playground $VER ($TAG)"

          base="https://git.lazyeval.net/oli/rdbms-playground/releases/download/$TAG"
          fetch_hash() {
            local asset="$1" line
            echo "homebrew: fetching $asset.sha256" >&2
            line=$(curl -fsSL "$base/$asset.sha256") \
              || { echo "ERROR: cannot fetch $asset.sha256 — is $TAG released with assets?" >&2; exit 1; }
            # First whitespace-delimited field is the hash. `read` is a bash
            # builtin (no awk, which the slim CI image may lack).
            local hash _
            read -r hash _ <<<"$line"
            printf '%s' "$hash"
          }
          mac_arm=$(fetch_hash "rdbms-playground-$TAG-aarch64-apple-darwin")
          mac_x64=$(fetch_hash "rdbms-playground-$TAG-x86_64-apple-darwin")
          lin_arm=$(fetch_hash "rdbms-playground-$TAG-aarch64-unknown-linux-musl")
          lin_x64=$(fetch_hash "rdbms-playground-$TAG-x86_64-unknown-linux-musl")

          echo "homebrew: rendering formula"
          bash scripts/render-homebrew-formula.sh "$VER" "$mac_arm" "$mac_x64" "$lin_arm" "$lin_x64" \
            > /tmp/rdbms-playground.rb
          grep -q '^class RdbmsPlayground < Formula$' /tmp/rdbms-playground.rb \
            || { echo "ERROR: rendered formula looks malformed" >&2; exit 1; }

          work=$(mktemp -d)
          echo "homebrew: cloning lazyeval/homebrew-tap"
          git clone --depth 1 "https://lazyeval-ci:${PKG_TOKEN}@git.lazyeval.net/lazyeval/homebrew-tap.git" "$work"
          mkdir -p "$work/Formula"
          cp /tmp/rdbms-playground.rb "$work/Formula/rdbms-playground.rb"

          cd "$work"
          git config user.name  "lazyeval-ci"
          git config user.email "ci@lazyeval.net"
          git add Formula/rdbms-playground.rb
          if git diff --cached --quiet; then
            echo "homebrew: formula already at $VER — nothing to commit."
            exit 0
          fi
          git commit -m "rdbms-playground $VER"
          # Push to main explicitly: a freshly-created (empty) repo clone may put
          # the first commit on a differently-named local branch. Assumes the
          # bucket/tap default branch is `main` (Gitea's default for new repos).
          git push origin HEAD:main
          echo "homebrew: tap updated to rdbms-playground $VER."

  # Update the winget package (Windows) by opening a PR to microsoft/winget-pkgs
  # with komac. Unlike scoop-bucket/homebrew-tap (which push to OUR repos and are
  # live at once), winget is a PR into Microsoft's central, human-gated catalog —
  # asynchronous, and re-submitting the same version would open a DUPLICATE PR. So
  # this job guards on both already-merged versions AND already-open PRs before
  # submitting, which keeps a repeated `publish` dispatch safe.
  #
  # Auth: komac needs a CLASSIC GitHub PAT with `public_repo` (fine-grained tokens
  # cannot open the cross-fork PR — komac #310). It is held on a dedicated GitHub
  # bot account (a leak can't reach other repos — same reasoning as lazyeval-ci)
  # and referenced ONLY in this job's env (job-level secret scoping keeps it out
  # of the other publish jobs).
  #
  # PREREQUISITE: the package `LazyEvaluation.RdbmsPlayground` must already exist
  # in winget-pkgs via a one-time `komac new` (interactive — run manually once;
  # see ADR-0056 Amendment 4). This job only does the per-release `komac update`.
  winget:
    runs-on: ci-public
    container:
      image: git.lazyeval.net/oli/rdbms-playground-ci:latest
    steps:
      - name: submit the winget update PR (idempotent)
        shell: bash
        env:
          TAG: ${{ inputs.tag }}
          # Classic public_repo PAT for the winget bot account. komac reads it
          # from GITHUB_TOKEN; the API guards below read it via a curl config file
          # so the token never appears in a command line / process list.
          GITHUB_TOKEN: ${{ secrets.WINGET_GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          VER="${TAG#v}"
          PKG="LazyEvaluation.RdbmsPlayground"
          echo "winget: targeting $PKG $VER ($TAG)"

          # Auth header in a 0600 curl config (keeps the token out of argv/logs).
          umask 077
          printf 'header = "Authorization: Bearer %s"\nheader = "Accept: application/vnd.github+json"\n' \
            "$GITHUB_TOKEN" > /tmp/gh-curlrc
          api="https://api.github.com"

          # Guard 1 — already merged into winget-pkgs?
          # (manifests/<first-letter>/<Publisher>/<Package>/<version>)
          merged=$(curl -sS -o /dev/null -w '%{http_code}' --config /tmp/gh-curlrc \
            "$api/repos/microsoft/winget-pkgs/contents/manifests/l/LazyEvaluation/RdbmsPlayground/$VER" || echo 000)
          if [ "$merged" = "200" ]; then
            echo "winget: $PKG $VER already in winget-pkgs — nothing to do."
            exit 0
          fi

          # Guard 2 — an open PR for this exact id+version already? (avoid a dup)
          # curl -G --data-urlencode does the URL-encoding (no jq in the image).
          curl -sS -G --config /tmp/gh-curlrc \
            --data-urlencode "q=repo:microsoft/winget-pkgs type:pr state:open in:title \"$PKG\" \"$VER\"" \
            "$api/search/issues" -o /tmp/winget-search.json
          open=$(node -e 'process.stdout.write(String(JSON.parse(require("fs").readFileSync("/tmp/winget-search.json","utf8")).total_count||0))')
          if [ "$open" != "0" ]; then
            echo "winget: an open PR for $PKG $VER already exists — skipping."
            exit 0
          fi

          # Install pinned komac (prebuilt glibc binary; the CI image has no
          # cargo/komac). Pinned for reproducibility — bump deliberately.
          KOMAC_VER=2.16.0
          curl -fsSL -o /tmp/komac.tgz \
            "https://github.com/russellbanks/Komac/releases/download/v$KOMAC_VER/komac-$KOMAC_VER-x86_64-unknown-linux-gnu.tar.gz"
          tar -xzf /tmp/komac.tgz -C /tmp
          komac_bin=$(find /tmp -maxdepth 2 -type f -name komac | head -1)
          [ -n "$komac_bin" ] || { echo "ERROR: komac binary not found after extract" >&2; exit 1; }

          base="https://git.lazyeval.net/oli/rdbms-playground/releases/download/$TAG"
          echo "winget: submitting update PR via komac $KOMAC_VER"
          # NB: confirm flags against `komac update --help` on first run — komac
          # evolves; --version/--urls/--submit are the stable core. komac infers
          # architecture + the `portable` installer type from the binaries.
          "$komac_bin" update "$PKG" \
            --version "$VER" \
            --urls "$base/rdbms-playground-$TAG-x86_64-pc-windows-gnu.exe" \
                   "$base/rdbms-playground-$TAG-aarch64-pc-windows-gnullvm.exe" \
            --submit
          echo "winget: update PR submitted for $PKG $VER (Microsoft review is async)."

  # No `needs:` between jobs — each is independent and idempotent, so one failing
  # or being added never breaks another.
