先看结论与判断条件
- 轨道 API 返回的 versionCodes、status、userFraction 和 targeting 是控制面事实,不等于设备已经收到候选。
- 候选身份元组至少包含 applicationId、versionCode、签名证书摘要、产物摘要、构建变体和 provenance 摘要。
- versionCode 只能连接轨道记录与上传版本,不能替代本地文件摘要、证书身份和构建来源。
- inProgress、halted、completed 与 draft 必须原样保存;halted 不能写成已安装版本回滚或召回。
- 发布操作回执与后续查询回执应分开保存,使 API 接受、编辑提交、审核与可见性保持不同证据状态。
- 任何重新构建、重新签名、轨道切换或用户比例变化都会使旧绑定失效,必须生成新的不可变回执。
先拆开平台控制面与本地候选身份
发布团队常见的证据错配,是把 Track update 返回成功直接写成“这个 APK 已上线”。Google Play Developer API track update 的返回对象描述编辑中的轨道配置,它能够确认服务端接受了受授权请求并返回 Track,但响应本身没有本地 APK 文件路径、文件摘要或签名证书摘要。若流水线只保存 HTTP 成功和 track 名称,就无法证明审核时看到的本地候选与轨道中 versionCode 对应的上传对象是同一个文件。
正确做法是先为候选生成不可变身份元组,再把轨道事实附加到该元组。身份元组至少记录 applicationId、versionCode、签名证书 SHA-256、APK 或 AAB 主体 SHA-256、构建变体、构建任务和 provenance 摘要。轨道回执记录 edit、track、release status、versionCodes、userFraction、countryTargeting、更新优先级与查询时间。两部分各自保留原始来源,不把缺失字段凭空补入平台响应。
绑定后的结论仍然分层。若本地摘要与构建证明一致,只能确认候选来源;若 versionCode 出现在目标 track,只能确认轨道引用该版本;若后续查询仍显示期望状态,只能确认控制面状态。设备是否可见、商店审核是否结束、分发是否覆盖目标地区以及业务质量是否合格,需要额外证据。报告应保留这些边界,避免一个绿色状态遮蔽尚未完成的环节。
| 事实域 | 核心字段 | 可信来源 | 不能证明 |
|---|---|---|---|
| 本地产物 | 主体摘要、包名、版本码 | 只读产物检查 | 轨道已接受 |
| 签名身份 | 证书摘要与轮换证明 | 签名校验工具与发布记录 | 设备已更新 |
| 构建来源 | builder、buildType、材料摘要 | provenance | 运行时质量 |
| 轨道配置 | track、status、versionCodes | Track API 回执 | 本地文件摘要 |
| 分阶段范围 | userFraction、countryTargeting | Track release 对象 | 每台设备已收到 |
| 后续状态 | 查询时间与当前 release | 独立查询回执 | 业务功能通过 |
轨道回执中哪些字段必须原样保留
Google Play Developer API tracks 描述 Track 及其 releases。每个 release 可以记录 name、versionCodes、releaseNotes、status、userFraction、countryTargeting 和 inAppUpdatePriority。门禁不应只截取 versionCode,因为同一版本处于 draft、inProgress、halted 或 completed 时含义不同;灰度比例和地区目标也会改变实际覆盖范围。保存结构化原始响应,才能在之后解释当时的发布意图。
status 必须按 API 值保存,不要翻译成一个模糊的“成功”。draft 表示尚未面向用户发布,inProgress 表示分阶段推进,halted 表示停止继续扩大该发布,completed 表示发布配置完成。halted 并不会自动移除已经安装该版本的用户,因此它不能作为客户端回滚、撤回或数据恢复回执。若业务要求回退,需要另外定义可交付版本、服务端兼容和用户状态处置。
userFraction 只有与 status、track、versionCodes 和查询时间一起保存才有意义。单独看到一个比例,无法知道它对应哪个版本、哪个国家范围或哪个编辑状态。countryTargeting 也不能被忽略,因为相同轨道名在不同目标范围下的用户可见性不同。回执应保持字段缺失与显式值的区别,API 未返回的值不能由流水线默认补为全量。
| 字段 | 用于回答 | 必须绑定 | 常见误写 |
|---|---|---|---|
| track | 发布进入哪个轨道 | package 与查询时间 | 把轨道名当成候选身份 |
| versionCodes | 该 release 引用了哪些版本 | 本地 versionCode 与上传记录 | 把版本码当作文件摘要 |
| status | 发布控制面处于何种状态 | 同一 release 快照 | 所有状态都写成已上线 |
| userFraction | 分阶段覆盖目标 | inProgress 或 halted 语义 | 写成实际安装比例 |
| countryTargeting | 目标国家范围 | 轨道与版本集合 | 省略后宣称全地区可见 |
| updatePriority | 应用内更新优先级参数 | 对应 versionCodes | 写成系统强制更新 |
versionCode 是连接键,不是产物指纹
versionCode 是 Play 轨道记录与 Android 更新判断的重要连接键,但它不是加密摘要。同一个本地构建环境可以生成内容不同却使用相同 versionCode 的未上传文件,团队也可能在上传后错误地拿另一份同版本产物做测试。门禁必须把 versionCode 与 artifactSha256 同时记录,随后通过上传回执或受控构建流程证明轨道引用的版本来自该候选。仅靠数值相同不能完成文件级身份确认。
Android publish your app 指南将 release 变体、签名产物、测试和依赖服务准备视为发布前工作,上传只是流程中的一个环节。候选绑定因此应在上传前完成:冻结产物摘要、证书摘要和测试回执;上传时记录版本与操作身份;更新轨道时记录 Track 响应;提交编辑后再查询当前轨道。任何阶段重新构建都会生成新主体,不能继续沿用之前的文件级门禁。
当一个 release 的 versionCodes 包含多个版本时,回执应保存完整集合,不要只保留目标版本。目标版本存在说明它被该 release 引用,其他版本则可能仍参与覆盖或兼容策略。若同一 versionCode 出现在多个 release 或多个轨道,发布负责人必须根据期望轨道、状态和范围做明确选择。脚本可以阻断歧义,但最终发布意图仍需责任人确认。
应用更新身份还要检查包名与签名连续性
How Android app updates work 说明,Android 将更新与 applicationId、签名身份和 versionCode 条件关联。候选绑定不能只检查轨道里的版本码,还要在本地检查包名与发布应用一致,并核对签名证书或合法轮换证明。包名错配意味着它不是同一应用更新,证书错配则可能无法形成有效更新路径。平台条件成立仍不代表业务迁移和数据兼容已经通过。
签名证书摘要应来自候选产物的只读验证,而不是构建脚本中的配置字符串。若使用 Play App Signing,需要区分上传密钥与应用签名密钥在流程中的角色,并在内部回执中写清证书摘要的来源。Track API 不返回本地签名证书摘要,因此不能声称平台响应已经核对该字段;这项关系要由上传记录、Play 配置和组织控制补足。
versionCode 必须满足平台更新顺序,但更高的数值不等于更安全或更兼容。数据库迁移、文件格式、服务端协议和加固配置都可能让新候选无法安全替换旧版本。本文只处理轨道回执与候选身份绑定,不给出业务防重放或版本回滚策略。发布报告应把平台更新条件和应用自己的兼容门禁分开,分别保存证据。
| 字段 | 来源 | 核对方法 | 错配处理 |
|---|---|---|---|
| applicationId | 最终 Manifest | 与 Play 应用包名精确比较 | 阻断并更换候选 |
| versionCode | 最终 Manifest | 在目标 release versionCodes 中查找 | 阻断或重建轨道编辑 |
| 签名证书摘要 | 签名验证输出 | 与批准的发布身份或轮换证明比较 | 阻断并调查签名链 |
| 产物摘要 | 最终 APK 或 AAB | 与上传和 provenance 主体比较 | 旧回执失效 |
| 构建变体 | 构建记录 | 与发布配置和测试范围比较 | 补齐正确变体证据 |
| provenance 摘要 | 不可变构建证明 | 验证主体和材料绑定 | 不得宣称来源已确认 |
provenance 连接产物摘要与构建来源
SLSA Provenance v1.1 定义的 provenance 可描述 subject、builder、buildType、外部参数和依赖 materials。对轨道绑定而言,最关键的是候选 artifactSha256 必须出现在 provenance subject 中,并且构建者与构建类型属于批准范围。这样可以证明发布门禁检查的文件来自记录的构建过程,而不是在上传前被未知步骤替换。
签名证书摘要不是 SLSA provenance 必须提供的标准发布字段。组织可以在受控的外部参数或单独签名回执中记录证书摘要,再由绑定程序比较,但必须标明这是项目约束而非规范保证。若 provenance 只覆盖未签名 AAB,而轨道绑定针对最终 APK,则还需记录从 AAB 到分发 APK 的受控转换关系,不能用一个不同主体的摘要直接替代。
provenance 同样有边界。它证明声明的构建来源,不证明 Track API 当前状态,不证明签名密钥未被滥用,也不证明候选运行正确。门禁需要同时持有本地产物验证、provenance 验证、轨道更新响应和后续轨道查询。缺少任一类时,应输出具体的未确认状态,不把可追溯性扩张为上线或安全结论。
| 回执 | 绑定输入 | 确认范围 | 失效条件 |
|---|---|---|---|
| 候选身份回执 | 包名、版本、证书、主体摘要 | 本地被测对象 | 文件或签名变化 |
| 构建来源回执 | subject、builder、buildType、materials | 声明的生成关系 | 主体或证明变化 |
| 轨道更新回执 | edit、track、release 请求与响应 | 平台接受控制请求 | 新编辑或配置变化 |
| 编辑提交回执 | edit 与提交时间 | 编辑进入后续处理 | 提交失败或编辑废弃 |
| 轨道查询回执 | track 当前快照与查询时间 | 控制面当前状态 | 后续轨道变化 |
| 运行验证回执 | 设备、安装来源、候选身份 | 指定环境中的行为 | 候选或环境变化 |
发布状态要按时间形成证据链
一次发布至少有三个时间点:本地候选冻结、轨道更新返回、编辑提交后的重新查询。轨道更新返回只能记录操作已被 API 接受,编辑提交回执说明该编辑进入平台后续处理,重新查询才能确认目标 track 当前展示的 release 状态。商店审核和设备可见性仍可能在另一时间发生,因此报告应保存 observedAt,而不是把不同时间的事实拼成一个同步状态。
查询还需要防止读错应用与轨道。请求中的 packageName、editId 和 track 要进入回执摘要,响应中的 track 必须与期望值精确匹配。若目标 versionCode 不存在、出现在多个 release、状态与期望不符或用户比例缺失,脚本应失败关闭。对于 halted 状态,报告应明确它停止继续推进,不声称已经安装的客户端被回滚。
分阶段发布期间,userFraction 或 countryTargeting 的任何变化都应产生新回执。旧回执可以保留审计历史,但不能继续代表当前范围。团队若要扩大比例,应先关联当前候选的质量信号和审批,再执行新的轨道更新。自动化可以校验字段和身份,不能替发布责任人判断异常率、服务端承载和业务风险是否允许扩大。
| 观察 | 可以确认 | 仍待确认 | 禁止表述 |
|---|---|---|---|
| update 返回 Track | 平台接受轨道更新请求 | 编辑提交和后续状态 | 候选已面向用户上线 |
| 查询为 draft | 当前配置仍为草稿 | 后续提交决定 | 正在灰度 |
| 查询为 inProgress | 控制面处于分阶段发布 | 实际设备覆盖与质量 | 所有目标用户已更新 |
| 查询为 halted | 停止继续扩大该 release | 已安装用户和恢复方案 | 客户端已自动回滚 |
| 查询为 completed | 轨道配置完成发布 | 地区可见性与设备行为 | 所有设备安装成功 |
| 目标版本缺失 | 当前回执未找到绑定 | 是否读错编辑或轨道 | 沿用旧成功回执 |
用校验脚本拒绝版本、轨道与来源错配
下面的 Python 脚本读取候选身份 JSON、Track 查询 JSON 和 SLSA provenance JSON。候选文件提供 packageName、versionCode、artifactSha256、signingCertSha256、expectedTrack、expectedStatus 与批准的 builder;Track 文件保留 packageName、track 和 releases;provenance 提供 subject 与 predicate.builder。脚本只比较结构化字段,不调用 Play API,也不接触服务账号凭据。
脚本要求目标 versionCode 在目标 track 中恰好出现一次,release 状态与期望一致,候选摘要在 provenance subject 中恰好匹配,并核对 builder 与摘要格式。它输出 track、versionCode、status、userFraction、证书摘要和 provenance 是否匹配,但不声称证书摘要来自 Track API。任何字段缺失、类型错误、重复版本或来源错配都会返回非零状态,适合接入发布门禁。
输入 JSON 的可信来源仍需组织控制。candidate.json 应由只读产物检查生成,track.json 应来自受授权的独立查询,provenance 应经过签名或可信分发验证。若有人可以同时修改三份输入,字段一致不代表真实。脚本也不验证商店审核和设备可见性,因此通过后的结论只能写成“本地候选、构建来源与当前轨道字段一致”。
from pathlib import Path
import json
import re
import sys
if len(sys.argv) != 4:
raise SystemExit(2)
paths = [Path(value) for value in sys.argv[1:]]
if any(not path.is_file() for path in paths):
raise SystemExit(2)
try:
candidate, track, provenance = [json.loads(path.read_text(encoding="utf-8")) for path in paths]
except (OSError, UnicodeError, json.JSONDecodeError):
raise SystemExit(2)
required = ("packageName", "versionCode", "artifactSha256", "signingCertSha256", "expectedTrack", "expectedStatus", "expectedBuilder")
if any(key not in candidate for key in required):
raise SystemExit(2)
if not isinstance(candidate["versionCode"], int) or candidate["versionCode"] <= 0:
raise SystemExit(2)
sha_pattern = re.compile(r"^[a-f0-9]{64}$")
if not sha_pattern.fullmatch(candidate["artifactSha256"]) or not sha_pattern.fullmatch(candidate["signingCertSha256"]):
raise SystemExit(2)
if track.get("packageName") != candidate["packageName"] or track.get("track") != candidate["expectedTrack"]:
raise SystemExit(3)
releases = track.get("releases")
if not isinstance(releases, list):
raise SystemExit(2)
target = str(candidate["versionCode"])
matches = []
for release in releases:
version_codes = release.get("versionCodes", [])
if not isinstance(version_codes, list) or any(not isinstance(value, str) for value in version_codes):
raise SystemExit(2)
if target in version_codes:
matches.append(release)
if len(matches) != 1:
raise SystemExit(3)
release = matches[0]
if release.get("status") != candidate["expectedStatus"]:
raise SystemExit(3)
subjects = provenance.get("subject")
if not isinstance(subjects, list):
raise SystemExit(2)
subject_matches = [item for item in subjects if item.get("digest", {}).get("sha256") == candidate["artifactSha256"]]
if len(subject_matches) != 1:
raise SystemExit(3)
predicate = provenance.get("predicate", {})
builder_id = predicate.get("runDetails", {}).get("builder", {}).get("id")
if builder_id != candidate["expectedBuilder"]:
raise SystemExit(3)
report = {
"packageName": candidate["packageName"],
"track": track["track"],
"versionCode": candidate["versionCode"],
"status": release["status"],
"userFraction": release.get("userFraction"),
"artifactSha256": candidate["artifactSha256"],
"signingCertSha256": candidate["signingCertSha256"],
"provenanceSubjectMatched": True,
"builderMatched": True,
"distributionConfirmed": False
}
print(json.dumps(report, ensure_ascii=False, indent=2))把门禁结果写成可审计发布决定
合格回执应包含四组材料:候选身份、构建来源、轨道操作和当前查询。候选身份保存包名、版本码、证书与主体摘要;构建来源保存 provenance 摘要及验证结果;轨道操作保存请求、响应和 edit;当前查询保存 track、release 字段与观察时间。发布决定引用这些回执摘要,不复制可变的控制台截图,也不把服务账号令牌写入报告。
NIST SP 800-218 SSDF 强调把安全活动、变更和供应链风险处理纳入开发流程。对应到轨道绑定,就是让候选冻结、测试、上传、审批、发布和查询形成有责任人的证据链。若任何摘要错配或字段缺失,发布流程应停止并定位输入来源。框架不定义 Play API 的具体状态,也不证明当前候选已达到安全、兼容或业务目标。
门禁通过后的决定必须带有限定词。例如“候选摘要与 provenance subject 一致,目标 versionCode 出现在 production 轨道的 inProgress release 中,观察时间为某时刻”;不要缩写成“生产已上线”。若仍待商店审核、地区可见性或设备验证,应逐项列出。这样下一个操作者能直接看到缺口,而不是重新猜测成功截图代表什么。
准备材料与适用限制
实施前应准备最终 APK 或 AAB、产物摘要、最终 Manifest 身份、签名证书摘要、构建变体、provenance、测试回执、目标 packageName、track、期望 status 和分阶段策略。受授权的 Play 操作应由最小权限服务身份执行,原始响应与请求摘要进入受控存储。文章中的代码不处理认证,也不建议把访问令牌写进命令行、日志或内容仓库。
本方法不解决所有发布风险。它不证明 Play 审核通过,不证明每个设备收到相同 APK,不验证 split 生成、运行时兼容、业务数据迁移或服务端防重放,也不替代签名轮换验证。Track 字段可能在操作后继续变化,所以观察时间和重新查询是回执的一部分。没有真实平台响应和同候选产物时,只能保留未接入或待确认状态。
准备接入评估时,可在御盾中央平台提交候选摘要、签名证书摘要、构建证明、目标轨道、发布请求与脱敏后的 Track 响应。登录、注册、申请、价格和控制台操作统一由中央平台承接。具体是否可发布仍取决于项目审批、Play 控制面状态和同候选验证,不因一份静态字段校验报告自动成立。
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| Google Play Track release 可以记录 versionCodes、status、userFraction、countryTargeting 与更新优先级。 | Google Play Developer API tracks 描述 Track 与 release 的字段和状态。 | 这些字段是控制面事实,不包含本地 APK 文件摘要,也不证明设备已经完成安装。 |
| 受授权客户端可以提交轨道更新,并从 API 获得返回的 Track 对象。 | Google Play Developer API track update 描述 edits.tracks.update 请求与响应。 | 更新请求成功不证明编辑已经完成后续处理、商店审核通过或业务质量合格。 |
| Android 发布前需要准备 release 变体、签名产物、测试和应用依赖服务。 | Android publish your app 描述发布准备、构建与签名等流程环节。 | 通用发布指南不证明某个具体候选能够直接上架,也不替代商店政策审核。 |
| Android 应用更新依赖 applicationId、兼容签名身份与可接受的 versionCode。 | How Android app updates work 描述应用更新的包身份、签名和版本条件。 | 平台更新条件不证明数据库迁移、业务协议、加固配置和运行时兼容已经通过。 |
| provenance 可以把产物主体与构建者、构建类型、外部参数及依赖材料关联。 | SLSA Provenance v1.1 定义 subject、builder、buildType、parameters 和 materials 等关系。 | 来源证明不能单独确认 Track 当前状态、签名密钥使用正确或候选运行安全。 |
| 安全发布应保留构建、验证、变更和供应链风险处理证据。 | NIST SP 800-218 SSDF 给出将安全实践纳入软件开发与发布生命周期的组织级框架。 | SSDF 不定义 Play 轨道字段,也不证明任何候选或发布已经完成。 |
| versionCode 应作为 Track 与候选的连接键,同时保留产物和证书摘要。 | 工程判断:版本码可重复出现在不同本地文件中,摘要和签名身份才能限制证据被错配。 | 本地元组一致仍需可信上传记录,才能进一步确认平台所持对象的来源。 |
| 轨道更新响应和提交后的独立查询应作为两个时间点的回执保存。 | 工程判断:控制请求被接受与当前轨道状态是不同事实,配置也可能在之后继续变化。 | 查询结果仍不能替代商店审核、用户可见性和真实设备验证。 |
工程常见问题
Track update 返回成功是否说明 APK 已经上线?
不能。它说明平台接受了受授权的轨道更新请求并返回 Track 对象。还需提交编辑、重新查询状态,并分别核对商店审核、地区可见性和设备行为。
versionCode 与轨道记录一致,为什么还要保存 APK 摘要?
versionCode 不是文件指纹,本地可能存在内容不同但版本码相同的文件。产物摘要、签名证书摘要和构建证明共同限制回执被挪用到错误候选。
halted 是否表示已经安装的版本被自动回滚?
不是。halted 表示停止继续推进分阶段发布,不影响已经安装该版本的用户。客户端回退、召回与数据恢复需要独立方案和证据。
completed 是否等于所有设备都安装了新版本?
不等于。completed 描述轨道 release 的控制面状态。设备资格、地区、缓存、用户更新选择和运行结果仍需其他数据确认。
provenance 匹配后还需要签名检查吗?
需要。provenance 绑定声明的构建主体和来源,但签名身份、上传过程和 Play 配置属于其他控制。证书摘要应来自候选验证并与批准身份比较。
申请发布候选绑定评估要准备哪些材料?
准备最终 APK 或 AAB、主体摘要、包名、versionCode、签名证书摘要、构建变体、provenance、测试回执、目标轨道、发布请求和脱敏 Track 响应,再通过御盾中央平台提交。