Files
pangolin/docs/superpowers/plans/2026-07-05-cicd.md
wangjia 6be777dff5 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>
2026-07-05 23:40:59 +08:00

14 KiB
Raw Permalink Blame History

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:20golang: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.shscripts/ci/lib-forgejo.shscripts/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.31.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.shscripts/ci/deploy-site.sh.gitea/workflows/deploy-site.yml
  • 参照:~/code/jiu/scripts/ci/compile-site.shdeploy-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.shbash 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.shscripts/ci/release-server.shscripts/ci/deploy-server.sh.gitea/workflows/deploy-server.yml

Interfaces:

  • Consumes:_env.shlib-forgejo.sh;secret DEPLOY_SSH_KEYFORGEJO_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 手动次序,带回滚):

#!/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.shdeploy-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 servergolang:1.25 容器 cd server && go test ./...;test.sh clientflutter 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.gradlesigningConfigs.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.shscripts/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.propertiesflutter 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.tsdownloads: { 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 --waitstapler → 打 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 代码签名、上架商店。