name: Release

on:
  push:
    tags:
      - "v*"

# A deleted-and-re-pushed tag (it has happened — see the checksum note in the
# publish job) must not race two publish runs: `gh release create` fails
# loudly on the second run, but the ghcr :latest push does not, and which run
# wins it would be arbitrary. Queue, never cancel — a half-cancelled release
# is worse than a slow one.
concurrency:
  group: release-${{ github.ref_name }}
  cancel-in-progress: false

jobs:
  # The v1.1.0-alpha.4 release shipped clients still versioned 1.1.0-alpha.3
  # because the client manifests weren't bumped before tagging — deployed
  # clients then never saw the update. Fail fast on that mismatch, before any
  # expensive build starts.
  verify-versions:
    name: Verify client version matches tag
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
      - name: Compare tag with client manifests
        shell: bash
        run: |
          TAG_VERSION="${GITHUB_REF_NAME#v}"
          TAURI_VERSION=$(node -p "require('./Client/tauri-client/src-tauri/tauri.conf.json').version")
          NPM_VERSION=$(node -p "require('./Client/tauri-client/package.json').version")
          CARGO_VERSION=$(sed -n 's/^version = "\(.*\)"$/\1/p' Client/tauri-client/src-tauri/Cargo.toml | head -1)
          fail=0
          for pair in "tauri.conf.json:$TAURI_VERSION" "package.json:$NPM_VERSION" "Cargo.toml:$CARGO_VERSION"; do
            file="${pair%%:*}"; ver="${pair#*:}"
            if [ "$ver" != "$TAG_VERSION" ]; then
              echo "::error::Release tag v$TAG_VERSION does not match client version $ver in $file — bump the client version before tagging."
              fail=1
            fi
          done
          exit $fail

  release-client-windows:
    name: Build Tauri (Windows)
    needs: verify-versions
    runs-on: windows-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
        with:
          node-version: 20
          cache: npm
          cache-dependency-path: Client/tauri-client/package-lock.json

      - name: Install Rust
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable

      - name: Rust cache
        uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
        with:
          workspaces: Client/tauri-client/src-tauri

      - name: Install npm dependencies
        working-directory: Client/tauri-client
        run: npm ci

      - name: Build Tauri app
        working-directory: Client/tauri-client
        env:
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
        run: npm run tauri build

      - name: Stage Windows release assets
        shell: bash
        run: |
          mkdir -p release-staging
          NSIS_DIR="Client/tauri-client/src-tauri/target/release/bundle/nsis"
          INSTALLER=$(find "$NSIS_DIR" -name "*.exe" | head -1)
          cp "$INSTALLER" release-staging/
          NSIS_ZIP=$(find "$NSIS_DIR" -name "*_x64-setup.nsis.zip" ! -name "*.sig" | head -1)
          if [ -n "$NSIS_ZIP" ] && [ -f "$NSIS_ZIP" ]; then cp "$NSIS_ZIP" release-staging/; fi
          NSIS_SIG=$(find "$NSIS_DIR" -name "*_x64-setup.nsis.zip.sig" | head -1)
          if [ -n "$NSIS_SIG" ] && [ -f "$NSIS_SIG" ]; then cp "$NSIS_SIG" release-staging/; fi

      - name: Upload Windows release assets
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: windows-release-assets
          path: release-staging/

  release-client-linux:
    name: Build Tauri (Linux)
    needs: verify-versions
    runs-on: ubuntu-22.04
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
        with:
          node-version: 20
          cache: npm
          cache-dependency-path: Client/tauri-client/package-lock.json

      - name: Install Linux system dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            libwebkit2gtk-4.1-dev \
            libgtk-3-dev \
            libayatana-appindicator3-dev \
            libsecret-1-dev \
            libdbus-1-dev \
            libasound2-dev \
            libssl-dev \
            patchelf \
            librsvg2-dev \
            xdg-utils

      - name: Install Rust
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable

      - name: Rust cache
        uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
        with:
          workspaces: Client/tauri-client/src-tauri

      - name: Install npm dependencies
        working-directory: Client/tauri-client
        run: npm ci

      - name: Build Tauri app (AppImage + deb)
        working-directory: Client/tauri-client
        env:
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
        run: npm run tauri build -- --bundles appimage,deb

      # linuxdeploy bundles the runner's libwayland-* into the AppImage, which
      # breaks Mesa EGL init on newer hosts (white window on Arch/Fedora —
      # EGL_BAD_PARAMETER). Strip them and regenerate the updater artifact +
      # signatures for the patched image.
      - name: Strip host-incompatible libs from AppImage and re-sign
        working-directory: Client/tauri-client
        shell: bash
        env:
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
        run: |
          BUNDLE_DIR="src-tauri/target/release/bundle/appimage"
          APPIMAGE=$(find "$BUNDLE_DIR" -name "*.AppImage" ! -name "*.sig" | head -1)
          bash scripts/strip-appimage-bundled-libs.sh "$APPIMAGE"
          TARBALL="$APPIMAGE.tar.gz"
          rm -f "$TARBALL" "$APPIMAGE.sig" "$TARBALL.sig"
          tar czf "$TARBALL" -C "$(dirname "$APPIMAGE")" "$(basename "$APPIMAGE")"
          # Sign straight from the environment. TAURI_SIGNING_PRIVATE_KEY is
          # the env form of --private-key, so ALSO passing -f/--private-key-path
          # makes the CLI abort: "the argument '--private-key-path' cannot be
          # used with '--private-key'". Keeping the key in the env instead of a
          # temp file also keeps it off the runner's disk.
          npx tauri signer sign -p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" "$APPIMAGE"
          npx tauri signer sign -p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" "$TARBALL"

      - name: Stage Linux release assets
        shell: bash
        run: |
          mkdir -p linux-staging
          BUNDLE_DIR="Client/tauri-client/src-tauri/target/release/bundle"
          # AppImage
          APPIMAGE=$(find "$BUNDLE_DIR/appimage" -name "*.AppImage" ! -name "*.sig" | head -1)
          if [ -n "$APPIMAGE" ] && [ -f "$APPIMAGE" ]; then cp "$APPIMAGE" linux-staging/; fi
          APPIMAGE_SIG=$(find "$BUNDLE_DIR/appimage" -name "*.AppImage.sig" | head -1)
          if [ -n "$APPIMAGE_SIG" ] && [ -f "$APPIMAGE_SIG" ]; then cp "$APPIMAGE_SIG" linux-staging/; fi
          APPIMAGE_TAR=$(find "$BUNDLE_DIR/appimage" -name "*.AppImage.tar.gz" ! -name "*.sig" | head -1)
          if [ -n "$APPIMAGE_TAR" ] && [ -f "$APPIMAGE_TAR" ]; then cp "$APPIMAGE_TAR" linux-staging/; fi
          APPIMAGE_TAR_SIG=$(find "$BUNDLE_DIR/appimage" -name "*.AppImage.tar.gz.sig" | head -1)
          if [ -n "$APPIMAGE_TAR_SIG" ] && [ -f "$APPIMAGE_TAR_SIG" ]; then cp "$APPIMAGE_TAR_SIG" linux-staging/; fi
          # .deb
          DEB=$(find "$BUNDLE_DIR/deb" -name "*.deb" | head -1)
          if [ -n "$DEB" ] && [ -f "$DEB" ]; then cp "$DEB" linux-staging/; fi

      - name: Upload Linux release assets
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: linux-release-assets
          path: linux-staging/

  release-server:
    name: Build server (${{ matrix.os }})
    needs: verify-versions
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: windows-latest
            artifact: server-windows
          - os: ubuntu-latest
            artifact: server-linux
    runs-on: ${{ matrix.os }}
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

      - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
        with:
          go-version: "1.26"

      - name: Extract version from tag
        shell: bash
        run: |
          VERSION="${GITHUB_REF_NAME#v}"
          echo "VERSION=$VERSION" >> "$GITHUB_ENV"

      - name: Build server (Windows)
        if: matrix.os == 'windows-latest'
        shell: bash
        run: cd Server && go build -o chatserver.exe -ldflags "-s -w -X main.version=$VERSION" .

      - name: Build server (Linux)
        if: matrix.os == 'ubuntu-latest'
        working-directory: Server
        env:
          CGO_ENABLED: "0"
        run: go build -o chatserver -ldflags "-s -w -X main.version=$VERSION" .

      # Boot-smoke the EXACT artifact that ships: this feed drives the signed
      # self-update, so a binary that compiles but dies on boot would deploy
      # itself to every auto-updating instance. CI's tests exercise the same
      # commit but never this build (release ldflags, CGO_ENABLED=0) and never
      # execute the produced binary. First run writes config.yaml, generates a
      # self-signed cert, migrates a fresh SQLite DB — a real cold boot.
      - name: Boot-smoke server binary
        shell: bash
        working-directory: Server
        run: |
          SMOKE_DIR="$RUNNER_TEMP/owncord-smoke"
          mkdir -p "$SMOKE_DIR"
          cd "$SMOKE_DIR"
          BIN="$GITHUB_WORKSPACE/Server/chatserver"
          [ -f "$GITHUB_WORKSPACE/Server/chatserver.exe" ] && BIN="$GITHUB_WORKSPACE/Server/chatserver.exe"
          "$BIN" &
          SERVER_PID=$!
          ok=0
          for _ in $(seq 1 30); do
            sleep 1
            if ! kill -0 "$SERVER_PID" 2>/dev/null; then
              echo "::error::server process exited during boot smoke"
              exit 1
            fi
            if "$BIN" healthcheck; then ok=1; break; fi
          done
          kill "$SERVER_PID" 2>/dev/null || true
          wait "$SERVER_PID" 2>/dev/null || true
          if [ "$ok" != "1" ]; then
            echo "::error::server never reported healthy within 30s"
            exit 1
          fi
          echo "boot smoke passed"

      - name: Create tar.gz (Linux)
        if: matrix.os == 'ubuntu-latest'
        working-directory: Server
        run: tar czf ../chatserver-linux-amd64.tar.gz chatserver

      - name: Upload Windows binary
        if: matrix.os == 'windows-latest'
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: ${{ matrix.artifact }}
          path: Server/chatserver.exe

      - name: Upload Linux archive
        if: matrix.os == 'ubuntu-latest'
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: ${{ matrix.artifact }}
          path: chatserver-linux-amd64.tar.gz

  release-client-linux-arm64:
    name: Build Tauri (Linux ARM64)
    needs: verify-versions
    runs-on: ubuntu-22.04-arm
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
        with:
          node-version: 20
          cache: npm
          cache-dependency-path: Client/tauri-client/package-lock.json

      - name: Install Linux system dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            libwebkit2gtk-4.1-dev \
            libgtk-3-dev \
            libayatana-appindicator3-dev \
            libsecret-1-dev \
            libdbus-1-dev \
            libasound2-dev \
            libssl-dev \
            patchelf \
            librsvg2-dev \
            xdg-utils

      - name: Install Rust
        uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable

      - name: Rust cache
        uses: swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
        with:
          workspaces: Client/tauri-client/src-tauri

      - name: Install npm dependencies
        working-directory: Client/tauri-client
        run: npm ci

      - name: Build Tauri app (AppImage + deb)
        working-directory: Client/tauri-client
        env:
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
        run: npm run tauri build -- --bundles appimage,deb

      # Same strip + re-sign as the x86_64 job — see the comment there.
      - name: Strip host-incompatible libs from AppImage and re-sign
        working-directory: Client/tauri-client
        shell: bash
        env:
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
        run: |
          BUNDLE_DIR="src-tauri/target/release/bundle/appimage"
          APPIMAGE=$(find "$BUNDLE_DIR" -name "*.AppImage" ! -name "*.sig" | head -1)
          bash scripts/strip-appimage-bundled-libs.sh "$APPIMAGE"
          TARBALL="$APPIMAGE.tar.gz"
          rm -f "$TARBALL" "$APPIMAGE.sig" "$TARBALL.sig"
          tar czf "$TARBALL" -C "$(dirname "$APPIMAGE")" "$(basename "$APPIMAGE")"
          # Sign straight from the environment. TAURI_SIGNING_PRIVATE_KEY is
          # the env form of --private-key, so ALSO passing -f/--private-key-path
          # makes the CLI abort: "the argument '--private-key-path' cannot be
          # used with '--private-key'". Keeping the key in the env instead of a
          # temp file also keeps it off the runner's disk.
          npx tauri signer sign -p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" "$APPIMAGE"
          npx tauri signer sign -p "$TAURI_SIGNING_PRIVATE_KEY_PASSWORD" "$TARBALL"

      - name: Stage Linux ARM64 release assets
        shell: bash
        run: |
          mkdir -p linux-arm64-staging
          BUNDLE_DIR="Client/tauri-client/src-tauri/target/release/bundle"
          # AppImage + updater artifact (.tar.gz) + signatures. Every filename
          # must carry the arch: FindClientAssets matches on the
          # _aarch64.AppImage.tar.gz suffix, and arch-less names would collide
          # with the x86_64 assets when both artifact sets are downloaded into
          # the same linux/ directory at publish time. Inserting _aarch64
          # before ".AppImage" renames installer, tar.gz, and .sig
          # consistently, so signatures keep pairing with their artifacts.
          for f in "$BUNDLE_DIR"/appimage/*.AppImage "$BUNDLE_DIR"/appimage/*.AppImage.tar.gz "$BUNDLE_DIR"/appimage/*.sig; do
            [ -f "$f" ] || continue
            base="$(basename "$f")"
            [[ "$base" == *aarch64* ]] || base="${base/.AppImage/_aarch64.AppImage}"
            cp "$f" "linux-arm64-staging/$base"
          done
          # .deb
          for f in "$BUNDLE_DIR"/deb/*.deb; do
            [ -f "$f" ] && cp "$f" linux-arm64-staging/
          done

      - name: Upload Linux ARM64 release assets
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: linux-arm64-release-assets
          path: linux-arm64-staging/

  release-server-docker:
    name: Build & Push Server Docker Image
    needs: verify-versions
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

      - name: Extract version from tag
        shell: bash
        run: |
          VERSION="${GITHUB_REF_NAME#v}"
          echo "VERSION=$VERSION" >> "$GITHUB_ENV"

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract Docker metadata
        id: meta
        uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
        with:
          images: ghcr.io/${{ github.repository_owner }}/owncord-server
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=raw,value=latest

      # Build locally first so the image can be boot-smoked BEFORE anything
      # is pushed — a pushed :latest that dies on boot deploys itself to every
      # `docker compose pull` upgrade.
      - name: Build image (local, for smoke test)
        uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
        with:
          context: Server/
          load: true
          build-args: VERSION=${{ env.VERSION }}
          tags: owncord-smoke:candidate
          cache-from: type=gha
          cache-to: type=gha,mode=max

      # Shared with ci.yml's docker-build job so the smoke itself is exercised
      # on every PR to main — the first alpha.3 release run died here on a
      # smoke-harness bug (bare `docker run`, nowhere writable for the
      # default config) that no pre-merge check had ever run.
      - name: Boot-smoke Docker image
        run: bash Server/scripts/docker-smoke.sh owncord-smoke:candidate

      - name: Build and push
        uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
        with:
          context: Server/
          push: true
          build-args: VERSION=${{ env.VERSION }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  publish:
    name: Publish GitHub Release
    needs: [release-client-windows, release-client-linux, release-client-linux-arm64, release-server, release-server-docker]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
        with:
          node-version: 20
          cache: npm
          cache-dependency-path: Client/tauri-client/package-lock.json

      - name: Download Windows client assets
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
        with:
          name: windows-release-assets
          path: windows

      - name: Download Linux x86_64 client assets
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
        with:
          name: linux-release-assets
          path: linux

      - name: Download Linux ARM64 client assets
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
        with:
          name: linux-arm64-release-assets
          path: linux

      - name: Download Windows server binary
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
        with:
          name: server-windows
          path: windows

      - name: Download Linux server archive
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
        with:
          name: server-linux
          path: linux

      - name: Extract version from tag
        shell: bash
        run: |
          VERSION="${GITHUB_REF_NAME#v}"
          echo "VERSION=$VERSION" >> "$GITHUB_ENV"

      - name: Create source snapshot (AGPL source availability)
        shell: bash
        run: |
          git archive --format=tar.gz --prefix="OwnCord-${VERSION}/" \
            -o "owncord-src-${{ github.ref_name }}.tar.gz" HEAD

      # Checksum lines must use bare asset filenames: the v1.0.0 updater's
      # ParseChecksumFile does an exact match on the last field, so a
      # "windows/" prefix would strand every deployed server on 1.0.0.
      - name: Generate SHA256 checksums
        shell: bash
        run: |
          (cd windows && sha256sum *) > checksums.sha256
          (cd linux && sha256sum *) >> checksums.sha256
          sha256sum owncord-src-*.tar.gz >> checksums.sha256

      # The legacy top-level asset/sha256 pair stays bound to the Windows
      # binary so already-deployed servers (which only understand the
      # single-asset schema) can still verify and update; the assets list
      # binds every OS. Server-side schema: updater.releaseManifest.
      - name: Generate server update manifest
        shell: bash
        run: |
          WIN_HASH=$(sha256sum windows/chatserver.exe | awk '{print $1}')
          LINUX_HASH=$(sha256sum linux/chatserver-linux-amd64.tar.gz | awk '{print $1}')
          printf '{"version":"v%s","asset":"chatserver.exe","sha256":"%s","assets":[{"asset":"chatserver.exe","sha256":"%s"},{"asset":"chatserver-linux-amd64.tar.gz","sha256":"%s"}]}' \
            "$VERSION" "$WIN_HASH" "$WIN_HASH" "$LINUX_HASH" > windows/server-update-manifest.json

      - name: Sign server update assets
        working-directory: Client/tauri-client
        shell: bash
        env:
          SERVER_UPDATE_SIGNING_PRIVATE_KEY: ${{ secrets.SERVER_UPDATE_SIGNING_PRIVATE_KEY }}
          SERVER_UPDATE_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.SERVER_UPDATE_SIGNING_PRIVATE_KEY_PASSWORD }}
        run: |
          KEY_PATH=$(mktemp)
          printf '%s' "$SERVER_UPDATE_SIGNING_PRIVATE_KEY" > "$KEY_PATH"
          trap 'rm -f "$KEY_PATH"' EXIT
          npm ci
          npx tauri signer sign -f "$KEY_PATH" -p "$SERVER_UPDATE_SIGNING_PRIVATE_KEY_PASSWORD" ../../windows/chatserver.exe
          npx tauri signer sign -f "$KEY_PATH" -p "$SERVER_UPDATE_SIGNING_PRIVATE_KEY_PASSWORD" ../../windows/server-update-manifest.json

      # Fail closed before publishing: prove the freshly signed assets verify
      # against the pinned public key that ships inside the server binary.
      # Catches key/pubkey mismatch, signature format drift, and signer flag
      # regressions — each of which has silently broken this pipeline before.
      - name: Verify signed assets against pinned server update key
        shell: bash
        run: |
          sudo apt-get update && sudo apt-get install -y minisign
          base64 -d Server/updater/server_update_public_key.txt > "$RUNNER_TEMP/server_update.pub"
          for f in windows/chatserver.exe windows/server-update-manifest.json; do
            base64 -d "$f.sig" > "$RUNNER_TEMP/asset.minisig"
            minisign -Vm "$f" -x "$RUNNER_TEMP/asset.minisig" -p "$RUNNER_TEMP/server_update.pub"
          done

      - name: Install root dependencies (changelogen)
        run: npm ci

      - name: Generate changelog
        shell: bash
        run: npx changelogen --output CHANGELOG.md

      # Sole publish target. This repo is public, so its own Releases page both
      # satisfies AGPL source availability (via the owncord-src snapshot below)
      # and serves as the publicly-readable feed that deployed servers and
      # clients poll for updates. The former mirror step to a separate public
      # releases repo existed only to work around this repo being private.
      - name: Create GitHub Release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          mapfile -t assets < <(find windows linux -type f)
          assets+=(checksums.sha256 owncord-src-*.tar.gz)
          gh release create "${{ github.ref_name }}" \
            --notes-file CHANGELOG.md \
            "${assets[@]}"
