From b35501e6adb3e0f00a06557a72c91b3898d1f09c Mon Sep 17 00:00:00 2001 From: zhayujie Date: Thu, 16 Jul 2026 15:08:15 +0800 Subject: [PATCH] feat(desktop): add win7 workflow --- .github/workflows/publish-desktop.yml | 154 -------------------------- .github/workflows/release-win7.yml | 111 +++++++++++++++++++ 2 files changed, 111 insertions(+), 154 deletions(-) delete mode 100644 .github/workflows/publish-desktop.yml create mode 100644 .github/workflows/release-win7.yml diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml deleted file mode 100644 index 6f2270bb..00000000 --- a/.github/workflows/publish-desktop.yml +++ /dev/null @@ -1,154 +0,0 @@ -name: Publish Desktop - -# STAGE 3 of the decoupled release pipeline: PROMOTE a built + notarized version -# to "live". By this point: -# - stage 1 (Release Desktop) built the installers, mirrored them to R2, and -# registered them in D1 as unpublished (is_latest=0); -# - stage 2 (local desktop/build/notarize-dmg.sh) notarized + stapled the mac -# dmgs and re-uploaded the stapled bytes to R2. -# -# This workflow, triggered manually with the version to publish: -# 1. pulls every artifact for that version back from R2, -# 2. recomputes sha512 from the real (stapled) bytes and updates D1, -# 3. flips is_latest=1 for that version (clearing the previous latest per -# platform) UNLESS it's a pre-release, which is recorded but never latest, -# 4. creates/updates the GitHub Release and attaches the installers. -# -# Run only after the mac dmgs for this version are notarized + re-uploaded. -on: - workflow_dispatch: - inputs: - version: - description: "Version to publish (e.g. 1.2.0). Must already be built + notarized." - type: string - required: true - make_latest: - description: "Mark this version as latest on the site (uncheck for a dry re-hash only)." - type: boolean - default: true - github_release: - description: "Create/update the GitHub Release and attach installers." - type: boolean - default: true - -permissions: - contents: write - -jobs: - publish: - name: Publish ${{ github.event.inputs.version }} - 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 [ -z "$CF_TOKEN" ]; then - echo "::error::CLOUDFLARE_API_TOKEN not set — cannot publish." - exit 1 - fi - - - name: Resolve version artifacts from D1 - id: rows - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - VER: ${{ github.event.inputs.version }} - run: | - # The build stage inserted one row per artifact with filename = v/. - # Read them back so we know exactly which objects to pull from R2. - out="$(npx --yes wrangler@latest d1 execute cow-desktop --remote --json \ - --command "SELECT platform, filename, update_filename FROM releases WHERE version = '${VER}';")" - echo "$out" - echo "$out" | node -e ' - const fs = require("fs"); - const data = JSON.parse(fs.readFileSync(0, "utf8")); - const rows = (Array.isArray(data) ? data : [data]) - .flatMap(r => (r.results || [])); - if (!rows.length) { - console.error("No D1 rows for this version — did stage 1 (build) run?"); - process.exit(1); - } - fs.writeFileSync(process.env.GITHUB_OUTPUT, "count=" + rows.length + "\n", { flag: "a" }); - fs.writeFileSync("rows.json", JSON.stringify(rows)); - ' - - - name: Download version artifacts from R2 - 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' }} - run: | - mkdir -p dist - # Pull BOTH the manual-download file (filename: dmg/exe) and the mac - # auto-update file (update_filename: zip) for every row. Keys are - # "v/"; the R2 key is "desktop/". - for key in $(node -e 'JSON.parse(require("fs").readFileSync("rows.json")).forEach(r => { if (r.filename) console.log(r.filename); if (r.update_filename) console.log(r.update_filename); })'); do - base="$(basename "$key")" - r2key="desktop/${key}" - echo "==> Downloading r2://${R2_BUCKET}/${r2key} -> dist/${base}" - npx --yes wrangler@latest r2 object get "${R2_BUCKET}/${r2key}" \ - --file "dist/${base}" --remote - done - echo "Downloaded:"; ls -la dist - - - name: Reminder — mac dmgs must be notarized before publishing - run: | - # Stapling can only be validated on macOS (xcrun stapler validate), - # which this Linux runner doesn't have. The authoritative check runs in - # stage 2 (desktop/build/notarize-dmg.sh) before re-uploading to R2. - # This step is just a loud reminder in the log. - echo "::notice::Publishing assumes the mac dmgs pulled from R2 are already notarized + stapled (stage 2). If you skipped stage 2, users will hit Gatekeeper warnings." - ls -la dist/*.dmg 2>/dev/null || echo "(no dmg in this version — win-only publish)" - - - name: Update D1 (recompute sha512 + set latest) - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - VER: ${{ github.event.inputs.version }} - MAKE_LATEST: ${{ github.event.inputs.make_latest }} - run: | - # Pre-releases (e.g. 1.2.0-beta / -rc.1 / -test) are recorded but never - # become latest, so the site keeps serving the last stable build. - case "$VER" in - *-*) is_pre=1 ;; - *) is_pre=0 ;; - esac - if [ "$MAKE_LATEST" = "true" ] && [ "$is_pre" = "0" ]; then - latest_flag="--latest"; echo "==> Publishing $VER as latest." - else - latest_flag=""; echo "==> Publishing $VER without latest flag (pre-release or dry re-hash)." - fi - - # Re-hash the real (stapled) bytes and re-store every row with both the - # dmg (manual) and mac zip (auto-update) columns. Same script as the - # build stage; --latest also clears the previous latest per platform. - node .github/scripts/register-releases.mjs --dir dist --version "$VER" --sql d1.sql $latest_flag - echo "==> D1 statements:"; cat d1.sql - npx --yes wrangler@latest d1 execute cow-desktop --remote --file d1.sql - - - name: Create/update GitHub Release and attach installers - if: github.event.inputs.github_release == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VER: ${{ github.event.inputs.version }} - run: | - tag="v${VER}" - case "$VER" in - *-*) prerelease="--prerelease" ;; - *) prerelease="" ;; - esac - if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then - gh release create "$tag" --repo "$GITHUB_REPOSITORY" \ - --title "$tag" --generate-notes $prerelease - fi - # --clobber so re-runs overwrite the stapled/updated assets. The mac - # zip is the auto-update artifact; attach it too so the GitHub Release - # is a complete mirror (nullglob avoids errors when a type is absent). - shopt -s nullglob - gh release upload "$tag" dist/*.dmg dist/*.zip dist/*.exe \ - --repo "$GITHUB_REPOSITORY" --clobber diff --git a/.github/workflows/release-win7.yml b/.github/workflows/release-win7.yml new file mode 100644 index 00000000..a5d56771 --- /dev/null +++ b/.github/workflows/release-win7.yml @@ -0,0 +1,111 @@ +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 touches the +# production R2/D1 publish flow or the auto-update feed. It just produces an +# unsigned NSIS installer and uploads it as a build artifact for manual +# download + testing. Delete this file whenever Win7 support is no longer worth +# maintaining — the main pipeline is completely 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 (e.g. 2.1.3-win7). Used for package.json and artifact name." + type: string + default: "0.0.0-win7" + +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 + # Deps are mostly unpinned, so pip will resolve the newest versions + # that still ship a Python 3.8 wheel. This is exactly what we want for + # a Win7-targeted backend. + pip install -r desktop/build/requirements-desktop.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 + + # Unsigned NSIS x64 build using the base config in package.json (no win + # sign hook, no R2/D1, no auto-update publish). --publish never still emits + # the installer. Invoke via node (not npx) to avoid the Windows npx.cmd + # batch-wrapper returning before the process finishes (see release.yml). + - name: Build installer (electron-builder, Electron 22) + working-directory: desktop + shell: bash + run: node node_modules/electron-builder/cli.js --win --x64 --publish never + + - 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