☰
App Store Connect CLI 签名对账(signing reconcile)完全指南:基于归档与设备清单的确定性、只增式 Ad Hoc 配置漂移修复
2026/9/29 8:02:26 网站建设 项目流程

【免费下载链接】App-Store-Connect-CLI

Fast, scriptable CLI for the App Store Connect API. Automate TestFlight, builds, submissions, signing, analytics, screenshots, subscriptions, and more

项目地址:https://gitcode.com/gh_mirrors/ap/App-Store-Connect-CLI
点击查看免费下载

asc signing reconcile是 App-Store-Connect-CLI 中一个以归档(archive)和设备清单为输入、采用"计划-应用"(plan/apply)两段式执行的签名对账命令组,用于把"已存在的 Provisioning Profile 是否包含新注册设备、是否覆盖归档内每个内嵌 target、是否在请求的时间窗口内仍然有效"这类问题变成可审计、可恢复、可幂等重试的确定性操作。读完本文,你将掌握plan/apply两个子命令的完整用法、设备清单 JSON 的严格格式、四种只增动作(registerDevice / createBundleId / createProfile / downloadProfile)的触发条件,以及底层如何用 SHA-256 指纹、计划哈希、原子写入和 CMS 内容校验来保证对账过程的安全与可追溯。

命令定位:为何需要 reconcile 而不是直接 fetch/sync

asc signing fetch只能查找或创建单个provisioning profile,它无法证明一个已存在的 profile:是否包含新注册的设备、是否与归档内每一个内嵌 target(扩展、Watch、App Clip)匹配、是否在请求的时间窗口内仍然有效。因此signing reconcile是紧挨着signing fetch与signing sync的附加型(additive)、以工件(artifact)为支撑的命令组。

从 命令注册源码 可以看到,signing命令下共挂载六个子命令:fetch、keychain、reconcile、resign、run、sync;reconcile本身又由SigningReconcilePlanCommand()与SigningReconcileApplyCommand()两个叶命令组成(见 signing_reconcile.go)。

命令组完整契约如下(同时见于 signing_reconcile.go 的示例帮助文本):

asc signing reconcile plan \ --archive-path .asc/artifacts/App.xcarchive \ --devices-file .asc/distribution/devices.json \ [--certificate CERTIFICATE_ID] \ [--minimum-validity-days 7] \ [--max-mutations 32] \ [--state-dir .asc/distribution/signing] \ [--overwrite] asc signing reconcile apply \ [--plan .asc/distribution/signing/plan.json] \ --confirm

两个叶命令都支持标准的--output json|table|markdown输出格式(通过shared.BindOutputFlags绑定,见 signing_reconcile.go)。

各选项的默认值与取值范围(源码确认)

选项默认值约束(源码)
--archive-path必填必须以.xcarchive结尾,见 validateSigningReconcilePlanFlags
--devices-file必填严格 JSON v1 设备清单,见下文
--certificate空(自动选择)显式指定 iOS 分发证书资源 ID;缺失或不符合作业会形成 blocker
--minimum-validity-days7取值范围0到3650(reconcileMaximumValidityDays),见 常量定义 与校验逻辑
--max-mutations32至少为1;计划内只增型远端变更动作的上限
--state-dir.asc/distribution/signing计划/回执/已下载 profile 的状态目录
--overwritefalse允许覆盖已存在的plan.json(否则已存在时报os.ErrExist)
--plan(apply).asc/distribution/signing/plan.json必须存在且哈希校验通过
--confirm(apply)false缺失时报--confirm is required用法错误,见 apply 命令

工作流语义:plan 只读、apply 只增

Planning 是只读的:它绝不修改 App Store Connect 远端状态,只做归档盘点、设备解析、证书筛选、profile 候选评估,然后把结果写成权限为 mode-0600 的plan.json。Apply 则是有条件的执行器:

  • 读取并校验plan.json(含计划哈希重算);
  • 在每次计划内的变更动作之前重新读取远端状态;
  • 只执行计划中记录的附加型(additive)动作;
  • 下载并逐一校验每个被选中或新建的 profile;
  • 写入 mode-0600 的receipt.json以及profiles/<UUID>.mobileprovision。

每个动作完成后都会写入部分回执(partial receipt)。重试时,每个幂等的 ensure/verification 动作都会针对当前状态重新执行——回执只是"进展的证据(evidence of progress)",而不是"跳过校验的授权(authority to skip checks)"。

设备清单输入格式与严格校验

设备输入是严格 JSON,会拒绝未知字段(DisallowUnknownFields)、重复 UDID、非 iOS 设备以及空设备列表。清单格式(schemaVersion必须为 1):

{"schemaVersion":1,"devices":[{"name":"Test iPhone","udid":"...","platform":"IOS"}]}

从 decodeSigningDevicesFile 的实现可见更多细节:

  • name必须为 1–128 个可打印字符,禁止控制字符与双向格式控制字符(safeReconcileDeviceName);
  • udid规范化(去掉-、:,转大写)后长度须在 8–48 之间,原始长度 8–64;
  • platform仅接受IOS(大小写不敏感,统一转大写后比较);
  • 重复 UDID(规范化后)直接报错;
  • 解析后的设备按指纹(fingerprint)排序,保证后续哈希与计划输出的确定性。

设备与计划输入是受保护的、有界常规文件:在 Unix 上必须为 mode 0600 或更严格(0o177掩码检查),且不允许跟随任何符号链接组件。读取过程中会做"读前/读后 stat 一致性"检查(readProtectedFileBounded),防止 TOCTOU 类替换。受保护输入与解析失败被归类为用法错误(usage error,exit 2),诊断信息对路径与值做了安全化;原始设备名与 UDID 会在远端 preflight 失败信息中被脱敏为[redacted](sanitizeReconcileError)。

隐私设计:计划与输出中不出现原始 UDID

无论是plan.json还是正常输出,都不会包含原始 UDID。设备引用一律使用SHA-256 派生的 16 位十六进制指纹(fingerprintDevice,见 signing_reconcile.go),以及已知时的 App Store Connect 资源 ID;设备名也只以nameSha256形式进入计划。这样即使 plan 工件被泄漏,也无法直接还原设备身份。

错误分类与阻塞状态

  • 校验失败(输入、计划不匹配、选项非法)→ 用法错误,exit 2;
  • 远端或工件失败(网络、API 错误、写盘失败)→ 普通非零错误;
  • 一个合法但被阻塞的计划会被成功写出(ready=false),apply 会拒绝执行它(executeSigningReconcileApplyPlanWithUsage)。

底层 API 契约:reconcile 用到的 App Store Connect 端点

离线 OpenAPI 快照(见 docs/openapi/latest.json 与 docs/openapi/paths.txt)定义了这里用到的精确操作:

  • GET /v1/devices,按filter[platform]分页,解析已启用设备;POST /v1/devices必填name、udid、platform;
  • GET /v1/bundleIds?filter[identifier]=...;对缺失的显式 iOS 标识符用POST /v1/bundleIds(必填name、identifier、platform);
  • GET /v1/certificates,分页并过滤为 iOS 分发类型;
  • GET /v1/bundleIds/{id}/profiles,随后分页GET /v1/profiles/{id}/certificates与/devices;
  • POST /v1/profiles(类型IOS_APP_ADHOC、一个 bundle ID、一个选中的证书、期望的设备集);GET /v1/profiles/{id}提供 base64 的 profile 内容用于校验与下载。

分页拉取在代码中通过asc.WithDevicesLimit(200)、WithCertificatesLimit(200)等选项配合Links.Next循环实现(getAllReconcileDevices、getAllReconcileCertificates)。

关键约束:App Store Connect API 中的 profile 是不可变的。因此 reconcile 在设备集变化时创建后继 profile(successor)并保留每一个旧 profile。它永远不会发送DELETE或PATCH,不会启用或重命名设备,不会创建证书,也不会变更 capability。

归档盘点:如何识别内嵌 target 与签名 entitlements

归档盘点逻辑位于 signing_reconcile_archive.go:

  1. 读取归档根Info.plist的ApplicationProperties,校验ApplicationPath不会逃逸归档(validateSigningArchiveRelativePath),并取出签名 team(Team键)。
  2. 主应用(main application)的Info.plist必须无歧义地声明iPhoneOS平台:CFBundleSupportedPlatforms若非空必须只有一个且等于iPhoneOS,DTPlatformName若非空必须为iphoneos;两者都缺失则无法验证平台,直接拒绝(validateSigningArchivePlatform)。非 iOS 或无法验证的归档平台会在规划 iOS 资源之前被拒绝。
  3. 通过discoverEmbeddedSigningTargets递归发现内嵌 target:主应用的PlugIns/*.appex(app 扩展)、Watch/*.app(Watch 应用)及其PlugIns/*.appex(Watch 扩展)、AppClips/*.app(App Clip),全部按稳定路径排序。
  4. 对每个 target 读取Info.plist的CFBundleIdentifier与CFBundleExecutable,用/usr/bin/codesign -d --entitlements :-提取签名可执行文件的 entitlements(readCodesignEntitlements)。实现细节上,为避免codesign拒绝/dev/fd代码对象,代码会把已打开的 no-follow 句柄复制到私有临时目录(mode 0700)再执行 codesign。

归档盘点要求:所有 target 必须使用同一个 team(entitlements 中的com.apple.developer.team-identifier必须等于归档 team);重复 bundle identifier 会被拒绝;每个 target 的application-identifierentitlements 必须与其CFBundleIdentifier自洽。

缺 App ID 时的能力基线判断

缺失的显式 App ID只有在基线 entitlements 集下才可以被计划创建。目标若要求一个尚未注册的 capability,则构成 blocker——因为本命令不改变 capability。基线集(signingCapabilitiesForEntitlements)包括application-identifier、com.apple.application-identifier、com.apple.developer.team-identifier、keychain-access-groups、get-task-allow、beta-reports-active;可映射为 capability 的 entitlements(如aps-environment→PUSH_NOTIFICATIONS、com.apple.developer.associated-domains→ASSOCIATED_DOMAINS、com.apple.developer.healthkit→HEALTHKIT等)要求对应 capability 已存在;带值型设置(value-specific settings)的 entitlements 如 App Groups(com.apple.security.application-groups)、Apple Pay 商家标识(com.apple.developer.in-app-payments)、Network Extensions(com.apple.developer.networking.networkextension)、Wallet pass 标识(com.apple.developer.pass-type-identifiers)则始终作为不可验证项阻塞——仅凭 capability 存在无法证明这些带值 entitlements 正确,宁可阻塞也不冒险创建不可用的 profile。

规划(plan)阶段:设备、证书与 profile 的判定规则

设备解析

planDesiredDevices(signing_reconcile_plan.go)把清单中的每个 UDID 与远端已启用设备做规范化匹配:

  • 恰好 1 个已启用匹配 → 记录资源 ID 与状态;
  • 多个已启用匹配 → blocker(设备解析到多个启用资源);
  • 存在但被禁用 → blocker(只增命令不会重新启用设备);
  • 完全不存在 → 生成registerDevice动作(POST /v1/devices)。

证书选择

selectReconcileCertificateWithFingerprint(signing_reconcile_plan.go)只考虑活跃、未过期、且有效期跨越--minimum-validity-days窗口的 iOS 分发证书(类型IOS_DISTRIBUTION或DISTRIBUTION,Activated仅当显式为 false 时拒绝):

  • 多于一个合格证书 → blocker,除非--certificate显式选定一个;
  • 显式指定的证书缺失或不合格 → blocker;
  • 选中的证书会被解析 DER(x509.ParseCertificate),并把证书的SHA-256 指纹、team ID(Subject OU,要求恰好一个)绑定进计划(certificatePlanRef);证书内容有效期与 API 过期时间不一致也会被拒绝。

此外计划会校验证书 team 与归档 team 一致(不一致即 blocker,executeSigningReconcilePlan)。

Profile 可复用性判定

一个既有 profile 只有在同时满足以下条件时才可复用(getProfileCandidates):

  • 类型为IOS_APP_ADHOC且状态为 active;
  • 有效期跨过最小窗口(minimumExpiration = now + minimumValidityDays);
  • 属于精确的App ID(findExactReconcileBundleID通过filter[identifier]分页并做精确匹配);
  • 使用选中的证书(profile 的 certificate 关联恰好为 1 个且等于计划证书 ID);
  • 设备集恰好等于期望的已启用设备集;
  • 其经认证的 CMS 内容(pkcs7.Parse+Verify)在语义上包含 target 的签名 entitlements 与选中的证书指纹(profileContentMatchesTarget)。

设备超集(superset)不会被复用,因为那会扩大超出显式输入的分发范围。合格 profile 按更晚过期时间优先、其次资源 ID排序。若没有合格者,计划包含一个确定性 profile 创建动作。Profile 名称是 bundle、证书与设备集的哈希(ASC Ad Hoc <bundle> <12位指纹>,见 deterministicProfileName),因此重试会收敛(converge)——同样的输入总是得到同样的名称与同样的候选结果。

App ID seed 校验

对于已存在的 App ID,seedId必须与归档 target 的AppIDPrefix(由application-identifierentitlements 去掉.<bundleID>后缀得到)精确匹配(validateReconcileBundleSeed)。apply 阶段会在并发创建收敛后、以及 profile 创建前各重复一次该检查。

计划哈希与 mutation 上限

hashSigningReconcilePlan(signing_reconcile.go)对除去generatedAt与哈希自身之外的整个计划工件做 JSON 序列化后取 SHA-256。计划哈希覆盖:归档 target 描述符、team 与 entitlements、期望设备指纹、选中的证书、观察到的远端前置条件、有序动作、变更上限与输出路径。动作计数(registerDevice/createBundleId/createProfile)若超过--max-mutations,计划会进入 blocker 状态。

应用(apply)阶段:哈希校验、远端重解析与幂等重试

apply 的执行流程(executeSigningReconcileApplyPlanWithUsage):

  1. 拒绝ready=false或带 blocker 的计划;
  2. 结构校验计划(validateSigningApplyPlan):action ID 唯一性、device:/bundle:/profile:/download:ID 与目标/设备/证书的一致性、mutation 计数与动作一致性;
  3. 重新盘点本地输入(verifySigningLocalInputs):重读 devices 文件、重新解析归档,比对 team、targets(entitlements 用精确 JSON 数值语义比较)与设备集 SHA-256;
  4. 重新解析远端前置条件:证书复选(ID + SHA-256 + 有效期窗口,必须与计划逐字段相等)、设备解析、每个 target 的规划复跑(只接受"单调、已满足"的漂移);冲突响应后是精确重读而非盲目重试(ensureReconcileDevice/ensureReconcileBundleID在 POST 后若结果不确定,会立即重新精确查找收敛,见 signing_reconcile_apply.go);
  5. 逐动作执行,每步持久化部分回执;registerDevice拒绝 PATCH 已禁用设备;createBundleId创建ASC <identifier>命名的 iOS bundle;createProfile先复验 capability、再检查合格候选、最后POST /v1/profiles(409/不确定响应仅在精确重读证明确定性名称与合格性后才接受,ensureReconcileProfile);
  6. 全部动作完成后回执标记complete=true。

数值精确性与回执绑定

apply 在比较计划与重新解析的 entitlements 数值时,使用json.Number+big.Rat做超越 float64 无损范围的整数精确比较,而不是四舍五入(exactSigningNumber)。

恢复回执在持久化前会被重新绑定到哈希保护的计划状态目录(loadOrStartSigningReceiptWithUsage校验receipt.StateDir/ReceiptPath与计划精确一致,见 signing_reconcile_apply.go),因此回执字段无法把恢复写入重定向到其他位置。

下载 profile 的原子发布

下载的 profile 以create-only(不覆盖)方式写入(writeSigningStateJSON 的CreateNewFileAtomic)。重试只有在既有文件字节的 SHA-256 与要写入内容完全一致时,才允许复用同一 UUID 文件名(测试 TestWriteVerifiedProfileRejectsDifferentContentForExistingUUID 覆盖此行为)。原子发布只容忍"目录持久性同步不支持"这类平台/文件系统错误(在无替换发布成功后),其他同步失败保持致命。

输出、回执与可观测性

plan与apply的终端输出分别是计划摘要与回执摘要(renderSigningPlan):

# plan 输出列:Ready | Plan Hash | Targets | Devices | Mutations | Blockers | Plan Path # apply 输出列:Complete | Plan Hash | Actions | Receipt Path

结合--output json可获得完整工件,便于 Agent 或脚本解析。状态目录最终包含:plan.json(计划,mode 0600)、receipt.json(回执,mode 0600)、profiles/<UUID>.mobileprovision(已校验的 profile 文件)。

兼容性、测试覆盖与设计取舍

兼容性:这是附加型(additive)表面——既有 signing 命令及其输出保持不变(见 signing.go 的命令组结构)。

测试覆盖(signing_reconcile_test.go 与配套的signing_reconcile_adapter_test.go、signing_reconcile_archive_test.go、signing_reconcile_protected_unix_test.go):测试从命令边界 RED 开始,覆盖严格输入校验先于认证(TestSigningReconcilePlanValidatesBeforeAuth)、受保护有界输入、确定性哈希与排序、GET-only 规划、分页、App ID 与 profile 创建载荷、幂等恢复、entitlement/profile 校验、伪造 CMS 拒绝(TestDecodeReconcileMobileProvisionRejectsForgedCMS)、内嵌证书绑定、精确设备集隐私、同 UUID 内容冲突、文件模式与符号链接/路径包含。另有聚焦包测试、命令测试、内置二进制冒烟测试、生成的命令文档与仓库门禁完成验证。

设计取舍(原文明确说明):

  • API 不支持更新既有 profile,删除陈旧 profile 会让工作流变成破坏性操作,因此刻意排除;
  • 接受原始 bundle/设备 flag 而非归档与版本化输入文件会更短,但会遗漏内嵌 target 且让 Agent 重试难以审计;
  • 自动启用 capability 会减少 blocker,但会实质性扩大账户变更范围,保留给单独的显式工作流(仓库中另见 signing-sync 相关设计 与 capability reconcile 输出)。

典型使用流程

在 macOS 上(signing reconcile依赖codesign读取签名 entitlements,因此仅支持 darwin,见 validateSigningReconcilePlatform),建议流程:

# 1) 准备严格格式的设备清单(mode 0600) chmod 600 .asc/distribution/devices.json # 2) 只读规划:检查并输出计划(不修改远端) asc signing reconcile plan \ --archive-path .asc/artifacts/App.xcarchive \ --devices-file .asc/distribution/devices.json \ --output json # 3) 人工/Agent 审查计划:确认 ready=true、动作集合与 mutation 数量 # 4) 确认并应用(只增动作 + 下载校验后的 profile) asc signing reconcile apply \ --plan .asc/distribution/signing/plan.json \ --confirm # 5) 用已校验 profile 进行签名/导出(可选) asc signing run --identity ./signing/App.p12 --profile .asc/distribution/signing/profiles/<UUID>.mobileprovision -- xcodebuild -exportArchive

结合 docs/design/read-only-mode.md 与 docs/design/flag-value-indirection.md 等设计文档,可以把plan阶段嵌入只读审计流水线,把apply --confirm作为人工把关后的发布步骤。若计划因 blocker(证书歧义、缺失 capability、App ID seed 不匹配、设备已禁用等)无法就绪,应先修正输入或显式指定--certificate,再重新规划——不要修改计划文件本身,因为任何手工改动都会使planHash校验失败并强制重新规划。

【免费下载链接】App-Store-Connect-CLI

Fast, scriptable CLI for the App Store Connect API. Automate TestFlight, builds, submissions, signing, analytics, screenshots, subscriptions, and more

项目地址:https://gitcode.com/gh_mirrors/ap/App-Store-Connect-CLI
点击查看免费下载
上一篇:思源宋体CN:7种字重免费商用字体完全指南
下一篇:Hasura GraphQL Engine CLI Migrations v3 镜像:入口点自动迁移与元数据应用实战指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询