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>