name: Release Desktop (Win7 legacy)

# One-off / on-demand build for legacy Windows 7 / 8 / 8.1 users.
#
# The main release pipeline (release.yml) ships Electron 33 (Chromium 130+) and
# a PyInstaller backend built with Python 3.11 — NEITHER runs on Windows 7,
# which is why those users hit "不是有效的 Win32 应用程序" when launching the exe.
#
# To support Win7 we must pin BOTH halves to the last versions that still
# target it:
#   - Electron 22.3.27  (Chromium 108, last major to support Win7/8/8.1)
#   - Python  3.8        (last CPython to support Win7)
#
# This is a SEPARATE, manually-triggered workflow so it never disturbs the main
# matrix. It produces a (signed, when SIGNTOOL_* secrets exist) NSIS installer,
# then — exactly like the main pipeline — uploads it to R2 and registers a
# release row in D1 as platform=win-legacy with is_latest=0 (UNPUBLISHED: it
# stays invisible until promoted, so it can't accidentally get served to Win10
# users). Because it's stamped with the SAME version as the standard release,
# the download page shows both Windows builds under one version row, and the
# /update feed serves each build to its own clients. Delete this file whenever
# legacy Windows support is no longer worth maintaining — the main pipeline is
# unaffected.
#
# IMPORTANT for end users: Win7 must have SP1 + update KB2533623 (or the rollup
# KB4457144) installed, otherwise the Python 3.8 backend still fails to start.

on:
  workflow_dispatch:
    inputs:
      version:
        description: "Version to stamp — MUST match the standard release (e.g. 2.1.3), so the download page merges both Windows builds into one version row."
        type: string
        default: "0.0.0"

permissions:
  contents: read

jobs:
  build:
    name: Build Windows x64 (Win7 legacy)
    runs-on: windows-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      # Python 3.8 is the last CPython that supports Windows 7. A backend built
      # with it (via PyInstaller) still runs on Win7 even though the CI host is
      # Server 2022 — PyInstaller's bootloader targets the interpreter's minimum
      # OS, not the build machine's.
      - name: Set up Python 3.8
        uses: actions/setup-python@v5
        with:
          python-version: "3.8"

      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Build Python backend (PyInstaller, Python 3.8)
        shell: bash
        run: |
          python -m pip install --upgrade pip
          # Most deps are unpinned, so pip auto-picks the newest Python-3.8 wheel.
          # But a few are pinned to versions with NO 3.8 wheel and must be relaxed
          # for this legacy build. We rewrite them into a throwaway requirements
          # file so the repo's source stays untouched (main pipeline keeps its
          # pins). playwright 1.48.0 is the last release with a cp38 wheel.
          sed 's/^playwright==.*/playwright==1.48.0/' \
            desktop/build/requirements-desktop.txt > /tmp/requirements-win7.txt
          pip install -r /tmp/requirements-win7.txt
          pip install pyinstaller
          # Run from repo root so the spec's relative datas resolve correctly.
          pyinstaller desktop/build/cowagent-backend.spec \
            --noconfirm \
            --distpath desktop/build/dist \
            --workpath desktop/build/build-work

      - name: Install desktop deps
        working-directory: desktop
        run: npm ci

      - name: Write version into package.json
        working-directory: desktop
        shell: bash
        run: npm version "${{ github.event.inputs.version }}" --no-git-tag-version --allow-same-version

      # Downgrade Electron to the last Win7-capable major (22). --no-save keeps
      # this out of package.json so the repo's committed deps stay on Electron 33
      # for the main pipeline. electron-builder reads the installed Electron
      # version from node_modules, so this is all that's needed to package v22.
      - name: Pin Electron to 22 (last Win7-capable)
        working-directory: desktop
        run: npm install --no-save electron@22.3.27

      - name: Compile (vite + tsc)
        working-directory: desktop
        shell: bash
        run: npm run build

      # Same signing setup as the main pipeline: download the signtool CLI (URL
      # from a repo variable so nothing is hardcoded in a public workflow). Only
      # runs when a URL is configured; otherwise the build stays unsigned but
      # still succeeds. SIGNTOOL_PATH is consumed by electron-builder.win.js.
      - name: Download Windows signing CLI
        if: vars.SIGNTOOL_CLI_URL != ''
        shell: bash
        env:
          SIGNTOOL_CLI_URL: ${{ vars.SIGNTOOL_CLI_URL }}
        run: |
          mkdir -p "$RUNNER_TEMP/signtool"
          curl -fsSL "$SIGNTOOL_CLI_URL" -o "$RUNNER_TEMP/signtool/cli.zip"
          unzip -o "$RUNNER_TEMP/signtool/cli.zip" -d "$RUNNER_TEMP/signtool" >/dev/null
          exe="$(find "$RUNNER_TEMP/signtool" -type f -iname 'signtool*.exe' | head -n1)"
          if [ -z "$exe" ]; then
            echo "signtool.exe not found in downloaded archive" >&2
            find "$RUNNER_TEMP/signtool" -type f >&2
            exit 1
          fi
          echo "SIGNTOOL_PATH=$(cygpath -w "$exe")" >> "$GITHUB_ENV"
          echo "resolved signtool: $exe"

      # NSIS x64 build. --config electron-builder.win.js wires the SAME signing
      # hook the main pipeline uses (signs app + backend + installer via the
      # signtool CLI). When SIGNTOOL_* aren't set the hook just skips and the
      # installer is still produced (unsigned). --publish never emits the exe
      # without touching any feed. Invoke via node (not npx) to avoid the
      # Windows npx.cmd wrapper returning early (see release.yml).
      #
      # -c.win.artifactName injects a "win7" segment into the file name
      # (CowAgent-Setup-<ver>-win7-x64.exe). That's exactly how the two mac
      # builds differ by ${arch}: it keeps the legacy exe from colliding with
      # the standard win exe in the same v<version>/ folder, and lets
      # register-releases.mjs map it to the win-legacy platform by name.
      - name: Build installer (electron-builder, Electron 22)
        working-directory: desktop
        shell: bash
        env:
          SIGNTOOL_ACCESS_KEY: ${{ secrets.SIGNTOOL_ACCESS_KEY }}
          SIGNTOOL_ACCESS_SECRET: ${{ secrets.SIGNTOOL_ACCESS_SECRET }}
          SIGNTOOL_CERT_CODE: ${{ secrets.SIGNTOOL_CERT_CODE }}
          COW_SIGN_DRY_RUN: ${{ vars.COW_SIGN_DRY_RUN }}
        run: |
          node node_modules/electron-builder/cli.js --win --x64 \
            --config electron-builder.win.js \
            -c.win.artifactName='${productName}-Setup-${version}-win7-${arch}.${ext}' \
            --publish never

      # Collect the installer + its blockmap (differential updates). The .yml
      # feed is NOT uploaded: the /update Function generates it dynamically from
      # D1 (same as the main pipeline), so it isn't needed here.
      - name: Upload installer artifact
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: cowagent-win7-x64
          path: |
            desktop/release/*.exe
            desktop/release/*.blockmap
          if-no-files-found: warn
          retention-days: 7

  # Publish to R2 + D1, exactly like the main pipeline's publish job: a SEPARATE
  # ubuntu-latest job (NO setup-node) so it uses the runner's Node 22+ and
  # wrangler@latest works (the build job pins Node 20 for Electron 22).
  #
  # The legacy exe lands in the SAME desktop/v<version>/ folder as the standard
  # build — its "win7" name segment keeps them distinct — and register-releases
  # writes a win-legacy row (is_latest=0, unpublished; promote it later via the
  # publish workflow). Because the version matches the standard release, the
  # download page merges both Windows builds into one version row.
  publish:
    name: Publish to R2 + D1
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Guard on Cloudflare secrets
        id: guard
        env:
          CF_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
        run: |
          if [ -n "$CF_TOKEN" ]; then
            echo "enabled=true" >> "$GITHUB_OUTPUT"
          else
            echo "enabled=false" >> "$GITHUB_OUTPUT"
            echo "::notice::CLOUDFLARE_API_TOKEN not set — skipping R2/D1 publish (use the artifact instead)."
          fi

      - name: Download build artifact
        if: steps.guard.outputs.enabled == 'true'
        uses: actions/download-artifact@v4
        with:
          name: cowagent-win7-x64
          path: dist

      - name: Upload installer to R2
        if: steps.guard.outputs.enabled == 'true'
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          R2_BUCKET: ${{ vars.R2_BUCKET != '' && vars.R2_BUCKET || 'cow-skills' }}
          VER: ${{ github.event.inputs.version }}
        run: |
          shopt -s nullglob
          for f in dist/*.exe dist/*.blockmap; do
            base="$(basename "$f")"
            key="desktop/v${VER}/${base}"
            echo "==> Uploading $base -> r2://${R2_BUCKET}/${key}"
            npx --yes wrangler@latest r2 object put "${R2_BUCKET}/${key}" \
              --file "$f" --remote
            echo "==> Download URL: https://cdn.cowagent.ai/${key}"
          done

      # Register the win-legacy row in D1 (is_latest=0). register-releases.mjs
      # maps the win7-named exe to platform=win-legacy; filename is v<ver>/<exe>
      # relative to R2_PUBLIC_BASE (=.../desktop), matching the upload key.
      - name: Register release row in D1
        if: steps.guard.outputs.enabled == 'true'
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          VER: ${{ github.event.inputs.version }}
        run: |
          node .github/scripts/register-releases.mjs --dir dist --version "$VER" --sql d1.sql
          echo "==> D1 statements:"; cat d1.sql
          npx --yes wrangler@latest d1 execute cow-desktop --remote --file d1.sql
