name: Release

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:

permissions:
  contents: write

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

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v6
        with:
          version: 10.33.0

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Set version from tag
        if: startsWith(github.ref, 'refs/tags/v')
        run: |
          VERSION="${GITHUB_REF#refs/tags/v}"
          pnpm pkg set version="$VERSION"

      - name: Verify Sentry release env
        if: startsWith(github.ref, 'refs/tags/v')
        env:
          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
          SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
        run: node ./scripts/ci/verify-sentry-release.cjs prebuild

      - name: Build app
        env:
          NODE_OPTIONS: '--max-old-space-size=8192'
          SENTRY_DSN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_DSN || '' }}
          SENTRY_AUTH_TOKEN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_AUTH_TOKEN || '' }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
          SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
        run: pnpm build

      - name: Verify Sentry source map upload
        if: startsWith(github.ref, 'refs/tags/v')
        env:
          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
          SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
        run: node ./scripts/ci/verify-sentry-release.cjs postbuild

      - name: Create GitHub Release
        if: startsWith(github.ref, 'refs/tags/v')
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          TAG="${GITHUB_REF#refs/tags/}"
          gh release create "$TAG" \
            --repo "$GITHUB_REPOSITORY" \
            --title "$TAG" \
            --generate-notes \
            --draft=true 2>/dev/null || echo "Release $TAG already exists, skipping creation"

      - name: Upload dist artifact
        uses: actions/upload-artifact@v7
        with:
          name: dist
          path: |
            out/renderer
            dist-electron
          retention-days: 1

  prepare-runtime:
    runs-on: ubuntu-latest
    timeout-minutes: 90

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Create GitHub Release
        if: startsWith(github.ref, 'refs/tags/v')
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          TAG="${GITHUB_REF#refs/tags/}"
          gh release create "$TAG" \
            --repo "$GITHUB_REPOSITORY" \
            --title "$TAG" \
            --generate-notes \
            --draft=true 2>/dev/null || echo "Release $TAG already exists, skipping creation"

      - name: Skip runtime asset preparation for manual builds
        if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
        run: echo "Runtime asset preparation is only needed for tagged releases."

      - name: Check runtime assets
        if: startsWith(github.ref, 'refs/tags/v')
        id: runtime-assets
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          TAG="${GITHUB_REF#refs/tags/}"
          existing="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' 2>/dev/null || true)"
          missing=0
          while IFS= read -r asset; do
            [ -n "$asset" ] || continue
            if ! grep -Fqx "$asset" <<<"$existing"; then
              echo "Missing runtime asset: $asset"
              missing=1
            fi
          done < <(node ./scripts/runtime-lock.mjs asset-list)
          echo "missing=$missing" >> "$GITHUB_OUTPUT"

      - name: Run and wait for private runtime build
        if: steps.runtime-assets.outputs.missing == '1'
        env:
          GH_TOKEN: ${{ secrets.RUNTIME_BUILD_DISPATCH_TOKEN }}
        run: |
          set -euo pipefail
          if [ -z "${GH_TOKEN:-}" ]; then
            echo "Missing RUNTIME_BUILD_DISPATCH_TOKEN secret" >&2
            exit 1
          fi

          TARGET_TAG="${GITHUB_REF#refs/tags/}"
          SOURCE_REPO="$(node ./scripts/runtime-lock.mjs source-repository)"
          SOURCE_REF="$(node ./scripts/runtime-lock.mjs source-ref)"
          RUNTIME_VERSION="$(node ./scripts/runtime-lock.mjs version)"
          RUN_TITLE="runtime ${RUNTIME_VERSION} -> ${GITHUB_REPOSITORY}@${TARGET_TAG}"
          STARTED_AT="$(node -e 'console.log(new Date(Date.now() - 120000).toISOString())')"

          gh workflow run release-runtime.yml \
            --repo "$SOURCE_REPO" \
            --ref main \
            -f "source_ref=$SOURCE_REF" \
            -f "runtime_version=$RUNTIME_VERSION" \
            -f "target_release_repo=$GITHUB_REPOSITORY" \
            -f "target_release_tag=$TARGET_TAG"

          run_id=""
          for attempt in $(seq 1 60); do
            run_id="$(
              gh run list \
                --repo "$SOURCE_REPO" \
                --workflow release-runtime.yml \
                --event workflow_dispatch \
                --limit 30 \
                --json databaseId,displayTitle,createdAt \
                --jq '.[] | select(.displayTitle == "'"$RUN_TITLE"'" and .createdAt >= "'"$STARTED_AT"'") | .databaseId' \
                | head -n 1
            )"

            if [ -n "$run_id" ]; then
              echo "Found orchestrator runtime workflow run: $run_id"
              for wait_attempt in $(seq 1 240); do
                run_state="$(
                  gh run view "$run_id" \
                    --repo "$SOURCE_REPO" \
                    --json status,conclusion,url \
                    --jq '[.status, (.conclusion // ""), .url] | @tsv'
                )"
                IFS=$'\t' read -r status conclusion url <<< "$run_state"

                if [ "$status" = "completed" ]; then
                  if [ "$conclusion" = "success" ]; then
                    echo "Orchestrator runtime workflow succeeded: $url"
                    exit 0
                  fi

                  echo "Orchestrator runtime workflow failed with conclusion '$conclusion': $url" >&2
                  exit 1
                fi

                echo "Orchestrator runtime workflow status: $status - wait $wait_attempt/240"
                sleep 15
              done

              echo "Timed out waiting for orchestrator runtime workflow completion: $run_id" >&2
              exit 1
            fi

            echo "Waiting for orchestrator runtime workflow run - attempt $attempt/60"
            sleep 5
          done

          echo "Timed out waiting for orchestrator runtime workflow run: $RUN_TITLE" >&2
          exit 1

      - name: Verify runtime assets
        if: steps.runtime-assets.outputs.missing == '1'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          TAG="${GITHUB_REF#refs/tags/}"
          for attempt in $(seq 1 12); do
            existing="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' 2>/dev/null || true)"
            all_found=1
            while IFS= read -r asset; do
              [ -n "$asset" ] || continue
              if ! grep -Fqx "$asset" <<<"$existing"; then
                all_found=0
                break
              fi
            done < <(node ./scripts/runtime-lock.mjs asset-list)

            if [ "$all_found" -eq 1 ]; then
              echo "Runtime assets are ready"
              exit 0
            fi

            echo "Waiting for runtime assets after orchestrator success - attempt $attempt/12"
            sleep 10
          done

          echo "Timed out waiting for runtime assets in release $TAG" >&2
          exit 1

  release-mac:
    needs: [build, prepare-runtime]
    timeout-minutes: 90
    strategy:
      fail-fast: false
      matrix:
        include:
          - arch: arm64
            runner: macos-14
            dist_command: pnpm pack:mac:arm64
          - arch: x64
            runner: macos-15-intel
            dist_command: pnpm pack:mac:x64
    runs-on: ${{ matrix.runner }}

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Download dist artifact
        uses: actions/download-artifact@v8
        with:
          name: dist

      - name: Setup pnpm
        uses: pnpm/action-setup@v6
        with:
          version: 10.33.0

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: pnpm

      - name: Setup Python for node-gyp
        uses: actions/setup-python@v6
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Set version from tag
        if: startsWith(github.ref, 'refs/tags/v')
        run: |
          VERSION="${GITHUB_REF#refs/tags/v}"
          pnpm pkg set version="$VERSION"

      - name: Stage bundled runtime (macOS ${{ matrix.arch }})
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        shell: bash
        run: |
          set -euo pipefail
          if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then
            TAG="${GITHUB_REF#refs/tags/}"
            node ./scripts/stage-runtime.mjs --platform "darwin-${{ matrix.arch }}" --release-tag "$TAG"
          else
            node ./scripts/stage-runtime.mjs --platform "darwin-${{ matrix.arch }}"
          fi

      - name: Verify Sentry release env (macOS ${{ matrix.arch }})
        if: startsWith(github.ref, 'refs/tags/v')
        env:
          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
          SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
        run: node ./scripts/ci/verify-sentry-release.cjs prebuild

      - name: Build app (macOS ${{ matrix.arch }})
        env:
          NODE_OPTIONS: '--max-old-space-size=8192'
          SENTRY_DSN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_DSN || '' }}
          SENTRY_AUTH_TOKEN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_AUTH_TOKEN || '' }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
          SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
        run: pnpm build

      - name: Verify Sentry source map upload (macOS ${{ matrix.arch }})
        if: startsWith(github.ref, 'refs/tags/v')
        env:
          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
          SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
        run: node ./scripts/ci/verify-sentry-release.cjs postbuild

      - name: Verify packaged inputs (macOS ${{ matrix.arch }})
        run: |
          test -f dist-electron/main/index.cjs
          test -f dist-electron/preload/index.js
          test -f out/renderer/index.html
          test -f mcp-server/dist/index.js
          test -f resources/runtime/VERSION

      - name: Package (macOS ${{ matrix.arch }})
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          CSC_LINK: ${{ secrets.CSC_LINK }}
          CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
        run: ${{ matrix.dist_command }} --publish never

      - name: Validate packaged bundle (macOS ${{ matrix.arch }})
        run: node ./scripts/electron-builder/verifyBundle.cjs "release/mac-${{ matrix.arch }}/Agent Teams UI.app" darwin ${{ matrix.arch }}

      - name: Smoke packaged app (macOS ${{ matrix.arch }})
        run: node ./scripts/electron-builder/smokePackagedApp.cjs "release/mac-${{ matrix.arch }}/Agent Teams UI.app" darwin

      - name: Upload assets to release
        if: startsWith(github.ref, 'refs/tags/v')
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          TAG="${GITHUB_REF#refs/tags/}"
          for f in release/*.dmg release/*.zip release/*.blockmap; do
            [ -f "$f" ] || continue
            echo "Uploading: $f"
            gh release upload "$TAG" "$f" --repo "$GITHUB_REPOSITORY" --clobber 2>&1 || \
              (sleep 5 && gh release upload "$TAG" "$f" --repo "$GITHUB_REPOSITORY" --clobber 2>&1)
          done

  release-win:
    needs: [build, prepare-runtime]
    runs-on: windows-latest
    timeout-minutes: 60

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Download dist artifact
        uses: actions/download-artifact@v8
        with:
          name: dist

      - name: Setup pnpm
        uses: pnpm/action-setup@v6
        with:
          version: 10.33.0

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: pnpm

      - name: Setup Python for node-gyp
        uses: actions/setup-python@v6
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Set version from tag
        if: startsWith(github.ref, 'refs/tags/v')
        shell: bash
        run: |
          VERSION="${GITHUB_REF#refs/tags/v}"
          pnpm pkg set version="$VERSION"

      - name: Stage bundled runtime (Windows)
        shell: pwsh
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          $ErrorActionPreference = "Stop"
          if ($env:GITHUB_REF -like 'refs/tags/v*') {
            $tag = $env:GITHUB_REF.Replace('refs/tags/', '')
            node ./scripts/stage-runtime.mjs --platform win32-x64 --release-tag $tag
          } else {
            node ./scripts/stage-runtime.mjs --platform win32-x64
          }

      - name: Verify Sentry release env (Windows)
        if: startsWith(github.ref, 'refs/tags/v')
        env:
          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
          SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
        run: node ./scripts/ci/verify-sentry-release.cjs prebuild

      - name: Build app (Windows)
        env:
          NODE_OPTIONS: '--max-old-space-size=8192'
          SENTRY_DSN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_DSN || '' }}
          SENTRY_AUTH_TOKEN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_AUTH_TOKEN || '' }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
          SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
        run: pnpm build

      - name: Verify Sentry source map upload (Windows)
        if: startsWith(github.ref, 'refs/tags/v')
        env:
          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
          SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
        run: node ./scripts/ci/verify-sentry-release.cjs postbuild

      - name: Verify packaged inputs (Windows)
        shell: bash
        run: |
          test -f dist-electron/main/index.cjs
          test -f dist-electron/preload/index.js
          test -f out/renderer/index.html
          test -f mcp-server/dist/index.js
          test -f resources/runtime/VERSION

      - name: Package (Windows)
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: pnpm pack:win --publish never

      - name: Validate packaged bundle (Windows)
        shell: bash
        run: node ./scripts/electron-builder/verifyBundle.cjs "release/win-unpacked" win32 x64

      - name: Smoke packaged app (Windows)
        shell: bash
        run: node ./scripts/electron-builder/smokePackagedApp.cjs "release/win-unpacked" win32

      - name: Upload assets to release
        if: startsWith(github.ref, 'refs/tags/v')
        shell: bash
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          TAG="${GITHUB_REF#refs/tags/}"
          for f in release/*.exe release/*.blockmap; do
            [ -f "$f" ] || continue
            echo "Uploading: $f"
            gh release upload "$TAG" "$f" --repo "$GITHUB_REPOSITORY" --clobber 2>&1 || \
              (sleep 5 && gh release upload "$TAG" "$f" --repo "$GITHUB_REPOSITORY" --clobber 2>&1)
          done

  release-linux:
    needs: [build, prepare-runtime]
    runs-on: ubuntu-latest
    timeout-minutes: 90

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Download dist artifact
        uses: actions/download-artifact@v8
        with:
          name: dist

      - name: Setup pnpm
        uses: pnpm/action-setup@v6
        with:
          version: 10.33.0

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 22
          cache: pnpm

      - name: Setup Python for node-gyp
        uses: actions/setup-python@v6
        with:
          python-version: '3.11'

      - name: Install Linux packaging dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y libarchive-tools rpm xvfb

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Set version from tag
        if: startsWith(github.ref, 'refs/tags/v')
        run: |
          VERSION="${GITHUB_REF#refs/tags/v}"
          pnpm pkg set version="$VERSION"

      - name: Stage bundled runtime (Linux)
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        shell: bash
        run: |
          set -euo pipefail
          if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then
            TAG="${GITHUB_REF#refs/tags/}"
            node ./scripts/stage-runtime.mjs --platform linux-x64 --release-tag "$TAG"
          else
            node ./scripts/stage-runtime.mjs --platform linux-x64
          fi

      - name: Verify Sentry release env (Linux)
        if: startsWith(github.ref, 'refs/tags/v')
        env:
          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
          SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
        run: node ./scripts/ci/verify-sentry-release.cjs prebuild

      - name: Build app (Linux)
        env:
          NODE_OPTIONS: '--max-old-space-size=8192'
          SENTRY_DSN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_DSN || '' }}
          SENTRY_AUTH_TOKEN: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.SENTRY_AUTH_TOKEN || '' }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
          SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
        run: pnpm build

      - name: Verify Sentry source map upload (Linux)
        if: startsWith(github.ref, 'refs/tags/v')
        env:
          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
          SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
        run: node ./scripts/ci/verify-sentry-release.cjs postbuild

      - name: Verify packaged inputs (Linux)
        run: |
          test -f dist-electron/main/index.cjs
          test -f dist-electron/preload/index.js
          test -f out/renderer/index.html
          test -f mcp-server/dist/index.js
          test -f resources/runtime/VERSION

      - name: Package (Linux)
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: pnpm pack:linux --publish never

      - name: Validate packaged bundle (Linux)
        run: node ./scripts/electron-builder/verifyBundle.cjs "release/linux-unpacked" linux x64

      - name: Smoke packaged app (Linux)
        run: xvfb-run -a node ./scripts/electron-builder/smokePackagedApp.cjs "release/linux-unpacked" linux

      - name: Upload assets to release
        if: startsWith(github.ref, 'refs/tags/v')
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -x
          TAG="${GITHUB_REF#refs/tags/}"
          ls -la release/ || true
          for f in release/*.AppImage release/*.deb release/*.rpm release/*.pacman release/*.blockmap; do
            [ -f "$f" ] || continue
            echo "Uploading: $f"
            gh release upload "$TAG" "$f" --repo "$GITHUB_REPOSITORY" --clobber 2>&1 || \
              (sleep 5 && gh release upload "$TAG" "$f" --repo "$GITHUB_REPOSITORY" --clobber 2>&1)
          done

  upload-stable-links:
    needs: [release-mac, release-win, release-linux]
    runs-on: ubuntu-latest
    timeout-minutes: 30
    if: startsWith(github.ref, 'refs/tags/v')

    steps:
      - name: Upload stable-named assets for /latest/download links
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          VERSION="${GITHUB_REF#refs/tags/v}"
          REPO="${GITHUB_REPOSITORY}"
          TMP_DIR="$(mktemp -d)"
          trap 'rm -rf "$TMP_DIR"' EXIT

          declare -A FILES=(
            ["Claude-Agent-Teams-UI-arm64.dmg"]="Agent.Teams.AI-${VERSION}-arm64.dmg"
            ["Claude-Agent-Teams-UI-x64.dmg"]="Agent.Teams.AI-${VERSION}-x64.dmg"
            ["Claude-Agent-Teams-UI-Setup.exe"]="Agent.Teams.AI.Setup.${VERSION}.exe"
            ["Claude-Agent-Teams-UI.AppImage"]="Agent.Teams.AI-${VERSION}.AppImage"
            ["Claude-Agent-Teams-UI-amd64.deb"]="agent-teams-ai_${VERSION}_amd64.deb"
            ["Claude-Agent-Teams-UI-x86_64.rpm"]="agent-teams-ai-${VERSION}.x86_64.rpm"
            ["Claude-Agent-Teams-UI.pacman"]="agent-teams-ai-${VERSION}.pacman"
          )

          # Download versioned files and re-upload with stable names
          for STABLE_NAME in "${!FILES[@]}"; do
            VERSIONED_NAME="${FILES[$STABLE_NAME]}"
            echo "Downloading ${VERSIONED_NAME} -> ${STABLE_NAME}"
            gh release download "v${VERSION}" \
              --repo "$REPO" \
              --pattern "${VERSIONED_NAME}" \
              --dir "$TMP_DIR" \
              --clobber
            cp "${TMP_DIR}/${VERSIONED_NAME}" "${TMP_DIR}/${STABLE_NAME}"
            gh release upload "v${VERSION}" "${TMP_DIR}/${STABLE_NAME}" --repo "$REPO" --clobber
          done

      - name: Publish canonical updater metadata
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          VERSION="${GITHUB_REF#refs/tags/v}"
          TAG="v${VERSION}"
          REPO="${GITHUB_REPOSITORY}"
          RELEASE_DATE="$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")"
          TMP_DIR="$(mktemp -d)"
          cd "$TMP_DIR"

          sha512_base64() {
            openssl dgst -sha512 -binary "$1" | openssl base64 -A
          }

          file_size() {
            wc -c < "$1" | tr -d '[:space:]'
          }

          download_asset() {
            local name="$1"
            gh release download "${TAG}" \
              --repo "${REPO}" \
              --pattern "$name" \
              --dir . \
              --clobber
          }

          # Canonical Windows feed
          download_asset "Claude-Agent-Teams-UI-Setup.exe"
          WIN_SHA="$(sha512_base64 "Claude-Agent-Teams-UI-Setup.exe")"
          WIN_SIZE="$(file_size "Claude-Agent-Teams-UI-Setup.exe")"
          cat > latest.yml <<EOF
          version: ${VERSION}
          files:
            - url: Claude-Agent-Teams-UI-Setup.exe
              sha512: ${WIN_SHA}
              size: ${WIN_SIZE}
          path: Claude-Agent-Teams-UI-Setup.exe
          sha512: ${WIN_SHA}
          releaseDate: '${RELEASE_DATE}'
          EOF

          # Canonical Linux feed
          download_asset "Claude-Agent-Teams-UI.AppImage"
          LINUX_SHA="$(sha512_base64 "Claude-Agent-Teams-UI.AppImage")"
          LINUX_SIZE="$(file_size "Claude-Agent-Teams-UI.AppImage")"
          cat > latest-linux.yml <<EOF
          version: ${VERSION}
          files:
            - url: Claude-Agent-Teams-UI.AppImage
              sha512: ${LINUX_SHA}
              size: ${LINUX_SIZE}
          path: Claude-Agent-Teams-UI.AppImage
          sha512: ${LINUX_SHA}
          releaseDate: '${RELEASE_DATE}'
          EOF

          # Canonical macOS feed.
          # electron-updater on GitHub still consumes a single latest-mac.yml, so we
          # publish the Apple Silicon feed here and suppress Intel auto-update in-app
          # until we switch to universal packaging or an arch-aware provider.
          download_asset "Agent.Teams.AI-${VERSION}-arm64-mac.zip"
          download_asset "Agent.Teams.AI-${VERSION}-arm64.dmg"
          MAC_ZIP_SHA="$(sha512_base64 "Agent.Teams.AI-${VERSION}-arm64-mac.zip")"
          MAC_ZIP_SIZE="$(file_size "Agent.Teams.AI-${VERSION}-arm64-mac.zip")"
          MAC_DMG_SHA="$(sha512_base64 "Agent.Teams.AI-${VERSION}-arm64.dmg")"
          MAC_DMG_SIZE="$(file_size "Agent.Teams.AI-${VERSION}-arm64.dmg")"
          cat > latest-mac.yml <<EOF
          version: ${VERSION}
          files:
            - url: Agent.Teams.AI-${VERSION}-arm64-mac.zip
              sha512: ${MAC_ZIP_SHA}
              size: ${MAC_ZIP_SIZE}
            - url: Agent.Teams.AI-${VERSION}-arm64.dmg
              sha512: ${MAC_DMG_SHA}
              size: ${MAC_DMG_SIZE}
          path: Agent.Teams.AI-${VERSION}-arm64-mac.zip
          sha512: ${MAC_ZIP_SHA}
          releaseDate: '${RELEASE_DATE}'
          EOF

          gh release upload "${TAG}" latest.yml latest-linux.yml latest-mac.yml --repo "${REPO}" --clobber

      - name: Publish release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          TAG="${GITHUB_REF#refs/tags/}"
          gh release edit "${TAG}" --repo "${GITHUB_REPOSITORY}" --draft=false --latest
