name: Release
on:
  push:
    tags: ['v*']

concurrency:
  group: release-${{ github.ref }}
  cancel-in-progress: true

env:
  CARGO_TERM_COLOR: always

jobs:
  validate:
    name: Validate tag matches versions
    runs-on: ubuntu-latest
    timeout-minutes: 5
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: Compare tag, Cargo.toml, snapcraft.yaml versions
        # Fails fast if the pushed tag (v0.1.10) disagrees with either
        # Cargo.toml's `version = "..."` or snapcraft.yaml's `version: '...'`.
        # Extraction logic lifted verbatim from ci.yml:version-sync.
        run: |
          TAG_VER="${GITHUB_REF#refs/tags/v}"
          CARGO_VER=$(awk -F'"' '/^version[[:space:]]*=[[:space:]]*"/{print $2; exit}' Cargo.toml)
          SNAP_VER=$(sed -n "s/^version:[[:space:]]*['\"]\\?\\([0-9]\\+\\.[0-9]\\+\\.[0-9]\\+\\)['\"]\\?.*/\\1/p" snap/snapcraft.yaml)
          echo "tag=$TAG_VER cargo=$CARGO_VER snap=$SNAP_VER"
          if [ "$TAG_VER" != "$CARGO_VER" ] || [ "$CARGO_VER" != "$SNAP_VER" ]; then
            echo "::error::Version mismatch: tag=v$TAG_VER cargo=$CARGO_VER snap=$SNAP_VER"
            exit 1
          fi

  build:
    name: Build (${{ matrix.asset_name }})
    runs-on: ${{ matrix.os }}
    needs: validate
    timeout-minutes: 30
    permissions:
      contents: read
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            asset_name: linux-x86_64
          - os: macos-14
            target: aarch64-apple-darwin
            asset_name: macos-arm64
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            asset_name: windows-x86_64
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Install system dependencies (Linux only)
        if: runner.os == 'Linux'
        run: |
          sudo apt-get update
          sudo apt-get install -y clang pkg-config libxcb1-dev libxcb-render0-dev \
            libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev \
            libgtk-3-dev libfontconfig1-dev libdbus-1-dev

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - uses: Swatinem/rust-cache@v2
        with:
          key: ${{ matrix.target }}

      - name: Remove local-only MCP dependency
        # Anchored to start-of-line so commented `# mcp = ...` / `# egui-mcp-bridge = ...`
        # lines are untouched (regression caught in v0.1.9 publish-crates failure).
        shell: python
        run: |
          import pathlib, re
          p = pathlib.Path("Cargo.toml")
          t = p.read_text()
          t = re.sub(r'(?m)^mcp\s*=\s*\["dep:egui-mcp-bridge"\]', 'mcp = []', t)
          t = re.sub(r'(?m)^egui-mcp-bridge\s*=\s*.*\n', '', t)
          p.write_text(t)

      - name: Install git-cliff
        if: runner.os == 'Linux'
        run: cargo install git-cliff

      - name: Generate changelog
        if: runner.os == 'Linux'
        run: git-cliff --latest --strip header > RELEASE_NOTES.md

      - name: Build release
        run: cargo build --release --target ${{ matrix.target }}

      - name: Package (Linux/macOS)
        if: runner.os != 'Windows'
        run: |
          VERSION=${GITHUB_REF#refs/tags/v}
          ASSET="md-viewer-${VERSION}-${{ matrix.asset_name }}.tar.gz"
          tar -czf "$ASSET" -C "target/${{ matrix.target }}/release" md-viewer
          shasum -a 256 "$ASSET" > "$ASSET.sha256"

      - name: Package (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          $VERSION = "${env:GITHUB_REF}" -replace '^refs/tags/v',''
          $ASSET = "md-viewer-$VERSION-${{ matrix.asset_name }}.zip"
          Compress-Archive -Path "target/${{ matrix.target }}/release/md-viewer.exe" -DestinationPath $ASSET
          $Hash = (Get-FileHash -Algorithm SHA256 $ASSET).Hash.ToLower()
          "$Hash  $ASSET" | Out-File -Encoding ascii "$ASSET.sha256"

      - uses: actions/upload-artifact@v4
        with:
          name: release-${{ matrix.asset_name }}
          if-no-files-found: error
          path: |
            md-viewer-*.tar.gz
            md-viewer-*.zip
            md-viewer-*.sha256
            RELEASE_NOTES.md

  publish-crates:
    name: Publish to crates.io
    runs-on: ubuntu-latest
    needs: [validate, build]
    timeout-minutes: 20
    permissions:
      contents: read
    env:
      CARGO_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
    steps:
      # Step-level gating, same pattern as publish-aur. GitHub Actions disallows
      # secrets.* and env.* in job-level `if:` — see LESSONS.md.
      - name: Check crates.io token
        id: check
        run: |
          if [ -z "$CARGO_TOKEN" ]; then
            echo "::notice::CARGO_REGISTRY_TOKEN not set; skipping crates.io publish (see PUBLISHING.md for setup)."
            echo "proceed=false" >> "$GITHUB_OUTPUT"
          else
            echo "proceed=true" >> "$GITHUB_OUTPUT"
          fi

      - uses: actions/checkout@v4
        if: steps.check.outputs.proceed == 'true'

      - name: Install system dependencies
        if: steps.check.outputs.proceed == 'true'
        # md-viewer's `cargo publish` verify step compiles the packaged crate
        # against crates.io deps (no patch), which links eframe/rfd/etc. Same
        # apt set as the build job.
        run: |
          sudo apt-get update
          sudo apt-get install -y clang pkg-config libxcb1-dev libxcb-render0-dev \
            libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev \
            libgtk-3-dev libfontconfig1-dev libdbus-1-dev

      - uses: dtolnay/rust-toolchain@stable
        if: steps.check.outputs.proceed == 'true'

      - uses: Swatinem/rust-cache@v2
        if: steps.check.outputs.proceed == 'true'
        with:
          key: publish-crates

      - name: Remove local-only MCP dependency
        # Same anchored transform as the build job: strips an uncommented
        # `mcp = [...]` / `egui-mcp-bridge = ...` line; leaves commented
        # versions untouched (the working dir stays clean → cargo publish
        # dirty-check passes).
        if: steps.check.outputs.proceed == 'true'
        shell: python
        run: |
          import pathlib, re
          p = pathlib.Path("Cargo.toml")
          t = p.read_text()
          t = re.sub(r'(?m)^mcp\s*=\s*\["dep:egui-mcp-bridge"\]', 'mcp = []', t)
          t = re.sub(r'(?m)^egui-mcp-bridge\s*=\s*.*\n', '', t)
          p.write_text(t)

      - name: Publish fork crates and md-viewer
        if: steps.check.outputs.proceed == 'true'
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: bash scripts/publish-crates.sh

  publish-snap:
    name: Publish to Snap Store
    runs-on: ubuntu-latest
    needs: [validate, build]
    timeout-minutes: 30
    permissions:
      contents: read
    env:
      SNAP_CREDS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
    steps:
      # Step-level gating, same pattern as publish-aur and publish-crates.
      # If SNAPCRAFT_STORE_CREDENTIALS isn't set, skip cleanly with a notice
      # instead of erroring out the whole release.
      - name: Check Snap Store credentials
        id: check
        run: |
          if [ -z "$SNAP_CREDS" ]; then
            echo "::notice::SNAPCRAFT_STORE_CREDENTIALS not set; skipping snap publish (see PUBLISHING.md for setup)."
            echo "proceed=false" >> "$GITHUB_OUTPUT"
          else
            echo "proceed=true" >> "$GITHUB_OUTPUT"
          fi

      - uses: actions/checkout@v4
        if: steps.check.outputs.proceed == 'true'

      - uses: snapcore/action-build@v1
        if: steps.check.outputs.proceed == 'true'
        id: build

      - uses: snapcore/action-publish@v1
        if: steps.check.outputs.proceed == 'true'
        with:
          snap: ${{ steps.build.outputs.snap }}
          release: stable
        env:
          SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}

  publish-aur:
    name: Publish to AUR
    runs-on: ubuntu-latest
    needs: [validate, build]
    timeout-minutes: 10
    permissions:
      contents: read
    env:
      AUR_SSH_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
    steps:
      # GitHub Actions disallows both `secrets.*` and `env.*` in job-level `if:`,
      # so we gate at step level instead. This step always succeeds and sets an
      # output that subsequent steps check, leaving the job green when the secret
      # isn't configured.
      - name: Check AUR secret
        id: check
        run: |
          if [ -z "$AUR_SSH_KEY" ]; then
            echo "::notice::AUR_SSH_PRIVATE_KEY not set; skipping AUR publish (see PUBLISHING.md for setup)."
            echo "proceed=false" >> "$GITHUB_OUTPUT"
          else
            echo "proceed=true" >> "$GITHUB_OUTPUT"
          fi

      - uses: actions/checkout@v4
        if: steps.check.outputs.proceed == 'true'

      - name: Set version
        if: steps.check.outputs.proceed == 'true'
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_ENV"

      - name: Set up SSH for AUR
        if: steps.check.outputs.proceed == 'true'
        run: |
          mkdir -p ~/.ssh
          printf '%s\n' "$AUR_SSH_KEY" > ~/.ssh/aur
          chmod 600 ~/.ssh/aur
          ssh-keyscan aur.archlinux.org > ~/.ssh/known_hosts 2>/dev/null
          cat > ~/.ssh/config <<'EOF'
          Host aur.archlinux.org
              IdentityFile ~/.ssh/aur
              User aur
              StrictHostKeyChecking yes
          EOF
          chmod 600 ~/.ssh/config

      - name: Clone AUR repo and update PKGBUILD
        if: steps.check.outputs.proceed == 'true'
        run: |
          git clone ssh://aur@aur.archlinux.org/md-viewer-git.git aur-repo
          cp aur/PKGBUILD aur-repo/PKGBUILD
          # Bump the literal pkgver line so the file reflects the latest tag.
          # (Build-time pkgver() still overrides at install, but this keeps the file readable.)
          sed -i "s/^pkgver=.*/pkgver=${VERSION}/" aur-repo/PKGBUILD

      - name: Regenerate .SRCINFO
        if: steps.check.outputs.proceed == 'true'
        run: |
          # makepkg refuses to run as root; spin up an Arch container with an unprivileged user.
          docker run --rm -v "$PWD/aur-repo:/pkg" -w /pkg archlinux/archlinux:base-devel bash -c '
            useradd -m builder &&
            chown -R builder /pkg &&
            sudo -u builder makepkg --printsrcinfo
          ' > aur-repo/.SRCINFO

      - name: Restore aur-repo ownership to runner
        if: steps.check.outputs.proceed == 'true'
        # The container's chown -R builder propagated through the bind mount, so
        # the host runner can no longer write inside aur-repo (e.g. .git/config).
        # Reclaim ownership before doing git config / commit / push.
        run: sudo chown -R "$(id -u):$(id -g)" aur-repo

      - name: Commit and push to AUR
        if: steps.check.outputs.proceed == 'true'
        working-directory: aur-repo
        run: |
          git config user.name "md-viewer CI"
          git config user.email "ci@noreply.aydiler"
          git add PKGBUILD .SRCINFO
          if git diff --cached --quiet; then
            echo "No changes to push to AUR."
            exit 0
          fi
          git commit -m "Update to ${VERSION}"
          git push

  publish-aur-bin:
    name: Publish to AUR (-bin)
    runs-on: ubuntu-latest
    needs: [validate, build]
    timeout-minutes: 10
    permissions:
      contents: read
    env:
      AUR_SSH_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
    steps:
      # Same step-level gating as publish-aur (see LESSONS.md → "GitHub Actions
      # blocks `secrets.*` AND `env.*` in job-level `if:`").
      - name: Check AUR secret
        id: check
        run: |
          if [ -z "$AUR_SSH_KEY" ]; then
            echo "::notice::AUR_SSH_PRIVATE_KEY not set; skipping AUR -bin publish (see PUBLISHING.md for setup)."
            echo "proceed=false" >> "$GITHUB_OUTPUT"
          else
            echo "proceed=true" >> "$GITHUB_OUTPUT"
          fi

      - uses: actions/checkout@v4
        if: steps.check.outputs.proceed == 'true'

      - name: Set version
        if: steps.check.outputs.proceed == 'true'
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_ENV"

      - name: Set up SSH for AUR
        if: steps.check.outputs.proceed == 'true'
        run: |
          mkdir -p ~/.ssh
          printf '%s\n' "$AUR_SSH_KEY" > ~/.ssh/aur
          chmod 600 ~/.ssh/aur
          ssh-keyscan aur.archlinux.org > ~/.ssh/known_hosts 2>/dev/null
          cat > ~/.ssh/config <<'EOF'
          Host aur.archlinux.org
              IdentityFile ~/.ssh/aur
              User aur
              StrictHostKeyChecking yes
          EOF
          chmod 600 ~/.ssh/config

      # The tarball checksum comes from the build job's artifact rather than
      # the GitHub Release. Both publish-aur-bin and create-release are
      # `needs: [validate, build]` and run in parallel — so this job races
      # ahead of the Release URL existing. The artifact already contains the
      # exact .sha256 file we'd otherwise fetch from the Release page.
      - name: Download linux build artifact (for tarball sha256)
        if: steps.check.outputs.proceed == 'true'
        uses: actions/download-artifact@v4
        with:
          name: release-linux-x86_64
          path: dist/

      - name: Clone AUR repo and update PKGBUILD
        if: steps.check.outputs.proceed == 'true'
        run: |
          # On first run for this package, the repo doesn't exist yet — AUR
          # creates it on first push. `git clone` of a not-yet-existing repo
          # returns "warning: You appear to have cloned an empty repository."
          # which is fine; we initialize it from the staged PKGBUILD.
          git clone ssh://aur@aur.archlinux.org/md-viewer-bin.git aur-repo || true
          if [ ! -d aur-repo ]; then
            mkdir aur-repo
            (cd aur-repo && git init -b master && \
             git remote add origin ssh://aur@aur.archlinux.org/md-viewer-bin.git)
          fi
          cp aur-bin/PKGBUILD aur-repo/PKGBUILD

          # Tarball sha256 comes from the downloaded artifact (see comment above).
          # The three aux files (.desktop, icon, LICENSE) come from raw.github
          # at the tagged commit — those URLs are valid as soon as the tag exists.
          mkdir -p sums
          cp "dist/md-viewer-${VERSION}-linux-x86_64.tar.gz.sha256" sums/tarball.sha256
          curl -fsSL "https://raw.githubusercontent.com/aydiler/md-viewer/v${VERSION}/data/md-viewer.desktop" \
            | sha256sum > sums/desktop.sha256
          curl -fsSL "https://raw.githubusercontent.com/aydiler/md-viewer/v${VERSION}/data/io.github.aydiler.md-viewer.png" \
            | sha256sum > sums/icon.sha256
          curl -fsSL "https://raw.githubusercontent.com/aydiler/md-viewer/v${VERSION}/LICENSE" \
            | sha256sum > sums/license.sha256

          # Rewrite pkgver and the four-element sha256sums=( ... ) array.
          # sed on a multi-line array is fragile, so use Python.
          python3 - <<'PY'
          import re, pathlib
          p = pathlib.Path("aur-repo/PKGBUILD")
          sums = [open(f"sums/{n}.sha256").read().split()[0]
                  for n in ("tarball", "desktop", "icon", "license")]
          import os
          version = os.environ["VERSION"]
          text = p.read_text()
          text = re.sub(r"^pkgver=.*$", f"pkgver={version}", text, count=1, flags=re.M)
          block = "sha256sums=(\n    '" + "'\n    '".join(sums) + "'\n)"
          text = re.sub(r"sha256sums=\([^)]*\)", block, text, count=1, flags=re.S)
          p.write_text(text)
          PY

      - name: Regenerate .SRCINFO
        if: steps.check.outputs.proceed == 'true'
        run: |
          docker run --rm -v "$PWD/aur-repo:/pkg" -w /pkg archlinux/archlinux:base-devel bash -c '
            useradd -m builder &&
            chown -R builder /pkg &&
            sudo -u builder makepkg --printsrcinfo
          ' > aur-repo/.SRCINFO

      - name: Restore aur-repo ownership to runner
        if: steps.check.outputs.proceed == 'true'
        # See LESSONS.md → "Docker bind-mount `chown` breaks host runner ownership".
        run: sudo chown -R "$(id -u):$(id -g)" aur-repo

      - name: Commit and push to AUR
        if: steps.check.outputs.proceed == 'true'
        working-directory: aur-repo
        run: |
          git config user.name "md-viewer CI"
          git config user.email "ci@noreply.aydiler"
          git add PKGBUILD .SRCINFO
          if git diff --cached --quiet; then
            echo "No changes to push to AUR."
            exit 0
          fi
          git commit -m "Update to ${VERSION}"
          # `-u origin HEAD:refs/heads/master` works for both first push
          # (brand-new AUR repo without an upstream) and subsequent pushes.
          git push -u origin HEAD:refs/heads/master

  create-release:
    name: Create GitHub Release
    runs-on: ubuntu-latest
    # Decoupled from publish-snap: the GH release page appears as soon as the
    # build matrix finishes (~12 min sooner). publish-aur-bin already pulls its
    # tarball from download-artifact (not the GH Release URL), so nothing else
    # depends on this ordering.
    needs: [validate, build]
    timeout-minutes: 5
    permissions:
      contents: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          pattern: release-*
          merge-multiple: true

      - uses: softprops/action-gh-release@v2
        with:
          body_path: RELEASE_NOTES.md
          files: |
            md-viewer-*.tar.gz
            md-viewer-*.zip
            md-viewer-*.sha256
