发布一个新版本时,最容易让团队误判的并不是打包失败,而是“包能安装、审核也通过,却只有一部分用户出现故障”。这类问题往往无法靠重新编译解决:用户已经升级,设备上有真实数据,新旧应用可能还会与同一套服务端通信。继续扩大分发比例,风险会累积;把旧安装包重新上传,又不一定符合版本递增与分发平台的要求。

前三篇讨论了多产品构建、签名关系以及交付产物指纹。本篇要把这些零散的检查串成一次可追溯的版本迭代:明确什么叫可发布,什么情况必须暂停,出现事故之后怎样降低影响范围,以及怎样交付修复版而不损伤本地数据。这里的“恢复”重点是控制分发与修复业务,不把它等同于“让所有用户自动降级安装旧包”。

版本迭代与分阶段发布技术封面

1. 把“构建成功”与“允许分发”分成两张票

先考虑一个有 internal 和 public 两类产品配置的 Stage 应用。内部产品服务于验证,公开产品服务于正式分发。两种 product 可以共享业务代码,但资源、服务端环境、签名和交付记录必须有清晰边界。这里真正需要追踪的不是某个文件夹名,而是已经交付到审核环节的那一个二进制文件。

建议建立两份记录。第一份是构建记录,至少写明 Git 提交、构建 product、应用版本号、签名检查结论、产物路径与 SHA-256。第二份是发布决策记录,写明目标版本、拟覆盖范围、负责人、人工合规检查结果以及暂停条件。二者通过产物哈希关联,不依赖“文件名看着差不多”的判断。哈希能证明文件内容是否发生变化,不能单独证明签名合法,也不能代替真机兼容性测试。

HarmonyOS 项目使用应用级 AppScope/app.json5 中的 versionCode 和 versionName 描述版本。前者应作为发布流水线的可比较整数,后者服务于用户可读展示;切勿拿字符串版本号的字典序代替版本递增检查,例如 1.10 与 1.9 的排序不应由普通字符串比较决定。版本号与提交的产物、签名信息必须保持一致,而不是审核前临时手动修改。

一次可靠的交付门禁可以拆为四道:配置一致、产物唯一、质量证据完整、发布策略已批准。其中前三道可以部分自动化,最后一道必须有业务负责人明确承担决策。应用隐私政策链接、权限用途解释、目标设备验证记录等材料也应归档,但不要塞进正式 APP 包内部。

2. 用一段小脚本阻止“拿错包”和“版本倒退”

下面这段 Node.js 脚本放在发布流水线侧运行,不占用 HarmonyOS 客户端权限,也不依赖某个尚未公开的端侧接口。它接收项目维护的发布清单,检查 public 产物存在、版本递增、文件摘要与预期值相同。清单中的值应由实际构建与审核流程填写,示例只描述格式。

{
  "product": "public",
  "versionName": "1.4.2",
  "versionCode": 10402,
  "previousVersionCode": 10401,
  "artifact": "./release/app-public.app",
  "expectedSha256": "由构建归档步骤写入64位十六进制摘要"
}
// scripts/verify-release.mjs;在 Node.js 18+ 环境执行
import { createHash } from 'node:crypto';
import { createReadStream, readFileSync, statSync } from 'node:fs';
import { resolve, dirname } from 'node:path';

function assert(condition, message) {
  if (!condition) throw new Error(message);
}

async function sha256(filePath) {
  const hash = createHash('sha256');
  for await (const chunk of createReadStream(filePath)) {
    hash.update(chunk);
  }
  return hash.digest('hex');
}

async function verify(manifestFile) {
  const cfg = JSON.parse(readFileSync(manifestFile, 'utf8'));
  assert(cfg.product === 'public', '禁止把内部产品当作正式发布包');
  assert(Number.isSafeInteger(cfg.versionCode), 'versionCode 必须是整数');
  assert(Number.isSafeInteger(cfg.previousVersionCode), '缺少已发布版本基线');
  assert(cfg.versionCode > cfg.previousVersionCode, '新版本号必须递增');
  assert(typeof cfg.versionName === 'string' && cfg.versionName.trim().length > 0,
    '缺少展示版本号');
  assert(typeof cfg.expectedSha256 === 'string' &&
    /^[0-9a-fA-F]{64}$/.test(cfg.expectedSha256), '摘要格式不正确');
  const artifact = resolve(dirname(manifestFile), cfg.artifact);
  assert(statSync(artifact).isFile(), '软件包文件不存在');
  const digest = await sha256(artifact);
  assert(digest === cfg.expectedSha256.toLowerCase(), '产物摘要与交付记录不一致');
  console.log(`交付摘要核对通过:${digest}`);
}

const file = process.argv[2];
if (!file) {
  console.error('用法:node scripts/verify-release.mjs manifest.json');
  process.exitCode = 2;
} else {
  verify(resolve(file)).catch((error) => {
    console.error(`交付检查失败:${error.message}`);
    process.exitCode = 1;
  });
}

这段代码刻意不尝试解析 .app 包的签名细节,因为签名证书、Profile、包名与发布环境的关系,应该用已核实的官方构建与签名流程检查。上述脚本属于内容一致性门禁,不是万能的包安全检测器。生产流水线还应从实际 app.json5 或构建产物元数据取得版本值,与清单交叉核对;否则单靠人工填写的两个整数仍有造假的空间。

脚本遇到路径错误、摘要不符或整数倒退会以非零状态结束,适合放在正式上传前。不要把密钥口令、证书私钥或真实账号凭据写进清单。若团队同时维护多架构、多渠道产物,应该为每个实际交付文件生成单独指纹,不要用其中一个包的摘要代表整批构建。

构建产物与发布门禁的校验流程

3. 灰度不是按钮,而是一份可停止的实验合同

华为 AppGallery Connect 的“分阶段发布”官方说明明确支持 HarmonyOS;它针对的是已有应用的版本更新,不适用于应用首次发布。开发者可以设置分发比例和生效时间,逐步放大覆盖面,也可以根据需要暂停、恢复或调整分阶段发布。这里应区分“平台有状态管理能力”和“程序可以任意调用某个自动回退 API”,两者不是同一件事。

真正的发布计划要同时写出观察窗口与触发条件。比如,登录成功率、核心任务完成率、启动异常趋势、关键设备与地区的反馈,都应有已确认的基线;如果还没有可靠基线,可以先安排内测与公开测试收集证据,不能随手填写漂亮的通过率。每次提高比例前应固定观察范围,避免同时修改服务端开关、数据库结构和客户端核心路径,否则发现问题后很难判断因果。

也不要把“审核通过”当成质量保证。审核面向合规与分发要求,而业务上的异常可能只发生在某种网络、某个历史数据库版本或特定的窗口形态。分阶段发布的价值,正在于让这些问题在有限范围内被观察到。出现持续性异常时,先记录当前版本、影响路径和监控证据,再暂停扩大覆盖,而不是先在群里喊一句“回退”。

4. 事故出现后的三条恢复路径

第一条是停止继续扩散。对于仍处于分阶段发布中的版本,先依据平台实际可用操作暂停进度,保留分发配置和异常证据,避免继续扩大受影响用户数量。暂停并不会自动卸载已经安装的新版本,因此现有受影响设备仍需要业务侧补救。

第二条是服务端缓解。如果问题集中在新接口、新功能开关或新数据格式,可以在预设的兼容边界内关闭有问题的功能、恢复兼容响应或者暂缓触发有风险的写入逻辑。这要求发布前就设计向后兼容的数据契约;在事故发生后临时改数据库字段的风险通常比关闭功能高得多。远程配置自身的历史版本回退与客户端 APP 版本回退也是不同概念,不能混用。

第三条是发布修复版本。修复版沿用已确定的应用身份和签名策略,并使用符合发布要求的新版本编号;代码内容可以恢复上一个稳定业务实现,但不宜把“重新安装较旧的低版本包”作为面向所有用户的常规发布方案。对于已有客户端写入的新数据,修复版仍需提供兼容读取或明确的数据迁移路径。特别是本地 Preferences 和数据库字段,不能假设代码一回退,数据就自动回到了旧结构。

分阶段发布中的暂停、缓解与修复版策略对照

这里可以设置一条简单的事故决策链:用户路径受损但可关闭功能时,先降级功能并观察;涉及数据损坏时,优先保护写入与备份;无法止损时暂停发布并准备修复包。对外公告、客服知识库与内部变更记录应使用同一版本编号,避免一线人员拿错处理方案。

5. 用一次故障演练检验制度是否真的能工作

发布前可以在测试环境构造三个场景:上传清单指向错误的 internal 包;签名验证通过但摘要与归档不一致;一批用户升级后服务端返回新旧端不兼容的字段。第一种应被 product 门禁拦截,第二种应被指纹核验拦截,第三种则应在逐步扩大覆盖之前被监测、暂停并处置。

演练时记录每一步由谁决策、需要什么证据、怎样回到安全服务状态,远比写一句“异常立即回滚”有效。对兼容性要求高的工程,还应分别检验“新客户端访问旧服务端”“旧客户端访问新服务端”,以及数据库升级后客户端旧版本访问现存数据的行为。任何发布策略都替代不了这些契约测试。

四篇构建与上架系列到这里形成闭环:C01 固定产品与目标配置,C02 确认签名关系,C03 保存准确的发布产物与审查证据,C04 再把版本编号、分阶段发布和事故止损组织起来。最值得保留的不是一份完美的检查表,而是每次发布都能回答:哪个包、什么版本、覆盖了谁、发现异常由谁叫停、如何让已有用户恢复服务。

参考资料:

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐