name: build desktop

on:
  workflow_dispatch:
    inputs:
      channel:
        description: Channel to build (stable or canary)
        required: false
        type: choice
        options:
          - stable
          - canary
        default: stable
      ref:
        description: Git ref to build (branch, tag, or commit SHA)
        required: false
        default: ''
        type: string
      skip_windows:
        description: Skip Windows builds
        required: false
        default: false
        type: boolean
      skip_macos:
        description: Skip macOS builds
        required: false
        default: false
        type: boolean
      skip_linux:
        description: Skip Linux builds
        required: false
        default: false
        type: boolean
      skip_windows_x64:
        description: Skip Windows x64 builds
        required: false
        default: false
        type: boolean
      skip_windows_arm64:
        description: Skip Windows ARM64 builds
        required: false
        default: false
        type: boolean
      skip_macos_x64:
        description: Skip macOS x64 builds
        required: false
        default: false
        type: boolean
      skip_macos_arm64:
        description: Skip macOS ARM64 builds
        required: false
        default: false
        type: boolean
      skip_linux_x64:
        description: Skip Linux x64 builds
        required: false
        default: false
        type: boolean
      skip_linux_arm64:
        description: Skip Linux ARM64 builds
        required: false
        default: false
        type: boolean

permissions:
  contents: write

concurrency:
  group: desktop-${{ inputs.channel }}
  cancel-in-progress: true

env:
  CHANNEL: ${{ inputs.channel }}
  BUILD_CHANNEL: ${{ inputs.channel == 'canary' && 'canary' || 'stable' }}
  SOURCE_REF: ${{ inputs.ref && inputs.ref || (inputs.channel == 'canary' && 'canary' || 'main') }}

jobs:
  meta:
    name: Resolve build metadata
    runs-on: blacksmith-2vcpu-ubuntu-2404
    outputs:
      version: ${{ steps.meta.outputs.version }}
      pub_date: ${{ steps.meta.outputs.pub_date }}
      channel: ${{ steps.meta.outputs.channel }}
      build_channel: ${{ steps.meta.outputs.build_channel }}
      source_ref: ${{ steps.meta.outputs.source_ref }}
    steps:
      - name: Set metadata
        id: meta
        shell: bash
        run: |
          set -euo pipefail
          VERSION="0.0.${GITHUB_RUN_NUMBER}"
          PUB_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
          echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
          echo "pub_date=${PUB_DATE}" >> "$GITHUB_OUTPUT"
          echo "channel=${{ inputs.channel }}" >> "$GITHUB_OUTPUT"
          echo "build_channel=${{ inputs.channel == 'canary' && 'canary' || 'stable' }}" >> "$GITHUB_OUTPUT"
          echo "source_ref=${{ (inputs.ref && inputs.ref) || (inputs.channel == 'canary' && 'canary' || 'main') }}" >> "$GITHUB_OUTPUT"

  matrix:
    name: Resolve build matrix
    runs-on: blacksmith-2vcpu-ubuntu-2404
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - name: Build platform matrix
        id: set-matrix
        shell: bash
        run: |
          set -euo pipefail

          PLATFORMS='[
            {"platform":"windows","arch":"x64","os":"windows-latest","electron_arch":"x64"},
            {"platform":"windows","arch":"arm64","os":"windows-11-arm","electron_arch":"arm64"},
            {"platform":"macos","arch":"x64","os":"macos-15-intel","electron_arch":"x64"},
            {"platform":"macos","arch":"arm64","os":"macos-15","electron_arch":"arm64"},
            {"platform":"linux","arch":"x64","os":"ubuntu-24.04","electron_arch":"x64"},
            {"platform":"linux","arch":"arm64","os":"ubuntu-24.04-arm","electron_arch":"arm64"}
          ]'

          FILTERED="$(echo "$PLATFORMS" | jq -c \
            --argjson skipWin '${{ inputs.skip_windows }}' \
            --argjson skipWinX64 '${{ inputs.skip_windows_x64 }}' \
            --argjson skipWinArm '${{ inputs.skip_windows_arm64 }}' \
            --argjson skipMac '${{ inputs.skip_macos }}' \
            --argjson skipMacX64 '${{ inputs.skip_macos_x64 }}' \
            --argjson skipMacArm '${{ inputs.skip_macos_arm64 }}' \
            --argjson skipLinux '${{ inputs.skip_linux }}' \
            --argjson skipLinuxX64 '${{ inputs.skip_linux_x64 }}' \
            --argjson skipLinuxArm '${{ inputs.skip_linux_arm64 }}' '
              [.[] | select(
                (
                  ((.platform == "windows") and (
                    $skipWin or
                    ((.arch == "x64") and $skipWinX64) or
                    ((.arch == "arm64") and $skipWinArm)
                  )) or
                  ((.platform == "macos") and (
                    $skipMac or
                    ((.arch == "x64") and $skipMacX64) or
                    ((.arch == "arm64") and $skipMacArm)
                  )) or
                  ((.platform == "linux") and (
                    $skipLinux or
                    ((.arch == "x64") and $skipLinuxX64) or
                    ((.arch == "arm64") and $skipLinuxArm)
                  ))
                ) | not
              )]
            ')"

          echo "matrix={\"include\":$FILTERED}" >> "$GITHUB_OUTPUT"

  build:
    name: Build ${{ matrix.platform }} (${{ matrix.arch }})
    needs:
      - meta
      - matrix
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix: ${{ fromJson(needs.matrix.outputs.matrix) }}
    env:
      APP_WORKDIR: fluxer_app
      CHANNEL: ${{ needs.meta.outputs.channel }}
      BUILD_CHANNEL: ${{ needs.meta.outputs.build_channel }}
      SOURCE_REF: ${{ needs.meta.outputs.source_ref }}
      VERSION: ${{ needs.meta.outputs.version }}
      PUB_DATE: ${{ needs.meta.outputs.pub_date }}
    steps:
      - name: Checkout source
        uses: actions/checkout@v6
        with:
          ref: ${{ env.SOURCE_REF }}

      - name: Shorten Windows paths (workspace + temp for Squirrel) and pin pnpm store
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          subst W: "$env:GITHUB_WORKSPACE"
          "APP_WORKDIR=W:\fluxer_app" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8

          New-Item -ItemType Directory -Force "C:\t" | Out-Null
          New-Item -ItemType Directory -Force "C:\sq" | Out-Null
          New-Item -ItemType Directory -Force "C:\ebcache" | Out-Null
          "TEMP=C:\t" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
          "TMP=C:\t" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
          "SQUIRREL_TEMP=C:\sq" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
          "ELECTRON_BUILDER_CACHE=C:\ebcache" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8

          New-Item -ItemType Directory -Force "C:\pnpm-store" | Out-Null
          "NPM_CONFIG_STORE_DIR=C:\pnpm-store" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
          "npm_config_store_dir=C:\pnpm-store" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8

          "store-dir=C:\pnpm-store" | Set-Content -Path "W:\.npmrc" -Encoding ascii
          git config --global core.longpaths true

      - name: Set up pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 10.26.0

      - name: Set up Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 20

      - name: Resolve pnpm store path (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          $store = pnpm store path --silent
          "PNPM_STORE_PATH=$store" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
          New-Item -ItemType Directory -Force $store | Out-Null

      - name: Resolve pnpm store path (Unix)
        if: runner.os != 'Windows'
        shell: bash
        run: |
          set -euo pipefail
          store="$(pnpm store path --silent)"
          echo "PNPM_STORE_PATH=$store" >> "$GITHUB_ENV"
          mkdir -p "$store"

      - name: Cache pnpm store
        uses: actions/cache@v4
        with:
          path: ${{ env.PNPM_STORE_PATH }}
          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-store-

      - name: Install Python setuptools (Windows ARM64)
        if: matrix.platform == 'windows' && matrix.arch == 'arm64'
        shell: pwsh
        run: |
          python -m pip install --upgrade pip
          python -m pip install "setuptools>=69" wheel

      - name: Install Python setuptools (macOS)
        if: matrix.platform == 'macos'
        run: brew install python-setuptools

      - name: Install Linux dependencies
        if: matrix.platform == 'linux'
        env:
          DEBIAN_FRONTEND: noninteractive
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            libx11-dev libxtst-dev libxt-dev libxinerama-dev libxkbcommon-dev libxrandr-dev \
            ruby ruby-dev build-essential rpm \
            libpixman-1-dev libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev
          sudo gem install --no-document fpm

      - name: Install dependencies
        working-directory: ${{ env.APP_WORKDIR }}
        run: pnpm install --frozen-lockfile

      - name: Update version
        working-directory: ${{ env.APP_WORKDIR }}
        run: pnpm version "${{ env.VERSION }}" --no-git-tag-version --allow-same-version

      - name: Build Electron main process
        working-directory: ${{ env.APP_WORKDIR }}
        env:
          BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }}
        run: pnpm electron:compile

      - name: Build Electron app (macOS)
        if: matrix.platform == 'macos'
        working-directory: ${{ env.APP_WORKDIR }}
        env:
          BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }}
          CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
          CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
        run: pnpm exec electron-builder --config electron-builder.config.cjs --mac --${{ matrix.electron_arch }}

      - name: Verify macOS bundle ID (fail fast if wrong channel)
        if: matrix.platform == 'macos'
        working-directory: ${{ env.APP_WORKDIR }}
        shell: bash
        env:
          BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }}
        run: |
          set -euo pipefail
          DIST="dist-electron"
          ZIP="$(ls -1 "$DIST"/*"${{ matrix.electron_arch }}"*.zip | head -n1)"
          tmp="$(mktemp -d)"
          ditto -xk "$ZIP" "$tmp"
          APP="$(find "$tmp" -maxdepth 2 -name "*.app" -print -quit)"
          BID=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP/Contents/Info.plist")

          expected="app.fluxer"
          if [[ "${BUILD_CHANNEL:-stable}" == "canary" ]]; then expected="app.fluxer.canary"; fi
          echo "Bundle id in zip: $BID (expected: $expected)"
          test "$BID" = "$expected"

      - name: Build Electron app (Windows)
        if: matrix.platform == 'windows'
        working-directory: ${{ env.APP_WORKDIR }}
        env:
          BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }}
          TEMP: C:\t
          TMP: C:\t
          SQUIRREL_TEMP: C:\sq
          ELECTRON_BUILDER_CACHE: C:\ebcache
        run: pnpm exec electron-builder --config electron-builder.config.cjs --win --${{ matrix.electron_arch }}

      - name: Analyze Squirrel nupkg for long paths
        if: matrix.platform == 'windows'
        working-directory: ${{ env.APP_WORKDIR }}
        shell: pwsh
        env:
          BUILD_VERSION: ${{ env.VERSION }}
          MAX_WINDOWS_PATH_LEN: 260
          PATH_HEADROOM: 10
        run: |
          $primaryDir = if ("${{ matrix.arch }}" -eq "arm64") { "dist-electron/squirrel-windows-arm64" } else { "dist-electron/squirrel-windows" }
          $fallbackDir = if ("${{ matrix.arch }}" -eq "arm64") { "dist-electron/squirrel-windows" } else { "dist-electron/squirrel-windows-arm64" }
          $dirs = @($primaryDir, $fallbackDir)

          $nupkg = $null
          foreach ($d in $dirs) {
            if (Test-Path $d) {
              $nupkg = Get-ChildItem -Path "$d/*.nupkg" -ErrorAction SilentlyContinue | Select-Object -First 1
              if ($nupkg) { break }
            }
          }

          if (-not $nupkg) {
            throw "No Squirrel nupkg found in: $($dirs -join ', ')"
          }

          Write-Host "Analyzing Windows installer $($nupkg.FullName)"
          $env:NUPKG_PATH = $nupkg.FullName

          $lines = @(
            'import os'
            'import zipfile'
            ''
            'path = os.environ["NUPKG_PATH"]'
            'build_ver = os.environ["BUILD_VERSION"]'
            'prefix = os.path.join(os.environ["LOCALAPPDATA"], "fluxer_app", f"app-{build_ver}", "resources", "app.asar.unpacked")'
            'max_len = int(os.environ.get("MAX_WINDOWS_PATH_LEN", "260"))'
            'headroom = int(os.environ.get("PATH_HEADROOM", "10"))'
            'limit = max_len - headroom'
            ''
            'with zipfile.ZipFile(path) as archive:'
            '    entries = []'
            '    for info in archive.infolist():'
            '        normalized = info.filename.lstrip("/\\\\")'
            '        total_len = len(os.path.join(prefix, normalized)) if normalized else len(prefix)'
            '        entries.append((total_len, info.filename))'
            ''
            'if not entries:'
            '    raise SystemExit("nupkg archive contains no entries")'
            ''
            'entries.sort(reverse=True)'
            'print(f"Assumed install prefix: {prefix} ({len(prefix)} chars). Maximum allowed path length: {limit} (total reserve {max_len}, headroom {headroom}).")'
            'print("Top 20 longest archived paths (length includes prefix):")'
            'for length, name in entries[:20]:'
            '    print(f"{length:4d} {name}")'
            ''
            'longest_len, longest_name = entries[0]'
            'if longest_len > limit:'
            '    raise SystemExit(f"Longest path {longest_len} for {longest_name} exceeds limit {limit}")'
            'print(f"Longest archived path {longest_len} is within the limit of {limit}.")'
          )

          $scriptPath = Join-Path $env:TEMP "nupkg-long-path-check.py"
          Set-Content -Path $scriptPath -Value $lines -Encoding utf8
          python $scriptPath

      - name: Build Electron app (Linux)
        if: matrix.platform == 'linux'
        working-directory: ${{ env.APP_WORKDIR }}
        env:
          BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }}
          USE_SYSTEM_FPM: true
        run: pnpm exec electron-builder --config electron-builder.config.cjs --linux --${{ matrix.electron_arch }}

      - name: Prepare artifacts (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          New-Item -ItemType Directory -Force upload_staging | Out-Null

          $dist = Join-Path $env:APP_WORKDIR "dist-electron"
          $sqDirName = if ("${{ matrix.arch }}" -eq "arm64") { "squirrel-windows-arm64" } else { "squirrel-windows" }
          $sqFallbackName = if ($sqDirName -eq "squirrel-windows") { "squirrel-windows-arm64" } else { "squirrel-windows" }

          $sq = Join-Path $dist $sqDirName
          $sqFallback = Join-Path $dist $sqFallbackName

          $picked = $null
          if (Test-Path $sq) { $picked = $sq }
          elseif (Test-Path $sqFallback) { $picked = $sqFallback }

          if ($picked) {
            Copy-Item -Force -ErrorAction SilentlyContinue "$picked\*.exe" "upload_staging\"
            Copy-Item -Force -ErrorAction SilentlyContinue "$picked\*.exe.blockmap" "upload_staging\"
            Copy-Item -Force -ErrorAction SilentlyContinue "$picked\RELEASES*" "upload_staging\"
            Copy-Item -Force -ErrorAction SilentlyContinue "$picked\*.nupkg" "upload_staging\"
            Copy-Item -Force -ErrorAction SilentlyContinue "$picked\*.nupkg.blockmap" "upload_staging\"
          }

          if (Test-Path $dist) {
            Copy-Item -Force -ErrorAction SilentlyContinue "$dist\*.yml" "upload_staging\"
            Copy-Item -Force -ErrorAction SilentlyContinue "$dist\*.zip" "upload_staging\"
            Copy-Item -Force -ErrorAction SilentlyContinue "$dist\*.zip.blockmap" "upload_staging\"
          }

          if (-not (Get-ChildItem upload_staging -Filter *.exe -ErrorAction SilentlyContinue)) {
            throw "No installer .exe staged. Squirrel outputs were not copied."
          }

          Get-ChildItem -Force upload_staging | Format-Table -AutoSize

      - name: Prepare artifacts (Unix)
        if: runner.os != 'Windows'
        shell: bash
        run: |
          set -euo pipefail
          mkdir -p upload_staging
          DIST="${{ env.APP_WORKDIR }}/dist-electron"

          cp -f "$DIST"/*.dmg upload_staging/ 2>/dev/null || true
          cp -f "$DIST"/*.zip upload_staging/ 2>/dev/null || true
          cp -f "$DIST"/*.zip.blockmap upload_staging/ 2>/dev/null || true
          cp -f "$DIST"/*.yml upload_staging/ 2>/dev/null || true

          cp -f "$DIST"/*.AppImage upload_staging/ 2>/dev/null || true
          cp -f "$DIST"/*.deb upload_staging/ 2>/dev/null || true
          cp -f "$DIST"/*.rpm upload_staging/ 2>/dev/null || true
          cp -f "$DIST"/*.tar.gz upload_staging/ 2>/dev/null || true

          ls -la upload_staging/

      - name: Normalize updater YAML (arm64)
        if: matrix.arch == 'arm64'
        shell: bash
        run: |
          set -euo pipefail
          cd upload_staging
          [[ "${{ matrix.platform }}" == "macos" && -f latest-mac.yml && ! -f latest-mac-arm64.yml ]] && mv latest-mac.yml latest-mac-arm64.yml || true

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: fluxer-desktop-${{ env.BUILD_CHANNEL }}-${{ matrix.platform }}-${{ matrix.arch }}
          path: |
            upload_staging/*.exe
            upload_staging/*.exe.blockmap
            upload_staging/*.dmg
            upload_staging/*.zip
            upload_staging/*.zip.blockmap
            upload_staging/*.AppImage
            upload_staging/*.deb
            upload_staging/*.rpm
            upload_staging/*.tar.gz
            upload_staging/*.yml
            upload_staging/*.nupkg
            upload_staging/*.nupkg.blockmap
            upload_staging/RELEASES*
          retention-days: 30

  upload:
    name: Upload to S3 (rclone)
    needs:
      - meta
      - build
    runs-on: blacksmith-2vcpu-ubuntu-2404
    env:
      CHANNEL: ${{ needs.meta.outputs.build_channel }}
      DISPLAY_CHANNEL: ${{ needs.meta.outputs.channel }}
      S3_ENDPOINT: https://s3.us-east-va.io.cloud.ovh.us
      S3_BUCKET: fluxer-downloads
      PUBLIC_DL_BASE: https://api.fluxer.app/dl
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    steps:
      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts
          pattern: fluxer-desktop-${{ needs.meta.outputs.build_channel }}-*

      - name: Install rclone
        run: |
          set -euo pipefail
          if ! command -v rclone >/dev/null 2>&1; then
            curl -fsSL https://rclone.org/install.sh | sudo bash
          fi

      - name: Configure rclone (OVH S3)
        run: |
          set -euo pipefail
          mkdir -p ~/.config/rclone
          cat > ~/.config/rclone/rclone.conf <<'RCLONEEOF'
          [ovh]
          type = s3
          provider = Other
          env_auth = true
          endpoint = https://s3.us-east-va.io.cloud.ovh.us
          acl = private
          RCLONEEOF

      - name: Build S3 payload layout (+ manifest.json)
        env:
          VERSION: ${{ needs.meta.outputs.version }}
          PUB_DATE: ${{ needs.meta.outputs.pub_date }}
        run: |
          set -euo pipefail

          mkdir -p s3_payload

          shopt -s nullglob
          for dir in artifacts/fluxer-desktop-${CHANNEL}-*; do
            [ -d "$dir" ] || continue

            base="$(basename "$dir")"
            if [[ "$base" =~ ^fluxer-desktop-[a-z]+-([a-z]+)-([a-z0-9]+)$ ]]; then
              platform="${BASH_REMATCH[1]}"
              arch="${BASH_REMATCH[2]}"
            else
              echo "Skipping unrecognized artifact dir: $base"
              continue
            fi

            case "$platform" in
              windows) plat="win32" ;;
              macos) plat="darwin" ;;
              linux) plat="linux" ;;
              *)
                echo "Unknown platform: $platform"
                continue
                ;;
            esac

            dest="s3_payload/desktop/${CHANNEL}/${plat}/${arch}"
            mkdir -p "$dest"
            cp -av "$dir"/* "$dest/" || true

            if [[ "$plat" == "darwin" ]]; then
              zip_file=""
              for z in "$dest"/*.zip; do
                zip_file="$z"
                break
              done

              if [[ -z "$zip_file" ]]; then
                echo "No .zip found for macOS $arch in $dest (auto-update requires zip artifacts)."
              else
                zip_name="$(basename "$zip_file")"
                url="${PUBLIC_DL_BASE}/desktop/${CHANNEL}/${plat}/${arch}/${zip_name}"

                cat > "$dest/RELEASES.json" <<EOF
          {
            "currentRelease": "${VERSION}",
            "releases": [
              {
                "version": "${VERSION}",
                "updateTo": {
                  "version": "${VERSION}",
                  "pub_date": "${PUB_DATE}",
                  "notes": "",
                  "name": "${VERSION}",
                  "url": "${url}"
                }
              }
            ]
          }
          EOF
                cp -f "$dest/RELEASES.json" "$dest/releases.json"
              fi
            fi

            setup_file=""
            dmg_file=""
            zip_file2=""
            appimage_file=""
            deb_file=""
            rpm_file=""
            targz_file=""

            if [[ "$plat" == "win32" ]]; then
              setup_file="$(ls -1 "$dest"/*.exe 2>/dev/null | grep -i 'setup' | head -n1 || true)"
              if [[ -z "$setup_file" ]]; then
                setup_file="$(ls -1 "$dest"/*.exe 2>/dev/null | head -n1 || true)"
              fi
            fi

            if [[ "$plat" == "darwin" ]]; then
              dmg_file="$(ls -1 "$dest"/*.dmg 2>/dev/null | head -n1 || true)"
              zip_file2="$(ls -1 "$dest"/*.zip 2>/dev/null | head -n1 || true)"
            fi

            if [[ "$plat" == "linux" ]]; then
              appimage_file="$(ls -1 "$dest"/*.AppImage 2>/dev/null | head -n1 || true)"
              deb_file="$(ls -1 "$dest"/*.deb 2>/dev/null | head -n1 || true)"
              rpm_file="$(ls -1 "$dest"/*.rpm 2>/dev/null | head -n1 || true)"
              targz_file="$(ls -1 "$dest"/*.tar.gz 2>/dev/null | head -n1 || true)"
            fi

            jq -n \
              --arg channel "${CHANNEL}" \
              --arg platform "${plat}" \
              --arg arch "${arch}" \
              --arg version "${VERSION}" \
              --arg pub_date "${PUB_DATE}" \
              --arg setup "$(basename "${setup_file:-}")" \
              --arg dmg "$(basename "${dmg_file:-}")" \
              --arg zip "$(basename "${zip_file2:-}")" \
              --arg appimage "$(basename "${appimage_file:-}")" \
              --arg deb "$(basename "${deb_file:-}")" \
              --arg rpm "$(basename "${rpm_file:-}")" \
              --arg tar_gz "$(basename "${targz_file:-}")" \
              '{
                channel: $channel,
                platform: $platform,
                arch: $arch,
                version: $version,
                pub_date: $pub_date,
                files: {
                  setup: $setup,
                  dmg: $dmg,
                  zip: $zip,
                  appimage: $appimage,
                  deb: $deb,
                  rpm: $rpm,
                  tar_gz: $tar_gz
                }
              }' > "$dest/manifest.json"
          done

          echo "Payload tree:"
          find s3_payload -maxdepth 6 -type f | sort

      - name: Upload payload to S3
        run: |
          set -euo pipefail
          rclone copy s3_payload/desktop "ovh:${S3_BUCKET}/desktop" \
            --transfers 32 \
            --checkers 16 \
            --fast-list \
            --s3-upload-concurrency 8 \
            --s3-chunk-size 16M \
            -v

      - name: Build summary
        run: |
          {
            echo "## Desktop ${DISPLAY_CHANNEL^} Upload Complete"
            echo ""
            echo "**Version:** ${{ needs.meta.outputs.version }}"
            echo ""
            echo "**S3 prefix:** desktop/${CHANNEL}/"
            echo ""
            echo "**Redirect endpoint shape:** /dl/desktop/${CHANNEL}/{plat}/{arch}/{format}"
          } >> "$GITHUB_STEP_SUMMARY"
