docs: CI/CD 多账号签名设计(Apple 多账号 × 多 App,配置切换)

方案:CI 签名拆成正交两维——account(用哪个 Apple 账号,Developer ID/Apple
Distribution 证书 + ASC Key 跨该账号所有 App 共享)× app(bundle 专属描述文件)。
App 在自己仓 scripts/signing.env 声明 SIGNING_ACCOUNT=us|cn 一行完成切换;工作流
读它、三元映射从 gitea 用户级取对应账号 SIGN_<ACCT>_* 密钥集,描述文件走仓库级 per-app。
证书按账号存一次共享 + 描述文件按 App 存本仓 → 避免多账号×多App 的密钥爆炸。

含三层配置模型、gitea 命名规范、workflow 切换机制(基于现有 secret→env 映射层)、
composite action 多 App 复用、新增账号/App/切换的操作手册、Phase 0(pangolin 仓库级
快速解锁)/Phase 1(完整多账号模型)分期、待核实的 gitea 能力。已登记 docs/index.html。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEVUXAbFT6bF1Qw27RHWoD
This commit is contained in:
wangjia
2026-09-07 00:08:30 +08:00
parent acb9d8052b
commit 5513377221
2 changed files with 186 additions and 0 deletions
+181
View File
@@ -0,0 +1,181 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CI/CD 多账号签名设计(Apple 多账号 × 多 App)</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:1000px;margin:0 auto;padding:48px 24px 96px}
h1{font-size:29px;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:52px 0 14px;padding-bottom:8px;border-bottom:1px solid var(--border)}
h3{font-size:16px;margin:28px 0 8px;color:var(--accent2)}
p{margin:10px 0}
code{font-family:var(--mono);font-size:.88em;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.6;color:#cdd3df}
pre .c{color:#6b7385}
pre .k{color:#e0b84f}
pre .s{color:#8fca7a}
pre .r{color:var(--bad)}
.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.acc{background:rgba(95,176,201,.16);color:var(--accent2)}
.tag.app{background:rgba(224,136,79,.16);color:var(--accent)}
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin:16px 0}
.card.root{border-left:3px solid var(--accent)}
.card.ok{border-left:3px solid var(--ok)}
.card.warn{border-left:3px solid var(--warn)}
.card h3{margin-top:0}
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}
td code{font-size:.85em}
ul,ol{padding-left:22px;margin:10px 0}
li{margin:6px 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}
hr{border:none;border-top:1px solid var(--border);margin:40px 0}
a{color:var(--accent2)}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media(max-width:720px){.grid2{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="wrap">
<h1>CI/CD 多账号签名设计</h1>
<p class="sub">Apple 多签名账号(中国 <span class="tag acc">CN</span> / 美国 <span class="tag acc">US</span> / …)× 多 Apppangolin / dudu / jiu …)· 按配置切换 · 2026-09-07</p>
<div class="lead">
<strong>一句话:</strong>把 CI 签名拆成正交的两维——<b>「用哪个账号」</b>account,跨 App 共享证书/ASC Key)和 <b>「哪个 App」</b>(bundle 专属描述文件)。App 在自己仓库的 <code>signing.env</code> 里声明 <code>SIGNING_ACCOUNT=us|cn</code> 一行即完成切换;工作流据此从 gitea 取对应账号的密钥集。新增账号=在 gitea 存一套 <code>SIGN_&lt;ACCT&gt;_*</code>;新增 App=拷工作流骨架 + 填 signing.env + 传本仓 profile。<b>零证书重复、切换即改一行。</b>
</div>
<h2>1. 目标与约束</h2>
<ul>
<li><b>多账号并存</b>CN(岩美北京 <code>BYL4KQHMTN</code>)与 USYanmei AI LLC <code>44WULXM6SV</code>)两套签名同时可用,互不干扰。</li>
<li><b>配置切换</b>:某 App 用哪个账号,由该 App 仓库内一处配置决定,改一行即切,无需动密钥。</li>
<li><b>多 App 复用</b>pangolin 之后 dudu/jiu 等都要发版;同账号的多个 App 共享同一套证书/ASC Key,不重复存。</li>
<li><b>产物无痕</b>:某 App 选了 US,产物里不得有 CN 痕迹(由各仓 <code>signing.env</code> 单源 + <code>gen-signing.mjs</code> 保证,见 <a href="apple-account-migration-runbook.html">迁移 Runbook</a>)。</li>
<li><b>不动 Android</b>Android keystore 自签、与 Apple 账号无关,始终 per-App 仓库级。</li>
</ul>
<h2>2. 核心心智模型:身份 ⟂ 密钥,account ⟂ app</h2>
<p>两组正交的拆分,是整个设计的地基:</p>
<div class="grid2">
<div class="card"><h3>身份 vs 密钥</h3>
<p><b>身份</b>(非机密,入 git,单源):Team ID / 实体名 / Bundle ID / App Group。已在各仓 <code>scripts/signing.env</code><br>
<b>密钥</b>(机密,存 gitea):证书 <code>.p12</code> / 私钥密码 / ASC <code>.p8</code> / 描述文件 base64。</p>
</div>
<div class="card"><h3>account-scoped vs app-scoped</h3>
<p><b>account 级</b>(一个 Apple 账号一套,跨该账号所有 App 共享):Developer ID / Apple Distribution 证书、ASC API Key。<br>
<b>app 级</b>(每个 App 各一份,因 bundle ID 不同):Provisioning Profiles。</p>
</div>
</div>
<p class="small">关键洞察:<b>证书是账号级的</b>(一张 Developer ID 证书能签该账号下任意 App),<b>描述文件是 App 级的</b>(绑定具体 bundle ID)。所以证书按账号存一次共享,描述文件按 App 存本仓——这条拆分让「多账号 × 多 App」不产生 N×M 的密钥爆炸。</p>
<h2>3. 三层配置模型</h2>
<table>
<tr><th></th><th>放哪</th><th>内容</th><th>粒度</th></tr>
<tr><td><b>① 身份单源</b></td><td>各 App 仓 <code>scripts/signing.env</code>(入 git</td><td>Team/实体/Bundle/AppGroup + <b><code>SIGNING_ACCOUNT=us|cn</code></b>(新增字段,即切换开关)</td><td><span class="tag app">per-app</span></td></tr>
<tr><td><b>② 账号密钥集</b></td><td>gitea <b>用户级</b> secret(跨仓共享)</td><td><code>SIGN_&lt;ACCT&gt;_*</code>Developer ID .p12 + 密码、Apple Distribution .p12 + 密码、ASC Key ID/Issuer/p8</td><td><span class="tag acc">per-account</span></td></tr>
<tr><td><b>③ App 描述文件</b></td><td>gitea <b>仓库级</b> secret</td><td>macOS App/Sysext + iOS App/Ext 四个 <code>*_PROVISION*_BASE64</code></td><td><span class="tag app">per-app</span></td></tr>
</table>
<p class="small">当前你的 gitea 是单用户账号拥有所有仓,「用户级」即事实上的「组织级共享层」;「仓库级」覆盖用户级同名值。Android <code>RELEASE_KEYSTORE</code>/<code>KEY_PASSWORD</code> 属 ③ 的 per-app,不变。</p>
<h2>4. gitea 密钥命名规范</h2>
<h3>② 账号级(用户级,每个账号一套)</h3>
<pre><span class="c"># 美国账号</span>
SIGN_US_DEVELOPER_ID_P12 <span class="c"># Developer ID Application .p12 base64macOS</span>
SIGN_US_DEVELOPER_ID_PASSWORD
SIGN_US_APPLE_DIST_P12 <span class="c"># Apple Distribution .p12 base64iOS</span>
SIGN_US_APPLE_DIST_PASSWORD
SIGN_US_ASC_KEY_ID <span class="c"># 8G78KGHL5C</span>
SIGN_US_ASC_ISSUER_ID
SIGN_US_ASC_KEY_P8 <span class="c"># AuthKey_*.p8 base64</span>
<span class="c"># 中国账号(把现有同类值改名到此前缀,或保留旧名让 cn 分支指向旧名)</span>
SIGN_CN_DEVELOPER_ID_P12 / _PASSWORD / SIGN_CN_APPLE_DIST_P12 / … / SIGN_CN_ASC_*</pre>
<h3>③ App 级(仓库级,每个 App 各一份)</h3>
<pre>MACOS_APP_PROVISION_PROFILE_BASE64
MACOS_SYSEXT_PROVISION_PROFILE_BASE64
IOS_APP_PROVISIONING_PROFILE_BASE64
IOS_PACKETTUNNEL_PROVISIONING_PROFILE_BASE64</pre>
<p class="small">描述文件绑定 bundle ID,天然 per-App;放仓库级,各 App 互不影响。</p>
<h2>5. 工作流:读账号 → 映射密钥</h2>
<p>现有 <code>deploy-client.yml</code> 每个 build job 本就有「secret → env 变量」映射层(<code>compile-*.sh</code> 消费)。切换只需把固定的 <code>secrets.X</code> 换成<b><code>SIGNING_ACCOUNT</code> 条件选择</b></p>
<h3>5.1 读出账号(一步,从 signing.env 或仓库变量)</h3>
<pre><span class="k">- name:</span> Resolve signing account
<span class="k">id:</span> acct
<span class="k">run:</span> <span class="c"># 从提交进仓的 signing.env 读单源,避免再设一处 gitea 变量</span>
. scripts/signing.env
echo "account=${SIGNING_ACCOUNT:?signing.env 缺 SIGNING_ACCOUNT}" &gt;&gt; "$GITHUB_OUTPUT"</pre>
<h3>5.2 按账号映射证书/ASC(三元表达式,2 账号足够;多账号见 §7)</h3>
<pre><span class="k">- name:</span> Compile (macOS)
<span class="k">env:</span>
<span class="k">ACCT:</span> ${{ steps.acct.outputs.account }}
MACOS_DEVELOPER_ID_CERT_P12_BASE64: ${{ env.ACCT == 'us'
&& secrets.SIGN_US_DEVELOPER_ID_P12 || secrets.SIGN_CN_DEVELOPER_ID_P12 }}
MACOS_DEVELOPER_ID_CERT_PASSWORD: ${{ env.ACCT == 'us'
&& secrets.SIGN_US_DEVELOPER_ID_PASSWORD || secrets.SIGN_CN_DEVELOPER_ID_PASSWORD }}
APPSTORE_API_KEY_ID: ${{ env.ACCT == 'us'
&& secrets.SIGN_US_ASC_KEY_ID || secrets.SIGN_CN_ASC_KEY_ID }}
APPSTORE_API_ISSUER_ID: ${{ env.ACCT == 'us'
&& secrets.SIGN_US_ASC_ISSUER_ID || secrets.SIGN_CN_ASC_ISSUER_ID }}
APPSTORE_API_KEY_P8_BASE64: ${{ env.ACCT == 'us'
&& secrets.SIGN_US_ASC_KEY_P8 || secrets.SIGN_CN_ASC_KEY_P8 }}
<span class="c"># ③ 描述文件是仓库级、per-app、与账号无关 → 直接引用</span>
MACOS_APP_PROVISION_PROFILE_BASE64: ${{ secrets.MACOS_APP_PROVISION_PROFILE_BASE64 }}
MACOS_SYSEXT_PROVISION_PROFILE_BASE64: ${{ secrets.MACOS_SYSEXT_PROVISION_PROFILE_BASE64 }}
<span class="k">run:</span> bash scripts/ci/compile-macos.sh "$REF_NAME"</pre>
<p class="small"><code>compile-macos.sh</code> 内部已 <code>source signing.env</code> 拿 Team/Bundle,与上面注入的证书/描述文件对齐 → 天然一致。iOS job 同理(<code>SIGN_&lt;ACCT&gt;_APPLE_DIST_*</code>)。</p>
<h2>6. 多 App 复用:抽成共享动作</h2>
<p>把「解析账号 + 导入证书到临时钥匙串 + 装描述文件」抽成一个 <b>composite action</b><code>.gitea/actions/apple-sign/action.yml</code>)或共享脚本,各 App 的 workflow 调用它,只传 <code>account</code> + 各自 profile。</p>
<pre><span class="c"># 某 App 的 deploy-client.yml 里</span>
<span class="k">- uses:</span> ./.gitea/actions/apple-sign
<span class="k">with:</span>
account: ${{ steps.acct.outputs.account }}
<span class="c"># 证书/ASC 由 action 内部按 account 从 SIGN_&lt;ACCT&gt;_* 取(secret 需显式透传)</span></pre>
<p class="small">gitea/GitHub 的 composite action 不自动继承 secret,须由调用方 <code>with</code>/<code>env</code> 显式传入 → §5 的三元映射留在调用方,action 收「已解析的值」。<b>新增 App</b> = 拷 workflow 骨架 + 写 <code>signing.env</code>(含 SIGNING_ACCOUNT) + 传 4 个仓库级 profile;账号侧零改动。</p>
<h2>7. 操作手册</h2>
<table>
<tr><th>场景</th><th>做什么</th></tr>
<tr><td><b>新增一个签名账号</b>(如再开个欧洲实体)</td><td>在 gitea 用户级存一套 <code>SIGN_&lt;NEW&gt;_*</code>;§5 的三元链加一档(或改 §7.1 的映射表法)。</td></tr>
<tr><td><b>新增一个 App</b>(同已有账号)</td><td>拷 workflow 骨架;App 仓 <code>signing.env</code><code>SIGNING_ACCOUNT</code> + 身份;建该 App 的 4 个描述文件传<b>仓库级</b>。证书/ASC 复用账号级,<b>不新增</b></td></tr>
<tr><td><b>切换某 App 的账号</b></td><td>改该仓 <code>signing.env</code><code>SIGNING_ACCOUNT</code>+ Team/Bundle 等身份、跑 <code>gen-signing.mjs</code>);换该仓的 4 个描述文件为新账号的。证书 secret 不用碰。</td></tr>
</table>
<h3>7.1 账号 &gt; 2 时:用映射步骤替代三元链</h3>
<p>三元 <code>a &amp;&amp; x || y</code> 只宜 2 档。多账号改用一个 shell 映射步骤,把 <code>SIGN_&lt;ACCT&gt;_*</code> 落成通用 env(需把该账号所有 secret 传进该步,用 <code>case "$ACCT"</code> 选)。或每账号一个 <code>--env-file</code>。本质是把「选择」从 YAML 表达式挪进脚本,可扩展到任意账号数。</p>
<h2>8. 落地分期</h2>
<div class="card ok"><h3>Phase 0 · 立即解锁 pangolin(最小改动,前向兼容)</h3>
<p>不动 workflow:把 US 证书/ASC/描述文件用<b>现有通用名</b><code>DEVELOPER_ID_P12</code>…)存到 <b>pangolin 仓库级</b>。仓库级覆盖用户级中国值 → pangolin CI 用美国、其他 App 用户级中国不变。<b>今天就能发版。</b></p>
<p class="small">代价:还没有 <code>SIGNING_ACCOUNT</code> 开关,是「按仓库物理隔离」而非「按配置切换」;后续升 Phase 1 要把这批 secret 改名。</p></div>
<div class="card"><h3>Phase 1 · 完整多账号模型(本设计)</h3>
<p><code>SIGN_&lt;ACCT&gt;_*</code> 账号层 + <code>signing.env</code><code>SIGNING_ACCOUNT</code> + workflow 三元映射 + composite action。第 2 个 App 迁移、或想要「配置切换」时做。</p></div>
<p><b>建议</b>pangolin 想尽快发版就先 Phase 0;若你不急发版、想一步到位,直接 Phase 1(多改一次 workflow,免日后改名返工)。两者产物完全一致,差别只在 secret 组织。</p>
<h2>9. 待核实的 gitea/forgejo 能力</h2>
<ul>
<li><b>用户级 secret 跨仓共享 + 仓库级覆盖同名</b>:现网已在用(记忆 <code>pangolin-apple-signing-assets</code>)→ ✅。</li>
<li><b>表达式三元 <code>&amp;&amp; ||</code> + <code>secrets.*</code><code>env:</code></b>GitHub 兼容语法,act_runner 应支持;上线前用一个 dummy secret 验一次。</li>
<li><b>composite action(本仓 <code>./.gitea/actions/*</code></b>forgejo runner 支持度需实测;不支持则退回「共享 shell 脚本」(scripts/ci/apple-sign-common.sh)。</li>
<li><b>仓库变量 <code>vars.*</code></b>:本设计改用 <code>signing.env</code><code>SIGNING_ACCOUNT</code>,不依赖 <code>vars</code>,规避版本差异。</li>
</ul>
<hr>
<p class="small">关联:<a href="apple-account-migration-runbook.html">Apple 账号迁移 Runbook</a> · 签名单源见 <code>scripts/signing.env</code> + <code>scripts/gen-signing.mjs</code> · 现网 secret 清单/层级见记忆 <code>pangolin-apple-signing-assets</code> / <code>pangolin-client-release-ci</code></p>
</div>
</body>
</html>
+5
View File
@@ -44,6 +44,11 @@
</div>
<h2>设计方案 / Specs</h2>
<a class="doc" href="ci-multi-account-signing-design.html">
<div class="t">CI/CD 多账号签名设计(Apple 多账号 × 多 App)<span class="tag html">HTML</span></div>
<div class="d">CI 签名拆成正交两维:account(用哪个 Apple 账号,证书/ASC Key 跨 App 共享)× app(bundle 专属描述文件)。App 在自己仓 signing.env 声明 SIGNING_ACCOUNT=us|cn 一行切换;工作流三元映射从 gitea 取对应账号 SIGN_&lt;ACCT&gt;_* 密钥集。三层配置(身份单源/账号级证书/App级profile)+ 命名规范 + composite action 复用 + 新增账号/App/切换操作手册 + Phase 0(仓库级快速解锁)/Phase 1(完整模型)分期。零证书重复、切换即改一行。</div>
<div class="path">docs/ci-multi-account-signing-design.html</div>
</a>
<a class="doc" href="notifications-design.html">
<div class="t">系统通知机制(Spec ③)<span class="tag html">HTML</span></div>
<div class="d">App 内统一通知收件箱(铃铛+列表),六类:重要/新特性/新闻/到账(个人定向)/版本/活动。单 notices 表 + user_id(NULL=广播);已读用服务端 last_read_at 水位多端同步;GET /v1/notices(合并广播+定向,unread_count) + POST /read。生产三路:nodectl notice 子命令(手工) / 事件钩子同事务(购买开通·邀请·首充·TG 到账,零孤儿通知) / 发版脚本联动插 version 公告。邮件仅 important+显式 --email(SMTP 直发+email_sent_at 幂等)。不做:APNs/FCM(二期)/逐条已读/偏好开关/六语内容。原型先行:两端通知视图先统一(桌面pill vs 移动icon)再动代码。</div>