docs(plan): CI/CD 实现计划(#30) —— .md 真相源 + HTML 阅读版 + 索引
三期 11 任务:Phase1 基座+官网+服务端(无签名) → Phase2 Android → Phase3 macOS/Windows。 服务端部署固化 F3/F4 备份/迁移/回滚;下载链接接 release 资产;密钥对齐 jiu。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <noreply@anthropic.com>`。
|
||||
- **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 <prefix> <ref>`(解析 `server-v1.2.3` → `1.2.3`);`lib-forgejo.sh` 提供 `forgejo_release_ensure <tag> <title>`、`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 代码签名、上架商店。
|
||||
Reference in New Issue
Block a user