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:
wangjia
2026-07-05 23:40:59 +08:00
parent 0ab31b4c58
commit 6be777dff5
3 changed files with 371 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pangolin CI/CD 实现计划(#30</title>
<style>
:root{
--bg:#0f1117; --panel:#171a22; --panel2:#1d2129; --fg:#e6e8ee; --fg2:#a8afbd;
--accent:#e0884f; --accent2:#5fb0c9; --ok:#5ec27a; --bad:#e06a6a; --warn:#e0b84f;
--border:#272c36; --mono:"SF Mono",ui-monospace,Menlo,Consolas,monospace;
--sans:-apple-system,"PingFang SC","Helvetica Neue",Arial,sans-serif;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.7;font-size:15px}
.wrap{max-width:960px;margin:0 auto;padding:48px 24px 96px}
h1{font-size:30px;line-height:1.3;margin:0 0 8px;letter-spacing:-.01em}
.sub{color:var(--fg2);font-size:15px;margin:0 0 32px}
h2{font-size:21px;margin:44px 0 14px;padding-bottom:8px;border-bottom:1px solid var(--border)}
h3{font-size:16px;margin:24px 0 8px;color:var(--fg)}
p{margin:10px 0}
code{font-family:var(--mono);font-size:.86em;background:var(--panel2);padding:1px 6px;border-radius:5px;color:#f0d9c4}
pre{background:#0a0c11;border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow-x:auto;font-family:var(--mono);font-size:12.5px;line-height:1.55;color:#cdd3df}
.tag{display:inline-block;font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px;vertical-align:middle}
.tag.ok{background:rgba(94,194,122,.16);color:var(--ok)}
.tag.warn{background:rgba(224,184,79,.16);color:var(--warn)}
.tag.info{background:rgba(95,176,201,.16);color:var(--accent2)}
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:16px 20px;margin:14px 0}
.card h3{margin-top:0;color:var(--accent2)}
.files{font-family:var(--mono);font-size:12px;color:var(--fg2);margin:6px 0 10px}
table{width:100%;border-collapse:collapse;margin:16px 0;font-size:14px}
th,td{text-align:left;padding:9px 12px;border-bottom:1px solid var(--border);vertical-align:top}
th{color:var(--fg2);font-weight:600;font-size:13px}
ul,ol{padding-left:22px;margin:8px 0}
li{margin:5px 0}
.lead{background:linear-gradient(180deg,rgba(224,136,79,.10),transparent);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin:0 0 8px}
.small{color:var(--fg2);font-size:13px}
a{color:var(--accent2)}
.back{display:inline-block;margin-bottom:24px;font-size:13px}
b{color:#fff}
.phase{font-size:19px;margin:40px 0 6px;color:var(--accent);font-weight:700}
</style>
</head>
<body>
<div class="wrap">
<a class="back" href="index.html">← 文档索引</a>
<h1>Pangolin CI/CD 实现计划</h1>
<p class="sub">#30 · 2026-07-05 · <span class="tag info">阅读版</span> · 执行真相源 <code>docs/superpowers/plans/2026-07-05-cicd.md</code>(带 checkbox)· 设计 <a href="cicd-design.html">cicd-design.html</a></p>
<div class="lead">
<b>目标:</b>tag 触发的「编译 → 测试 → 发版(Gitea release)→ 部署」全自动。镜像 jiu 的
<code>scripts/ci/*.sh</code>(逻辑)+ <code>.gitea/workflows/*.yml</code>(编排)。runner:nas(官网+服务端,容器化
<code>node:20</code>/<code>golang:1.25</code>)、macAndroid+macOS)、windowsWindows)。
<b>CI「测试」= <code>workflow_dispatch</code> 手动触发跑一遍 + 观察产物/部署</b>(非经典单元 TDD)。
</div>
<h2>全局约束</h2>
<ul>
<li>参考:jiu 的 <code>~/code/jiu/.gitea/workflows/*</code> + <code>~/code/jiu/scripts/ci/*</code>provencopy+adapt)。</li>
<li>nas 每 job <code>docker run</code> 官方镜像(<code>node:20</code>/<code>golang:1.25</code>),不装宿主工具链。</li>
<li>国内镜像:<code>GOPROXY=goproxy.cn,direct</code><code>PUB_HOSTED_URL/FLUTTER_STORAGE_BASE_URL=flutter-io.cn</code></li>
<li>Secrets(已建,对齐 jiu):账户级 <code>FORGEJO_TOKEN/URL</code><code>MACOS_DEVELOPER_ID_CERT_P12_BASE64/PASSWORD</code><code>APPSTORE_API_KEY_P8_BASE64/APPSTORE_API_KEY_ID/APPSTORE_API_ISSUER_ID</code>;仓库级 <code>DEPLOY_SSH_KEY</code><code>ANDROID_KEYSTORE_BASE64/PASSWORD</code><code>ANDROID_KEY_ALIAS/PASSWORD</code><code>MACOS_APP_PROVISION_PROFILE_BASE64</code><code>MACOS_SYSEXT_PROVISION_PROFILE_BASE64</code></li>
<li>客户端铁律:macOS 递增 <code>CURRENT_PROJECT_VERSION</code>Android NDK≥28、gomobile JDK17、libbox 包名 <code>io.nekohasekai.libbox</code></li>
<li>Bash 禁 <code>$()</code>;提交带 Co-Authored-By footer。部署机 <code>pangolin1</code>103.119.13.48),官网 <code>pangolin.yanmeiai.com</code></li>
</ul>
<div class="phase">Phase 1 —— 基座 + 官网 + 服务端(无签名,可立即上线)</div>
<div class="card">
<h3>Task 1 · scripts/ci 基座</h3>
<div class="files">Create: scripts/ci/_env.sh · lib-forgejo.sh · notify.sh(抄 jiu 同名改 pangolin 专属值)</div>
<b>产出:</b><code>_env.sh</code>(镜像源 + <code>ver_from_tag</code><code>${ref#refs/tags/prefix-v}</code> 参数展开);<code>lib-forgejo.sh</code><code>forgejo_release_ensure</code> / <code>forgejo_upload_asset</code>curl+API);<code>notify.sh</code>
验证:<code>bash -n</code> + <code>shellcheck</code> 0 告警 → commit。
</div>
<div class="card">
<h3>Task 2 · checks 工作流(保留)</h3>
<div class="files">Modify: .gitea/workflows/ci.yml</div>
现有 ci.ymlnasshellcheck/openapi/redline/flutter/go test)保留;把 <code>scripts/ci/*.sh</code> 纳入 shellcheck 扫描。push 观察全绿。
</div>
<div class="card">
<h3>Task 3 · 官网发布</h3>
<div class="files">Create: scripts/ci/compile-site.sh · deploy-site.sh · .gitea/workflows/deploy-site.yml</div>
<b>前置(基础设施,改机器前问用户):</b>pangolin1 装 nginx/caddy 配 <code>pangolin.yanmeiai.com</code> vhostweb 根 <code>/var/www/pangolin-site</code>);CF DNS 指向 103.119.13.48<code>cf-api</code>,记 baize);TLS 先 CF 橙云或并入 #25。
<ul>
<li><code>compile-site.sh</code><code>node:20</code> 容器 <code>npm ci && SITE_URL=https://pangolin.yanmeiai.com npm run build</code></li>
<li><code>deploy-site.sh</code><code>DEPLOY_SSH_KEY</code><code>rsync -az --delete dist/ pangolin1:/var/www/pangolin-site/</code></li>
<li><code>deploy-site.yml</code>tag <code>site-v*</code> + dispatch<code>runs-on: nas</code></li>
</ul>
验证:dispatch → <code>curl -I https://pangolin.yanmeiai.com/</code> 200。
</div>
<div class="card">
<h3>Task 4 · 服务端发布(固化 F3/F4,带回滚)</h3>
<div class="files">Create: scripts/ci/compile-backend.sh · release-server.sh · deploy-server.sh · .gitea/workflows/deploy-server.yml</div>
<ul>
<li><code>compile-backend.sh</code><code>golang:1.25</code> 容器,<code>CGO_ENABLED=0 GOOS=linux GOARCH=amd64</code> 编 server/agent/migrate。</li>
<li><code>release-server.sh</code><code>forgejo_release_ensure</code> + 上传三个二进制。</li>
<li><code>deploy-server.sh</code>(核心,复刻手动次序):</li>
</ul>
<pre>#!/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" &lt;&lt;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 &gt;/dev/null &amp;&amp; echo healthz OK</pre>
<code>deploy-server.yml</code>tag <code>server-v*</code> + dispatchnascompile → test → release → deploy。
验证:dispatch → migrate 版本前进 + <code>/healthz</code> 200 + 行数守恒。
</div>
<div class="card">
<h3>Task 5 · scripts/ci/test.sh</h3>
<code>test.sh server</code><code>golang:1.25</code> <code>go test ./...</code><code>test.sh client</code><code>flutter test</code>。接入 deploy-server 的 test 步骤。
</div>
<div class="phase">Phase 2 —— Android(解锁官网下载链接)</div>
<div class="card">
<h3>Task 6 · Android gradle 接 release 签名</h3>
<div class="files">Modify: client/android/app/build.gradle · Create: keystore.propertiesgitignore</div>
<code>signingConfigs.release</code>,从 env/<code>keystore.properties</code> 读 keystore + 三密码(<code>ANDROID_*</code>);<code>buildTypes.release.signingConfig</code> 指向它。本机验证 <code>apksigner verify --print-certs</code> 显示 CN=Pangolin(非 debug)。
</div>
<div class="card">
<h3>Task 7 · Android CI</h3>
<div class="files">Create: scripts/ci/compile-android.sh · release-client.sh · .gitea/workflows/build-android.yml</div>
<code>compile-android.sh</code><code>build-libbox.sh android</code>JDK17/NDK≥28)→ secrets 落 keystore → <code>flutter build apk --release --split-per-abi --dart-define=PANGOLIN_API_URL=…</code><code>build-android.yml</code>tag <code>client-v*</code><code>runs-on: mac</code>provision → compile → release。验证:真机 <code>adb install -r</code> 可用。
</div>
<div class="card">
<h3>Task 8 · 官网下载链接接 Android</h3>
<div class="files">Modify: web/website/src/config/site.tsdownloads.android)· Download.astro</div>
<code>site.ts.downloads.android</code> = Forgejo <code>/releases/latest/download/&lt;asset&gt;</code> 稳定 URL(不支持则构建期烘焙)。重部署官网,点击落到最新 apk。关 todo 30AAndroid 部分)。
</div>
<div class="phase">Phase 3 —— macOS + Windows</div>
<div class="card">
<h3>Task 9 · macOS 签名+公证 dmg</h3>
<div class="files">Create: scripts/ci/compile-macos.sh · .gitea/workflows/build-macos.yml(证书导入+notarytool 可复用 jiu compile-macos.sh</div>
建临时 keychain → <code>MACOS_DEVELOPER_ID_CERT_P12_BASE64</code> 导入 → 两个描述文件解码装入 → 递增 <code>CURRENT_PROJECT_VERSION</code> → Xcode Developer ID 构建 app+sysext → <code>notarytool submit --key-id $APPSTORE_API_KEY_ID --issuer $APPSTORE_API_ISSUER_ID --wait</code><code>stapler</code> → dmg。<code>runs-on: mac</code>。验证:另一台 mac <code>spctl -a -vv</code> + <code>stapler validate</code> 通过。
</div>
<div class="card">
<h3>Task 10 · Windows 安装包</h3>
<div class="files">Create: scripts/ci/compile-windows.sh · .gitea/workflows/build-windows.yml(参照 jiu install-innosetup.ps1</div>
<code>flutter build windows --release</code> → Inno Setup 打包(<b>先不代码签名</b>,首装有 SmartScreen 提示可接受)。tag <code>client-v*</code>/<code>winbuild*</code><code>runs-on: windows</code>。验证:windows 机装上能连。
</div>
<div class="card">
<h3>Task 11 · 下载链接全端闭环</h3>
<code>site.ts.downloads</code> 补齐 macos/windows;三端按钮全接 release。客户端发版触发官网重部署刷新。关 todo 30A/30B。
</div>
<h2>不在本计划</h2>
<p class="small">iOSG)、备份/容灾(#26)、TLS#25)、Windows 代码签名、上架商店。</p>
</div>
</body>
</html>
+5
View File
@@ -86,6 +86,11 @@
</a>
<h2>实现计划 / Plans</h2>
<a class="doc" href="cicd-plan.html">
<div class="t">CI/CD 全流程 实现计划(#30<span class="tag html">HTML</span></div>
<div class="d">阅读版;执行真相源 <code>docs/superpowers/plans/2026-07-05-cicd.md</code>(含 checkbox)。三期 11 任务:Phase1 基座+官网+服务端(无签名可立即上线,服务端固化 F3/F4 备份/迁移/回滚) → Phase2 Android(接 release keystore 签名,解锁下载链接) → Phase3 macOS 公证 dmg + Windows 安装包。runner 混合 nas/mac/windows;密钥已建(对齐 jiu)。设计见 cicd-design.html。</div>
<div class="path">docs/cicd-plan.html · 真相源 docs/superpowers/plans/2026-07-05-cicd.md</div>
</a>
<a class="doc" href="device-session-management-plan.html">
<div class="t">设备 &amp; 会话管理 + 每设备流量归因 实现计划(P1–P6)<span class="tag html">HTML</span></div>
<div class="d">阅读版;执行真相源为 <code>docs/superpowers/plans/2026-06-29-device-session-management.md</code>(含 checkbox)。P1 设备注册打通 → P2 sessions表+在线/最后登录 → P3 强制退出/清除 → P4 每设备流量 → P5 2FA信任(future) → P6 UI重做。</div>
+193
View File
@@ -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 代码签名、上架商店。