Files
pangolin/docs/cicd-plan.html
T
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

174 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>