diff --git a/docs/ci-multi-account-signing-design.html b/docs/ci-multi-account-signing-design.html new file mode 100644 index 0000000..c4bd639 --- /dev/null +++ b/docs/ci-multi-account-signing-design.html @@ -0,0 +1,181 @@ + + + + + +CI/CD 多账号签名设计(Apple 多账号 × 多 App) + + + +
+ +

CI/CD 多账号签名设计

+

Apple 多签名账号(中国 CN / 美国 US / …)× 多 App(pangolin / dudu / jiu …)· 按配置切换 · 2026-09-07

+ +
+一句话:把 CI 签名拆成正交的两维——「用哪个账号」(account,跨 App 共享证书/ASC Key)和 「哪个 App」(bundle 专属描述文件)。App 在自己仓库的 signing.env 里声明 SIGNING_ACCOUNT=us|cn 一行即完成切换;工作流据此从 gitea 取对应账号的密钥集。新增账号=在 gitea 存一套 SIGN_<ACCT>_*;新增 App=拷工作流骨架 + 填 signing.env + 传本仓 profile。零证书重复、切换即改一行。 +
+ +

1. 目标与约束

+ + +

2. 核心心智模型:身份 ⟂ 密钥,account ⟂ app

+

两组正交的拆分,是整个设计的地基:

+
+

身份 vs 密钥

+

身份(非机密,入 git,单源):Team ID / 实体名 / Bundle ID / App Group。已在各仓 scripts/signing.env
+密钥(机密,存 gitea):证书 .p12 / 私钥密码 / ASC .p8 / 描述文件 base64。

+
+

account-scoped vs app-scoped

+

account 级(一个 Apple 账号一套,跨该账号所有 App 共享):Developer ID / Apple Distribution 证书、ASC API Key。
+app 级(每个 App 各一份,因 bundle ID 不同):Provisioning Profiles。

+
+
+

关键洞察:证书是账号级的(一张 Developer ID 证书能签该账号下任意 App),描述文件是 App 级的(绑定具体 bundle ID)。所以证书按账号存一次共享,描述文件按 App 存本仓——这条拆分让「多账号 × 多 App」不产生 N×M 的密钥爆炸。

+ +

3. 三层配置模型

+ + + + + +
放哪内容粒度
① 身份单源各 App 仓 scripts/signing.env(入 git)Team/实体/Bundle/AppGroup + SIGNING_ACCOUNT=us|cn(新增字段,即切换开关)per-app
② 账号密钥集gitea 用户级 secret(跨仓共享)SIGN_<ACCT>_*:Developer ID .p12 + 密码、Apple Distribution .p12 + 密码、ASC Key ID/Issuer/p8per-account
③ App 描述文件gitea 仓库级 secretmacOS App/Sysext + iOS App/Ext 四个 *_PROVISION*_BASE64per-app
+

当前你的 gitea 是单用户账号拥有所有仓,「用户级」即事实上的「组织级共享层」;「仓库级」覆盖用户级同名值。Android RELEASE_KEYSTORE/KEY_PASSWORD 属 ③ 的 per-app,不变。

+ +

4. gitea 密钥命名规范

+

② 账号级(用户级,每个账号一套)

+
# 美国账号
+SIGN_US_DEVELOPER_ID_P12          # Developer ID Application .p12 base64(macOS)
+SIGN_US_DEVELOPER_ID_PASSWORD
+SIGN_US_APPLE_DIST_P12            # Apple Distribution .p12 base64(iOS)
+SIGN_US_APPLE_DIST_PASSWORD
+SIGN_US_ASC_KEY_ID               # 8G78KGHL5C
+SIGN_US_ASC_ISSUER_ID
+SIGN_US_ASC_KEY_P8               # AuthKey_*.p8 base64
+# 中国账号(把现有同类值改名到此前缀,或保留旧名让 cn 分支指向旧名)
+SIGN_CN_DEVELOPER_ID_P12 / _PASSWORD / SIGN_CN_APPLE_DIST_P12 / … / SIGN_CN_ASC_*
+

③ App 级(仓库级,每个 App 各一份)

+
MACOS_APP_PROVISION_PROFILE_BASE64
+MACOS_SYSEXT_PROVISION_PROFILE_BASE64
+IOS_APP_PROVISIONING_PROFILE_BASE64
+IOS_PACKETTUNNEL_PROVISIONING_PROFILE_BASE64
+

描述文件绑定 bundle ID,天然 per-App;放仓库级,各 App 互不影响。

+ +

5. 工作流:读账号 → 映射密钥

+

现有 deploy-client.yml 每个 build job 本就有「secret → env 变量」映射层(compile-*.sh 消费)。切换只需把固定的 secrets.X 换成SIGNING_ACCOUNT 条件选择

+

5.1 读出账号(一步,从 signing.env 或仓库变量)

+
- name: Resolve signing account
+  id: acct
+  run: # 从提交进仓的 signing.env 读单源,避免再设一处 gitea 变量
+    . scripts/signing.env
+    echo "account=${SIGNING_ACCOUNT:?signing.env 缺 SIGNING_ACCOUNT}" >> "$GITHUB_OUTPUT"
+

5.2 按账号映射证书/ASC(三元表达式,2 账号足够;多账号见 §7)

+
- name: Compile (macOS)
+  env:
+    ACCT: ${{ 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 }}
+    # ③ 描述文件是仓库级、per-app、与账号无关 → 直接引用
+    MACOS_APP_PROVISION_PROFILE_BASE64: ${{ secrets.MACOS_APP_PROVISION_PROFILE_BASE64 }}
+    MACOS_SYSEXT_PROVISION_PROFILE_BASE64: ${{ secrets.MACOS_SYSEXT_PROVISION_PROFILE_BASE64 }}
+  run: bash scripts/ci/compile-macos.sh "$REF_NAME"
+

compile-macos.sh 内部已 source signing.env 拿 Team/Bundle,与上面注入的证书/描述文件对齐 → 天然一致。iOS job 同理(SIGN_<ACCT>_APPLE_DIST_*)。

+ +

6. 多 App 复用:抽成共享动作

+

把「解析账号 + 导入证书到临时钥匙串 + 装描述文件」抽成一个 composite action.gitea/actions/apple-sign/action.yml)或共享脚本,各 App 的 workflow 调用它,只传 account + 各自 profile。

+
# 某 App 的 deploy-client.yml 里
+- uses: ./.gitea/actions/apple-sign
+  with:
+    account: ${{ steps.acct.outputs.account }}
+    # 证书/ASC 由 action 内部按 account 从 SIGN_<ACCT>_* 取(secret 需显式透传)
+

gitea/GitHub 的 composite action 不自动继承 secret,须由调用方 with/env 显式传入 → §5 的三元映射留在调用方,action 收「已解析的值」。新增 App = 拷 workflow 骨架 + 写 signing.env(含 SIGNING_ACCOUNT) + 传 4 个仓库级 profile;账号侧零改动。

+ +

7. 操作手册

+ + + + + +
场景做什么
新增一个签名账号(如再开个欧洲实体)在 gitea 用户级存一套 SIGN_<NEW>_*;§5 的三元链加一档(或改 §7.1 的映射表法)。
新增一个 App(同已有账号)拷 workflow 骨架;App 仓 signing.envSIGNING_ACCOUNT + 身份;建该 App 的 4 个描述文件传仓库级。证书/ASC 复用账号级,不新增
切换某 App 的账号改该仓 signing.envSIGNING_ACCOUNT(+ Team/Bundle 等身份、跑 gen-signing.mjs);换该仓的 4 个描述文件为新账号的。证书 secret 不用碰。
+

7.1 账号 > 2 时:用映射步骤替代三元链

+

三元 a && x || y 只宜 2 档。多账号改用一个 shell 映射步骤,把 SIGN_<ACCT>_* 落成通用 env(需把该账号所有 secret 传进该步,用 case "$ACCT" 选)。或每账号一个 --env-file。本质是把「选择」从 YAML 表达式挪进脚本,可扩展到任意账号数。

+ +

8. 落地分期

+

Phase 0 · 立即解锁 pangolin(最小改动,前向兼容)

+

不动 workflow:把 US 证书/ASC/描述文件用现有通用名DEVELOPER_ID_P12…)存到 pangolin 仓库级。仓库级覆盖用户级中国值 → pangolin CI 用美国、其他 App 用户级中国不变。今天就能发版。

+

代价:还没有 SIGNING_ACCOUNT 开关,是「按仓库物理隔离」而非「按配置切换」;后续升 Phase 1 要把这批 secret 改名。

+

Phase 1 · 完整多账号模型(本设计)

+

SIGN_<ACCT>_* 账号层 + signing.envSIGNING_ACCOUNT + workflow 三元映射 + composite action。第 2 个 App 迁移、或想要「配置切换」时做。

+

建议:pangolin 想尽快发版就先 Phase 0;若你不急发版、想一步到位,直接 Phase 1(多改一次 workflow,免日后改名返工)。两者产物完全一致,差别只在 secret 组织。

+ +

9. 待核实的 gitea/forgejo 能力

+ + +
+

关联:Apple 账号迁移 Runbook · 签名单源见 scripts/signing.env + scripts/gen-signing.mjs · 现网 secret 清单/层级见记忆 pangolin-apple-signing-assets / pangolin-client-release-ci

+ +
+ + diff --git a/docs/index.html b/docs/index.html index 91a668a..fb7ba33 100644 --- a/docs/index.html +++ b/docs/index.html @@ -44,6 +44,11 @@

设计方案 / Specs

+ +
CI/CD 多账号签名设计(Apple 多账号 × 多 App)HTML
+
CI 签名拆成正交两维:account(用哪个 Apple 账号,证书/ASC Key 跨 App 共享)× app(bundle 专属描述文件)。App 在自己仓 signing.env 声明 SIGNING_ACCOUNT=us|cn 一行切换;工作流三元映射从 gitea 取对应账号 SIGN_<ACCT>_* 密钥集。三层配置(身份单源/账号级证书/App级profile)+ 命名规范 + composite action 复用 + 新增账号/App/切换操作手册 + Phase 0(仓库级快速解锁)/Phase 1(完整模型)分期。零证书重复、切换即改一行。
+
docs/ci-multi-account-signing-design.html
+
系统通知机制(Spec ③)HTML
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)再动代码。