diff --git a/docs/cicd-plan.html b/docs/cicd-plan.html new file mode 100644 index 0000000..c0a2531 --- /dev/null +++ b/docs/cicd-plan.html @@ -0,0 +1,173 @@ + + + + + +Pangolin CI/CD 实现计划(#30) + + + +
+← 文档索引 +

Pangolin CI/CD 实现计划

+

#30 · 2026-07-05 · 阅读版 · 执行真相源 docs/superpowers/plans/2026-07-05-cicd.md(带 checkbox)· 设计 cicd-design.html

+ +
+目标:tag 触发的「编译 → 测试 → 发版(Gitea release)→ 部署」全自动。镜像 jiu 的 +scripts/ci/*.sh(逻辑)+ .gitea/workflows/*.yml(编排)。runner:nas(官网+服务端,容器化 +node:20/golang:1.25)、mac(Android+macOS)、windows(Windows)。 +CI「测试」= workflow_dispatch 手动触发跑一遍 + 观察产物/部署(非经典单元 TDD)。 +
+ +

全局约束

+ + +
Phase 1 —— 基座 + 官网 + 服务端(无签名,可立即上线)
+ +
+

Task 1 · scripts/ci 基座

+
Create: scripts/ci/_env.sh · lib-forgejo.sh · notify.sh(抄 jiu 同名改 pangolin 专属值)
+产出:_env.sh(镜像源 + ver_from_tag${ref#refs/tags/prefix-v} 参数展开);lib-forgejo.shforgejo_release_ensure / forgejo_upload_asset,curl+API);notify.sh。 +验证:bash -n + shellcheck 0 告警 → commit。 +
+ +
+

Task 2 · checks 工作流(保留)

+
Modify: .gitea/workflows/ci.yml
+现有 ci.yml(nas,shellcheck/openapi/redline/flutter/go test)保留;把 scripts/ci/*.sh 纳入 shellcheck 扫描。push 观察全绿。 +
+ +
+

Task 3 · 官网发布

+
Create: scripts/ci/compile-site.sh · deploy-site.sh · .gitea/workflows/deploy-site.yml
+前置(基础设施,改机器前问用户):pangolin1 装 nginx/caddy 配 pangolin.yanmeiai.com vhost(web 根 /var/www/pangolin-site);CF DNS 指向 103.119.13.48(cf-api,记 baize);TLS 先 CF 橙云或并入 #25。 + +验证:dispatch → curl -I https://pangolin.yanmeiai.com/ 200。 +
+ +
+

Task 4 · 服务端发布(固化 F3/F4,带回滚)

+
Create: scripts/ci/compile-backend.sh · release-server.sh · deploy-server.sh · .gitea/workflows/deploy-server.yml
+ +
#!/usr/bin/env bash
+set -euo pipefail
+DB=/var/lib/pangolin/pangolin.db; BIN=/usr/local/bin; TAG="$1"
+scp server/out/pangolin-{server,agent,migrate} pangolin1:/tmp/
+ssh pangolin1 "bash -s" <<REMOTE
+set -euo pipefail
+systemctl stop pangolin-server
+runuser -u pangolin -- sqlite3 "$DB" 'PRAGMA wal_checkpoint(TRUNCATE);'
+cp -p "$DB" "$DB.bak-pre-$TAG"
+if ! runuser -u pangolin -- env DB_DRIVER=sqlite DB_DSN=$DB /tmp/pangolin-migrate up; then
+  echo "!! migrate 失败,回滚"; cp -p "$DB.bak-pre-$TAG" "$DB"; systemctl start pangolin-server; exit 1
+fi
+cp -p "$BIN/pangolin-server" "$BIN/pangolin-server.bak-$TAG" || true
+install -m755 /tmp/pangolin-server "$BIN/pangolin-server"
+install -m755 /tmp/pangolin-agent  "$BIN/pangolin-agent"
+install -m755 /tmp/pangolin-migrate "$BIN/pangolin-migrate"
+systemctl start pangolin-server; systemctl is-active pangolin-server
+REMOTE
+curl -fsS -m10 --retry 5 --retry-connrefused http://103.119.13.48:8080/healthz >/dev/null && echo healthz OK
+deploy-server.yml:tag server-v* + dispatch,nas,compile → test → release → deploy。 +验证:dispatch → migrate 版本前进 + /healthz 200 + 行数守恒。 +
+ +
+

Task 5 · scripts/ci/test.sh

+test.sh servergolang:1.25 go test ./...test.sh clientflutter test。接入 deploy-server 的 test 步骤。 +
+ +
Phase 2 —— Android(解锁官网下载链接)
+ +
+

Task 6 · Android gradle 接 release 签名

+
Modify: client/android/app/build.gradle · Create: keystore.properties(gitignore)
+加 signingConfigs.release,从 env/keystore.properties 读 keystore + 三密码(ANDROID_*);buildTypes.release.signingConfig 指向它。本机验证 apksigner verify --print-certs 显示 CN=Pangolin(非 debug)。 +
+ +
+

Task 7 · Android CI

+
Create: scripts/ci/compile-android.sh · release-client.sh · .gitea/workflows/build-android.yml
+compile-android.shbuild-libbox.sh android(JDK17/NDK≥28)→ secrets 落 keystore → flutter build apk --release --split-per-abi --dart-define=PANGOLIN_API_URL=…build-android.yml:tag client-v*runs-on: mac,provision → compile → release。验证:真机 adb install -r 可用。 +
+ +
+

Task 8 · 官网下载链接接 Android

+
Modify: web/website/src/config/site.ts(downloads.android)· Download.astro
+site.ts.downloads.android = Forgejo /releases/latest/download/<asset> 稳定 URL(不支持则构建期烘焙)。重部署官网,点击落到最新 apk。关 todo 30A(Android 部分)。 +
+ +
Phase 3 —— macOS + Windows
+ +
+

Task 9 · macOS 签名+公证 dmg

+
Create: scripts/ci/compile-macos.sh · .gitea/workflows/build-macos.yml(证书导入+notarytool 可复用 jiu compile-macos.sh)
+建临时 keychain → MACOS_DEVELOPER_ID_CERT_P12_BASE64 导入 → 两个描述文件解码装入 → 递增 CURRENT_PROJECT_VERSION → Xcode Developer ID 构建 app+sysext → notarytool submit --key-id $APPSTORE_API_KEY_ID --issuer $APPSTORE_API_ISSUER_ID --waitstapler → dmg。runs-on: mac。验证:另一台 mac spctl -a -vv + stapler validate 通过。 +
+ +
+

Task 10 · Windows 安装包

+
Create: scripts/ci/compile-windows.sh · .gitea/workflows/build-windows.yml(参照 jiu install-innosetup.ps1)
+flutter build windows --release → Inno Setup 打包(先不代码签名,首装有 SmartScreen 提示可接受)。tag client-v*/winbuild*runs-on: windows。验证:windows 机装上能连。 +
+ +
+

Task 11 · 下载链接全端闭环

+site.ts.downloads 补齐 macos/windows;三端按钮全接 release。客户端发版触发官网重部署刷新。关 todo 30A/30B。 +
+ +

不在本计划

+

iOS(G)、备份/容灾(#26)、TLS(#25)、Windows 代码签名、上架商店。

+
+ + diff --git a/docs/index.html b/docs/index.html index d63e5b9..e3264b9 100644 --- a/docs/index.html +++ b/docs/index.html @@ -86,6 +86,11 @@

实现计划 / Plans

+ +
CI/CD 全流程 实现计划(#30)HTML
+
阅读版;执行真相源 docs/superpowers/plans/2026-07-05-cicd.md(含 checkbox)。三期 11 任务:Phase1 基座+官网+服务端(无签名可立即上线,服务端固化 F3/F4 备份/迁移/回滚) → Phase2 Android(接 release keystore 签名,解锁下载链接) → Phase3 macOS 公证 dmg + Windows 安装包。runner 混合 nas/mac/windows;密钥已建(对齐 jiu)。设计见 cicd-design.html。
+
docs/cicd-plan.html · 真相源 docs/superpowers/plans/2026-07-05-cicd.md
+
设备 & 会话管理 + 每设备流量归因 实现计划(P1–P6)HTML
阅读版;执行真相源为 docs/superpowers/plans/2026-06-29-device-session-management.md(含 checkbox)。P1 设备注册打通 → P2 sessions表+在线/最后登录 → P3 强制退出/清除 → P4 每设备流量 → P5 2FA信任(future) → P6 UI重做。
diff --git a/docs/superpowers/plans/2026-07-05-cicd.md b/docs/superpowers/plans/2026-07-05-cicd.md new file mode 100644 index 0000000..c0dd834 --- /dev/null +++ b/docs/superpowers/plans/2026-07-05-cicd.md @@ -0,0 +1,193 @@ +# Pangolin CI/CD 全流程 Implementation Plan(#30) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** tag 触发的「编译 → 测试 → 发版(Gitea release)→ 部署」全自动流水线,覆盖官网 + 服务端 + Android/macOS/Windows 客户端。 + +**Architecture:** 镜像 jiu 的 `scripts/ci/*.sh`(逻辑)+ `.gitea/workflows/*.yml`(编排)。逻辑放脚本便于本地复现,工作流只调脚本。runner 混合:nas(官网+服务端,容器化 `node:20`/`golang:1.25`)、mac(Android+macOS)、windows(Windows)。 + +**Tech Stack:** Gitea Actions(act_runner,host-mode)、Bash、Astro/Node、Go 交叉编译、Flutter、gomobile libbox、Xcode notarytool、Inno Setup、Forgejo release API。 + +## Global Constraints + +- 参考真相源:`docs/superpowers/specs/2026-07-05-cicd-design.md`;jiu 的 `~/code/jiu/.gitea/workflows/*` + `~/code/jiu/scripts/ci/*`(proven,copy+adapt)。 +- runner label:`nas` / `mac` / `windows`;nas 上每 job `docker run` 官方镜像(`node:20`、`golang:1.25`),不装宿主工具链。 +- 国内镜像:`GOPROXY=https://goproxy.cn,direct`;`PUB_HOSTED_URL=https://pub.flutter-io.cn`;`FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn`。 +- Secrets(已建,命名对齐 jiu):账户级 `FORGEJO_TOKEN`/`FORGEJO_URL`/`MACOS_DEVELOPER_ID_CERT_P12_BASE64`/`MACOS_DEVELOPER_ID_CERT_PASSWORD`/`APPSTORE_API_KEY_P8_BASE64`/`APPSTORE_API_KEY_ID`/`APPSTORE_API_ISSUER_ID`;pangolin 仓库级 `DEPLOY_SSH_KEY`/`ANDROID_KEYSTORE_BASE64`/`ANDROID_KEYSTORE_PASSWORD`/`ANDROID_KEY_ALIAS`/`ANDROID_KEY_PASSWORD`/`MACOS_APP_PROVISION_PROFILE_BASE64`/`MACOS_SYSEXT_PROVISION_PROFILE_BASE64`。 +- 客户端铁律:macOS 每次构建递增 `CURRENT_PROJECT_VERSION`;Android NDK≥28、gomobile JDK17、libbox 包名 `io.nekohasekai.libbox`。 +- Bash:禁 `$()` 命令替换(拆分/管道);提交 footer 带 `Co-Authored-By: Claude Opus 4.8 `。 +- **CI 脚本的「测试」= `workflow_dispatch` 手动触发跑一遍 + 观察产物/部署结果**(非经典单元 TDD);每条流水线先手动 dispatch 跑通再依赖 tag。 +- 部署机 pangolin1 别名 `pangolin1`(103.119.13.48);官网域名 `pangolin.yanmeiai.com`。 + +--- + +# Phase 1 —— 基座 + 官网 + 服务端(无签名,可立即上线) + +### Task 1: `scripts/ci/` 基座(env + forgejo 库 + 通知) + +**Files:** +- Create: `scripts/ci/_env.sh`、`scripts/ci/lib-forgejo.sh`、`scripts/ci/notify.sh` +- 参照:`~/code/jiu/scripts/ci/_env.sh`、`~/code/jiu/scripts/ci/lib-forgejo.sh`、`~/code/jiu/scripts/ci/notify.sh` + +**Interfaces:** +- Produces:`_env.sh` 导出 `GOPROXY`/`PUB_HOSTED_URL`/`FLUTTER_STORAGE_BASE_URL` + `ver_from_tag `(解析 `server-v1.2.3` → `1.2.3`);`lib-forgejo.sh` 提供 `forgejo_release_ensure `、`forgejo_upload_asset <tag> <file>`(用 `FORGEJO_TOKEN`/`FORGEJO_URL`,curl+API);`notify.sh` 提供 `notify_ok`/`notify_fail`。 + +- [ ] Step 1:抄 jiu 三个脚本到 `scripts/ci/`,把 ali/jiu 专属值替换为 pangolin(仓库名、域名);`ver_from_tag` 用 `${ref#refs/tags/${prefix}-v}` 参数展开(不 `$()`)。 +- [ ] Step 2:`chmod +x scripts/ci/*.sh`;本地 `bash -n` 语法检查每个脚本。Expected:无输出(语法 OK)。 +- [ ] Step 3:`shellcheck scripts/ci/*.sh`。Expected:0 告警(或仅可接受的 info)。 +- [ ] Step 4:Commit `feat(ci): scripts/ci 基座(_env/lib-forgejo/notify)`。 + +### Task 2: checks 工作流(保留现有) + +**Files:** Modify(可选 rename): `.gitea/workflows/ci.yml` + +- [ ] Step 1:确认现有 `ci.yml`(nas,shellcheck/openapi/redline/flutter analyze+test/go test)仍覆盖需求;把新增的 `scripts/ci/*.sh` 纳入 shellcheck job 的扫描路径。 +- [ ] Step 2:push 一个无关小改到分支,观察 checks 全绿。Expected:所有 job pass。 +- [ ] Step 3:Commit(若有改动)`ci(checks): shellcheck 覆盖 scripts/ci`。 + +### Task 3: 官网发布(compile + deploy + workflow) + +**Files:** +- Create: `scripts/ci/compile-site.sh`、`scripts/ci/deploy-site.sh`、`.gitea/workflows/deploy-site.yml` +- 参照:`~/code/jiu/scripts/ci/compile-site.sh`、`deploy-site.sh`、`.gitea/workflows/deploy-site.yml` + +**Interfaces:** +- Consumes:`_env.sh`;secret `DEPLOY_SSH_KEY`。 +- Produces:`pangolin.yanmeiai.com` 静态站上线。 + +**前置(基础设施,需先做 / 确认——改机器前问用户):** +- pangolin1 上装 nginx(或 caddy),配 `pangolin.yanmeiai.com` vhost,web 根如 `/var/www/pangolin-site`。 +- CF DNS:`pangolin.yanmeiai.com` A/CNAME → 103.119.13.48(用 `cf-api`,记 baize)。 +- TLS:先 HTTP 起,证书并入 #25 或用 CF proxy 橙云。 + +- [ ] Step 1:写 `compile-site.sh` —— 在 `node:20` 容器内 `cd web/website && npm ci && SITE_URL=https://pangolin.yanmeiai.com npm run build`,产物 `web/website/dist/`。 +- [ ] Step 2:写 `deploy-site.sh` —— 用 `DEPLOY_SSH_KEY` 起 ssh agent,`rsync -az --delete web/website/dist/ pangolin1:/var/www/pangolin-site/`;远端 `nginx -s reload` 非必需(静态文件即时生效)。 +- [ ] Step 3:写 `deploy-site.yml` —— `on.push.tags: ['site-v[0-9]*.[0-9]*.[0-9]*']` + `workflow_dispatch`;`runs-on: nas`;并发组 `deploy-site`;steps: checkout → `docker run --rm -v $PWD:/w -w /w node:20 bash scripts/ci/compile-site.sh` → `bash scripts/ci/deploy-site.sh`。 +- [ ] Step 4:**手动验证** —— `workflow_dispatch` 触发 deploy-site;`curl -I https://pangolin.yanmeiai.com/` 返回 200,页面 canonical 正确。Expected:站点可访问。 +- [ ] Step 5:Commit `feat(ci): 官网 site-v* 构建+部署到 pangolin.yanmeiai.com`。 + +### Task 4: 服务端发布(compile + release + deploy + workflow) + +**Files:** +- Create: `scripts/ci/compile-backend.sh`、`scripts/ci/release-server.sh`、`scripts/ci/deploy-server.sh`、`.gitea/workflows/deploy-server.yml` + +**Interfaces:** +- Consumes:`_env.sh`、`lib-forgejo.sh`;secret `DEPLOY_SSH_KEY`、`FORGEJO_TOKEN`/`FORGEJO_URL`。 +- Produces:pangolin1 上 `pangolin-server`/`pangolin-agent`/`pangolin-migrate` 更新到 tag 版本,migrate 已应用。 + +- [ ] Step 1:写 `compile-backend.sh` —— `golang:1.25` 容器内 `cd server && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o out/pangolin-server ./cmd/server`(同样出 agent、migrate);产物 `server/out/`。 +- [ ] Step 2:写 `release-server.sh` —— `forgejo_release_ensure "$TAG" "server $TAG"` + 逐个 `forgejo_upload_asset`。 +- [ ] Step 3:写 `deploy-server.sh`(固化 F3/F4 手动次序,**带回滚**): +```bash +#!/usr/bin/env bash +set -euo pipefail +DB=/var/lib/pangolin/pangolin.db; BIN=/usr/local/bin; TAG="$1" +scp server/out/pangolin-server server/out/pangolin-agent server/out/pangolin-migrate pangolin1:/tmp/ +ssh pangolin1 "bash -s" <<REMOTE +set -euo pipefail +systemctl stop pangolin-server +runuser -u pangolin -- sqlite3 "$DB" 'PRAGMA wal_checkpoint(TRUNCATE);' +cp -p "$DB" "$DB.bak-pre-$TAG" +if ! runuser -u pangolin -- env DB_DRIVER=sqlite DB_DSN=$DB /tmp/pangolin-migrate up; then + echo "!! migrate 失败,回滚"; cp -p "$DB.bak-pre-$TAG" "$DB"; systemctl start pangolin-server; exit 1 +fi +cp -p "$BIN/pangolin-server" "$BIN/pangolin-server.bak-$TAG" || true +install -m755 /tmp/pangolin-server "$BIN/pangolin-server" +install -m755 /tmp/pangolin-agent "$BIN/pangolin-agent" +install -m755 /tmp/pangolin-migrate "$BIN/pangolin-migrate" +systemctl start pangolin-server +systemctl is-active pangolin-server +REMOTE +curl -fsS -m 10 --retry 5 --retry-connrefused http://103.119.13.48:8080/healthz >/dev/null && echo "healthz OK" +``` +- [ ] Step 4:写 `deploy-server.yml` —— tag `server-v*` + dispatch;`runs-on: nas`;并发组 `deploy-server`;steps: checkout → compile(golang 容器)→ `test.sh server`(见 Task 5)→ `release-server.sh` → `deploy-server.sh $VER`。 +- [ ] Step 5:**手动验证** —— 打 tag `server-v0.0.1-ci`(或 dispatch)→ 观察:migrate 版本前进、`/healthz` 200、`sqlite3 nodes` 行数守恒(同 F3 核对法)。Expected:部署成功、无数据丢失。 +- [ ] Step 6:Commit `feat(ci): 服务端 server-v* 编译+release+部署(备份/迁移/回滚)`。 + +### Task 5: `scripts/ci/test.sh` + +**Files:** Create `scripts/ci/test.sh` + +- [ ] Step 1:`test.sh server` → `golang:1.25` 容器 `cd server && go test ./...`;`test.sh client` → `flutter test`(容器或 nas flutter)。 +- [ ] Step 2:接入 deploy-server.yml 的 test 步骤;dispatch 跑通。Expected:go test 全绿才继续部署。 +- [ ] Step 3:Commit `ci: test.sh(go test / flutter test)`。 + +--- + +# Phase 2 —— D Android(解锁官网下载链接) + +### Task 6: Android gradle 接 release 签名 + +**Files:** Modify `client/android/app/build.gradle(.kts)`、Create `client/android/keystore.properties`(gitignore,占位) + +**Interfaces:** Consumes secrets `ANDROID_KEYSTORE_BASE64`/`ANDROID_KEYSTORE_PASSWORD`/`ANDROID_KEY_ALIAS`/`ANDROID_KEY_PASSWORD`。 + +- [ ] Step 1:`build.gradle` 加 `signingConfigs.release`,从环境变量/`keystore.properties` 读 keystore 路径与三密码;`buildTypes.release.signingConfig = signingConfigs.release`。参照 jiu 的 android 签名接法。 +- [ ] Step 2:本机用真 keystore(你已建)`flutter build apk --release` 验证签名生效:`apksigner verify --print-certs build/app/outputs/flutter-apk/app-release.apk` 显示 CN=Pangolin。Expected:release 签名(非 debug)。 +- [ ] Step 3:Commit `build(android): release keystore 签名接线`。 + +### Task 7: Android CI(compile + release + workflow) + +**Files:** Create `scripts/ci/compile-android.sh`、`scripts/ci/release-client.sh`、`.gitea/workflows/build-android.yml`;参照 jiu `compile-android.sh`/`release-client.sh`/`deploy-client.yml`。 + +- [ ] Step 1:`compile-android.sh` —— `bash scripts/build-libbox.sh android`(JDK17/NDK≥28)→ 把 secrets 落成 keystore 文件 + `keystore.properties` → `flutter build apk --release --split-per-abi --dart-define=PANGOLIN_API_URL=...` → 产物 `app-arm64-v8a-release.apk`。 +- [ ] Step 2:`release-client.sh` —— `forgejo_release_ensure client-$VER` + 上传该端资产(多端共用一个 `client-v*` release,各自 upload)。 +- [ ] Step 3:`build-android.yml` —— tag `client-v*` + dispatch;`runs-on: mac`;steps:provision-mac → compile-android → release-client。 +- [ ] Step 4:**手动验证** —— dispatch → release 出现 arm64 apk → 真机 `adb install -r` 成功、能连。Expected:签名 apk 可装可用。 +- [ ] Step 5:Commit `feat(ci): Android client-v* 构建+release`。 + +### Task 8: 官网下载链接接 Android release + +**Files:** Modify `web/website/src/config/site.ts`(加 `downloads.android`)、`web/website/src/components/Download.astro`。 + +- [ ] Step 1:`site.ts` 加 `downloads: { android: '<Forgejo latest-download 稳定 URL 或构建期烘焙>' }`;`Download.astro` android 按钮 `href={SITE.downloads.android}`。先探 Forgejo 是否支持 `/releases/latest/download/<asset>`;不支持则 `deploy-site.sh` 构建期用 `FORGEJO_TOKEN` 查最新 `client-v*` 版本注入。 +- [ ] Step 2:重部署官网,点 Android 下载按钮落到最新 apk。Expected:下载可用。 +- [ ] Step 3:Commit `feat(website): Android 下载按钮接 release 资产`;更新 todo 子任务 30A(Android 部分)。 + +--- + +# Phase 3 —— E macOS + F Windows + +### Task 9: macOS CI(签名+公证 dmg) + +**Files:** Create `scripts/ci/compile-macos.sh`、`.gitea/workflows/build-macos.yml`;参照 jiu `compile-macos.sh`(证书导入 + notarytool 部分可几乎直接复用)。 + +**Interfaces:** Consumes 账户级 Apple secrets + 仓库级 `MACOS_APP_PROVISION_PROFILE_BASE64`/`MACOS_SYSEXT_PROVISION_PROFILE_BASE64`。 + +- [ ] Step 1:`compile-macos.sh` —— 建临时 keychain,`MACOS_DEVELOPER_ID_CERT_P12_BASE64` 解码导入(复用 jiu)→ 两个 provisioning profile 解码装入 `~/Library/MobileDevice/Provisioning Profiles/` → 递增 `CURRENT_PROJECT_VERSION` → Xcode Developer ID 构建 app+sysext(`scripts/local_test.sh build` 逻辑)→ `notarytool submit --key <p8> --key-id $APPSTORE_API_KEY_ID --issuer $APPSTORE_API_ISSUER_ID --wait` → `stapler` → 打 dmg。 +- [ ] Step 2:`build-macos.yml` —— tag `client-v*` + dispatch;`runs-on: mac`;provision → compile-macos → release-client(上传 dmg)。 +- [ ] Step 3:**手动验证** —— dispatch → dmg 出现 → 另一台 mac `spctl -a -vv` 通过、`stapler validate` OK、装上能连。Expected:公证 dmg 可分发。 +- [ ] Step 4:Commit `feat(ci): macOS client-v* 签名+公证 dmg`;下载链接接 macOS。 + +### Task 10: Windows CI(exe/installer) + +**Files:** Create `scripts/ci/compile-windows.sh`(或 .ps1)、`.gitea/workflows/build-windows.yml`;参照 jiu `compile-windows.sh`/`install-innosetup.ps1`/`build-windows.yml`。 + +- [ ] Step 1:`compile-windows.sh` —— `flutter build windows --release --dart-define=PANGOLIN_API_URL=...` → Inno Setup 打安装包(`install-innosetup.ps1` 装 ISCC)。**先不做代码签名**(用户首装 SmartScreen 提示,可接受)。 +- [ ] Step 2:`build-windows.yml` —— tag `client-v*` / `winbuild*` + dispatch;`runs-on: windows`;compile → release-client(上传 exe)。参考记忆:Windows 出包复制到桌面 pangolin 目录。 +- [ ] Step 3:**手动验证** —— dispatch → installer 出现 → windows 机装上能连。Expected:安装包可用。 +- [ ] Step 4:Commit `feat(ci): Windows client-v* 安装包`;下载链接接 Windows。 + +### Task 11: 下载链接全端闭环 + 文档 + +**Files:** Modify `web/website/src/config/site.ts`(downloads.macos/windows)、`docs/index.html`(若有)。 + +- [ ] Step 1:`site.ts.downloads` 补齐 macos/windows;Download.astro 三端按钮全部接 release 资产。 +- [ ] Step 2:客户端发版触发官网重部署(`build-*` 成功后 dispatch `deploy-site`,或用 latest-download URL 免重建)。 +- [ ] Step 3:**验证** —— 官网三端下载按钮均落到最新 release。Commit;关掉 todo 子任务 30A/30B。 + +--- + +## 验证(整体) + +- 每条流水线先 `workflow_dispatch` 跑通再依赖 tag。 +- 服务端:migrate 版本 + `/healthz` + 行数守恒。官网:站点可访问 + canonical + redline。客户端:各端资产可装可连。 +- 下载链接:官网按钮落到最新 release 资产。 + +## 风险(见 spec §9) + +nas 内存(容器化单 job 缓解)、migrate 生产出错(备份+回滚)、keystore/公证凭据(Bitwarden+不落盘)、下载链接刷新(build-* 触发 deploy-site 或 latest-download)。 + +## 不在本计划 + +iOS(G)、备份/容灾(#26)、TLS(#25,官网 TLS 先 CF 橙云或并入 #25)、Windows 代码签名、上架商店。