三期 11 任务:Phase1 基座+官网+服务端(无签名) → Phase2 Android → Phase3 macOS/Windows。 服务端部署固化 F3/F4 备份/迁移/回滚;下载链接接 release 资产;密钥对齐 jiu。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 KiB
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 上每 jobdocker 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;secretDEPLOY_SSH_KEY。 - Produces:
pangolin.yanmeiai.com静态站上线。
前置(基础设施,需先做 / 确认——改机器前问用户):
-
pangolin1 上装 nginx(或 caddy),配
pangolin.yanmeiai.comvhost,web 根如/var/www/pangolin-site。 -
CF DNS:
pangolin.yanmeiai.comA/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;secretDEPLOY_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 手动次序,带回滚):
#!/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—— tagserver-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 版本前进、/healthz200、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—— tagclient-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.astroandroid 按钮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—— tagclient-v*+ dispatch;runs-on: mac;provision → compile-macos → release-client(上传 dmg)。 - Step 3:手动验证 —— dispatch → dmg 出现 → 另一台 mac
spctl -a -vv通过、stapler validateOK、装上能连。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—— tagclient-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-*成功后 dispatchdeploy-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 代码签名、上架商店。