6be777dff5
三期 11 任务:Phase1 基座+官网+服务端(无签名) → Phase2 Android → Phase3 macOS/Windows。 服务端部署固化 F3/F4 备份/迁移/回滚;下载链接接 release 资产;密钥对齐 jiu。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
174 lines
12 KiB
HTML
174 lines
12 KiB
HTML
<!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>)、mac(Android+macOS)、windows(Windows)。
|
||
<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>(proven,copy+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.yml(nas,shellcheck/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> vhost(web 根 <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" <<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</pre>
|
||
<code>deploy-server.yml</code>:tag <code>server-v*</code> + dispatch,nas,compile → 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.properties(gitignore)</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.ts(downloads.android)· Download.astro</div>
|
||
<code>site.ts.downloads.android</code> = Forgejo <code>/releases/latest/download/<asset></code> 稳定 URL(不支持则构建期烘焙)。重部署官网,点击落到最新 apk。关 todo 30A(Android 部分)。
|
||
</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">iOS(G)、备份/容灾(#26)、TLS(#25)、Windows 代码签名、上架商店。</p>
|
||
</div>
|
||
</body>
|
||
</html>
|