#30 · 2026-07-05 · 阅读版 · 执行真相源 docs/superpowers/plans/2026-07-05-cicd.md(带 checkbox)· 设计 cicd-design.html
scripts/ci/*.sh(逻辑)+ .gitea/workflows/*.yml(编排)。runner:nas(官网+服务端,容器化
node:20/golang:1.25)、mac(Android+macOS)、windows(Windows)。
CI「测试」= workflow_dispatch 手动触发跑一遍 + 观察产物/部署(非经典单元 TDD)。
~/code/jiu/.gitea/workflows/* + ~/code/jiu/scripts/ci/*(proven,copy+adapt)。docker run 官方镜像(node:20/golang:1.25),不装宿主工具链。GOPROXY=goproxy.cn,direct、PUB_HOSTED_URL/FLUTTER_STORAGE_BASE_URL=flutter-io.cn。FORGEJO_TOKEN/URL、MACOS_DEVELOPER_ID_CERT_P12_BASE64/PASSWORD、APPSTORE_API_KEY_P8_BASE64/APPSTORE_API_KEY_ID/APPSTORE_API_ISSUER_ID;仓库级 DEPLOY_SSH_KEY、ANDROID_KEYSTORE_BASE64/PASSWORD、ANDROID_KEY_ALIAS/PASSWORD、MACOS_APP_PROVISION_PROFILE_BASE64、MACOS_SYSEXT_PROVISION_PROFILE_BASE64。CURRENT_PROJECT_VERSION;Android NDK≥28、gomobile JDK17、libbox 包名 io.nekohasekai.libbox。$();提交带 Co-Authored-By footer。部署机 pangolin1(103.119.13.48),官网 pangolin.yanmeiai.com。_env.sh(镜像源 + ver_from_tag 用 ${ref#refs/tags/prefix-v} 参数展开);lib-forgejo.sh(forgejo_release_ensure / forgejo_upload_asset,curl+API);notify.sh。
验证:bash -n + shellcheck 0 告警 → commit。
scripts/ci/*.sh 纳入 shellcheck 扫描。push 观察全绿。
pangolin.yanmeiai.com vhost(web 根 /var/www/pangolin-site);CF DNS 指向 103.119.13.48(cf-api,记 baize);TLS 先 CF 橙云或并入 #25。
compile-site.sh:node:20 容器 npm ci && SITE_URL=https://pangolin.yanmeiai.com npm run build。deploy-site.sh:DEPLOY_SSH_KEY → rsync -az --delete dist/ pangolin1:/var/www/pangolin-site/。deploy-site.yml:tag site-v* + dispatch,runs-on: nas。curl -I https://pangolin.yanmeiai.com/ 200。
compile-backend.sh:golang:1.25 容器,CGO_ENABLED=0 GOOS=linux GOARCH=amd64 编 server/agent/migrate。release-server.sh:forgejo_release_ensure + 上传三个二进制。deploy-server.sh(核心,复刻手动次序):#!/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 + 行数守恒。
test.sh server → golang:1.25 go test ./...;test.sh client → flutter test。接入 deploy-server 的 test 步骤。
signingConfigs.release,从 env/keystore.properties 读 keystore + 三密码(ANDROID_*);buildTypes.release.signingConfig 指向它。本机验证 apksigner verify --print-certs 显示 CN=Pangolin(非 debug)。
compile-android.sh:build-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 可用。
site.ts.downloads.android = Forgejo /releases/latest/download/<asset> 稳定 URL(不支持则构建期烘焙)。重部署官网,点击落到最新 apk。关 todo 30A(Android 部分)。
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 --wait → stapler → dmg。runs-on: mac。验证:另一台 mac spctl -a -vv + stapler validate 通过。
flutter build windows --release → Inno Setup 打包(先不代码签名,首装有 SmartScreen 提示可接受)。tag client-v*/winbuild*,runs-on: windows。验证:windows 机装上能连。
site.ts.downloads 补齐 macos/windows;三端按钮全接 release。客户端发版触发官网重部署刷新。关 todo 30A/30B。
iOS(G)、备份/容灾(#26)、TLS(#25)、Windows 代码签名、上架商店。