构建工具输出了一个安装包,距离“可以上架”还有多远?答案并不是一次打包命令的距离。多产品工程容易出现这样的错位:用于内测的配置被带进公开发布包,应用介绍中的隐私说明与代码实际权限不一致,审核人员拿到的包又不是功能测试时使用的那一份。构建行为可以自动化,但发布决策必须把产物、配置、材料和人工复核放到同一条链路中。

技术封面

前两篇已经把 internal、public 产品以及签名配置关联起来。本篇不再重复 Hvigor 的构建命令,而是讨论一个更靠近交付的环节:如何让同一产品的构建产物具有可追溯的文件指纹,并在提交应用市场前,对每项证据建立“已完成、待复核、阻断”的判定。目标不是替代应用市场审核,也不靠编造一个自动审核 API 来完成所有工作,而是把容易遗漏的发布动作变成可执行的检查。

从“打出包”到“允许交付”:为什么要分层

将发布检查分成四个不同性质的关口更稳妥。第一关检查本地构建输出的存在性与类型;第二关检查文件摘要以及项目声明的版本、产品标识是否符合发布计划;第三关核对签名配置、权限用途、隐私政策、截图和应用介绍;第四关由提交人将经过核对的软件包和材料上传到 AppGallery Connect。前两关适合代码校验,后两关涉及账户授权、业务说明与审核规则,仍需要人工确认。

四道发布关口与证据流向

关于产物格式,需要区分构建阶段的 HAP、打包阶段的 APP,以及面向特定安装调试场景的其他输出。不能因为某个目录里出现 .hap 就认为它必然是当前发布页面要求上传的文件。本例以选择已经准备好的 .app 文件为输入,只检查本地目标文件的存在性与摘要;实际提交时仍以当期应用市场后台提示的类型及要求为准。这样可以避免把“本地成功生成”偷换成“平台验收通过”。

哈希也有明确边界:SHA-256 能帮助团队确认测试、归档、提交过程中使用的是不是同一份文件字节,但它不会检验数字签名是否被信任,不能代替证书链验证、Profile 匹配、BundleName 核验和设备安装检查。上一节的签名排障结论仍然有效,建议由工程检查表单独留痕。

用 Node.js 对软件包生成确定性的交付记录

不依赖内部插件,直接利用 Node.js 标准库就能完成这层可重复的校验。约定发布员先在自己的终端为公开产品构建包,接着显式指定预期产物、版本和目标产品。不要在脚本里扫描整个输出目录并“自动挑最新文件”,否则临时包与发布包相邻时,很容易把选择错误隐藏起来。

下面的 release-gate.mjs 可以放到工程中的 tools/ 目录。代码接受三个参数:产物文件路径、产品名称和本次计划版本号;对文件路径、扩展名、空文件以及产品名做基本检查,并返回机器可读的 JSON。异常时设置非零退出码,方便接入现有 CI 流程。

// tools/release-gate.mjs  Node.js 18+
import { createReadStream } from 'node:fs';
import { stat } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { basename, extname, resolve } from 'node:path';

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

async function run() {
  const [fileArg, product, version] = process.argv.slice(2);
  if (!fileArg || !product || !version) {
    throw new Error('用法: node tools/release-gate.mjs <包路径> <产品> <版本>');
  }
  if (!['public', 'internal'].includes(product)) {
    throw new Error('产品必须为 public 或 internal');
  }
  if (!/^\d+\.\d+\.\d+$/.test(version)) {
    throw new Error('版本号采用主版本.次版本.修订号格式');
  }
  const file = resolve(fileArg);
  const info = await stat(file);
  if (!info.isFile() || info.size === 0) {
    throw new Error('指定产物不存在、不是普通文件或为空');
  }
  if (extname(file).toLowerCase() !== '.app') {
    throw new Error('当前交付流程要求选择 APP 软件包');
  }
  return {
    product,
    plannedVersion: version,
    packageName: basename(file),
    bytes: info.size,
    sha256: await sha256(file)
  };
}

run().then(result => {
  process.stdout.write(JSON.stringify(result, null, 2) + '\n');
}).catch(error => {
  process.stderr.write(String(error.message) + '\n');
  process.exitCode = 1;
});

调用方式是 node tools/release-gate.mjs <实际APP路径> public 1.2.0。输出值里的 plannedVersion 是操作员输入的发布计划,并非脚本从 APP 包内部提取出来的真实版本;product 也只是任务参数,不是对包内产品身份的认证。二者必须继续与实际工程配置及上传页面显示的信息交叉核对。这样的命名虽然不如“自动验证版本”漂亮,却能避免给审核人错误的确定性。

这里用流式读取避免把整个软件包一次性载入内存。文件越大,流式摘要越能保持稳定的内存占用。还要注意,文件可能在摘要计算过程中被其他进程改写,所以构建完成后应固定产物、禁止并行覆盖,再执行摘要与归档,最终提交前可重新计算一次以确认未发生变化。

发布证据怎样避免变成一份形式清单

代码关口完成之后,建议以“检查对象—依据—责任人—处理状态”记录人工关口。签名证据至少包括:签名配置名称与公开产品映射是否一致、证书与 Profile 是否与实际发布主体一致、BundleName 是否与发布记录一致。请勿将 .p12、私钥口令、Profile 原件或其他敏感资料打进文章素材包,也不要把密钥记录在工单截图中。

隐私和权限证据应针对应用当前实现逐项描述,而不是引用一个通用模板。比如示例应用如果收集设备定位,就需要结合实际申请权限的用途、收集信息范围和隐私政策说明逐项核对;如果代码没有调用定位能力,就不应为了显示“权限检查齐全”而额外申请权限。引入三方 SDK 时,还需检查其数据处理情况是否在适用的隐私披露中体现。此类差异靠静态扫描无法完全说明,需要产品与合规责任人确认。

发布差异的红笔核对示意

交付记录应包含实际软件包名称、SHA-256、目标版本、检查责任人、证据日期,以及准备上传的渠道与国家地区。上传后的页面信息也需要和记录比对。假设市场后台显示的版本与工单不一致,处理路径是暂停提交、返回构建记录定位差异,而不是改写工单来配合后台结果。

如何让门禁真正挡住有风险的包

推荐采用三级规则:可自动验证的“缺包、空包、错误扩展名、摘要变化”直接阻断;需要资料核对的“截图、隐私政策、签名对应、权限说明”只有责任人确认后放行;不影响交付一致性的“文案优化建议”进入待办,不应冒充阻断项。规则必须写得足够具体,不能用“质量良好”“看起来正常”这样的模糊描述充当证据。

设计演练可覆盖五个输入:指定路径不存在;文件为零字节;误选 .hap 作为本流程输入;同一 APP 重新计算得到不同摘要;全部自动项通过但隐私材料未签字。前三类应被脚本拒绝,第四类由两份记录对比后阻断,第五类必须在人工门禁停下。这样的验证不需要伪造性能数字,也不会把程序验证与应用市场审核混为一谈。

即使所有本地门禁都完成,提交应用市场后仍可能因为平台规则、素材质量或账户资质被退回。流程必须保留退回后的“原因—责任人—修订版本—重新核对”闭环,并确保修订后的包获得新的摘要记录。当期发布流程以华为官方页面和开发者中心的实际要求为准,不要把某次操作截图写成长期不变的菜单路径。

连载最后一篇将处理版本迭代与回退:一个已经交付的版本如何定位原始包、如何判断修复后能否覆盖既有计划、如何确保开发分支与已发布包的来源对应。在进入版本管理之前,最重要的基础是让每份软件包的来源和用途都能被解释清楚。

官方参考:华为应用市场上架流程、AppGallery Connect、DevEco Studio签名排查。

Logo

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

更多推荐