HarmonyOS 构建退出码与发布资源清单(连载06)【共创稿事节】
多语资源的结构检查与修订审阅已经分别建立,但流水线如果只运行其中一个命令,仍可能把未完成的资源推到打包阶段。还有一种隐蔽错误:上次检查通过留下的清单仍在目录里,本次检查失败后,下游继续拿旧清单发布。校对工具需要统一退出码、清理旧输出,并把本次检查对应的资源内容明确保存下来。

汇总状态之前先统一含义
LocaleReview沿用前篇所有脚本。profile_audit.py负责模块归属、语言键集合、参数与指定组合覆盖,revision_check.py负责相对基线的内容差异及精确例外。本篇不把两套判断混成一套规则,而是用同一个入口顺序执行,再汇总结果。
约定零表示该项可继续,一表示存在待处理内容,二表示输入、合同或运行错误。汇总时输入错误优先于资源问题,资源问题优先于可继续。不能简单取最后一个命令的退出码,也不宜把多个状态相加;状态相加会产生没有定义的新值,让下游难以判断修复位置。
执行前移除当前指定输出路径的旧清单,执行成功后才写入新文件。清单是本次检查的产物,不是长期缓存。下游同时检查汇总退出码和文件是否存在,避免把仅有文件存在当成可发布的充分条件。
对现有脚本建立受控执行入口
新增release_steps.py,调用Python解释器与脚本绝对路径,不经过命令字符串拼接。每一步保留标准输出、标准错误和退出码。未知退出码统一转换为二,避免进程异常结束后被当成一种可接受业务状态。参数列表中的路径即使包含空格,也保持单独参数传递。
from pathlib import Path
from dataclasses import dataclass
import subprocess
import sys
@dataclass(frozen=True)
class StepResult:
name: str
code: int
output: str
error: str
def run_step(name: str, script: Path, arguments: list[str]) -> StepResult:
try:
process = subprocess.run([sys.executable, str(script), *arguments],
capture_output=True, text=True, timeout=120)
code = process.returncode if process.returncode in (0, 1, 2) else 2
return StepResult(name, code, process.stdout, process.stderr)
except (OSError, subprocess.TimeoutExpired) as error:
return StepResult(name, 2, "", str(error))
def aggregate(results: list[StepResult]) -> int:
if not results:
raise ValueError("检查步骤不能为空")
codes = [result.code for result in results]
return 2 if 2 in codes else (1 if any(codes) else 0)
超时应作为执行问题返回,并保留完整的错误信息;资源检查结果必须来自实际完成的步骤。保留错误文本有助于区分脚本缺失、输入文件错误和运行时间超限。超时阈值是本项目入口的配置选择,工程规模变化后可以调整,但应在流水线配置中保持一致。
两个步骤都执行完再汇总,可以在一次反馈中看到结构与修订问题。若产品要求遇到输入错误立即停止,也可以采用短路策略,但应明确下游哪些检查结果尚未产生,不能把没有结果的步骤显示成零。当前入口保留两项实际返回值。
清单绑定同一份资源内容
新增release_bundle.py,继续使用resource_revision的三元身份采集。检查前后分别读取字符串集合,若内容或身份发生变化,返回输入错误并暂缓写清单,要求使用稳定的源码工作区重新检查。这样可以发现检查过程中编辑资源导致的前后不一致。
清单记录每项模块、目录、键与文本摘要,不直接暴露整份业务文案。摘要采用UTF-8编码的SHA-256,并按三元身份排序,保证相同输入得到相同排列。摘要描述文件内容,不代替人工审阅,也不表示构建产物已经生成。
from pathlib import Path
import argparse
import hashlib
import json
import sys
from resource_scope import load_scopes
from resource_revision import Identity, collect_strings
from release_steps import StepResult, run_step, aggregate
def write_inventory(output: Path, current: dict[Identity, str]) -> None:
rows = [{"module": item[0], "folder": item[1], "key": item[2],
"sha256": hashlib.sha256(current[item].encode("utf-8")).hexdigest()}
for item in sorted(current)]
output.write_text(json.dumps({"format": 1, "strings": rows},
ensure_ascii=False, indent=2), encoding="utf-8")
def run_release(root: Path, contract: Path, baseline: Path,
output: Path, exceptions: Path | None) -> int:
protected = [contract.resolve(), baseline.resolve()]
if exceptions is not None:
protected.append(exceptions.resolve())
if output.resolve() in protected or output.resolve().is_relative_to(root.resolve()):
raise ValueError("输出应位于工程之外且不能覆盖检查输入")
output.unlink(missing_ok=True)
scopes, _ = load_scopes(root, contract)
before = collect_strings(scopes)
scripts = Path(__file__).resolve().parent
common = [str(root), str(contract)]
revision_args = ["check", *common, str(baseline)]
if exceptions is not None:
revision_args.extend(["--exceptions", str(exceptions)])
results: list[StepResult] = [
run_step("structure", scripts / "profile_audit.py", common),
run_step("revision", scripts / "revision_check.py", revision_args)
]
for result in results:
print(json.dumps({"step": result.name, "code": result.code,
"output": result.output, "error": result.error},
ensure_ascii=False))
codes = [result.code for result in results]
code = 2 if 2 in codes else (1 if any(codes) else 0)
current = collect_strings(scopes)
if current != before:
raise ValueError("检查期间资源发生变化,请重新检查")
if code == 0:
write_inventory(output, current)
return code
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("root", type=Path)
parser.add_argument("contract", type=Path)
parser.add_argument("baseline", type=Path)
parser.add_argument("output", type=Path)
parser.add_argument("--exceptions", type=Path)
args = parser.parse_args()
try:
return run_release(args.root, args.contract, args.baseline,
args.output, args.exceptions)
except (OSError, ValueError) as error:
print(str(error), file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
输出路径必须放在工程之外,也不能覆盖合同、基线或例外输入。入口只清理明确指定的单个输出,不遍历删除目录。选择路径时应把检查输入与构建输出分别管理,避免一个便利的相对路径意外覆盖了源资源。
前后采集是对源码变化的额外检查,稳定发布仍应采用固定修订的工作区,并让检查与后续打包使用同一份源码。若进程在两次读取之间修改后又恢复原文,前后集合可以相等;构建隔离应由流水线工作区管理完成,不能仅依赖这次比较。
命令入口保留真实退出码
下面保存为check_release.sh,参数顺序为工程、合同、基线、清单输出;其余参数继续传给Python入口。脚本目录通过自身位置确定,Python脚本与此前各模块放在同一工具目录。shell只负责执行并原样返回状态,不解析中文报告文本。
#!/usr/bin/env bash
set -u
if [ "$#" -lt 4 ]; then
echo '需要工程、合同、基线和输出路径' >&2
exit 2
fi
script_dir="$(cd -- "$(dirname -- "$0")" && pwd)" || exit 2
python3 "$script_dir/release_bundle.py" "$@"
result=$?
exit "$result"
在构建作业中,把这个命令设为打包前置步骤,并让非零状态阻断后续作业。不要在命令后接一个总会返回零的输出语句,也不要用忽略错误配置掩盖检查失败。流水线界面可以保留原始报告,但判定应读取进程状态而不是搜索“可继续”这样的文本。

清单页面只呈现已取得的结果
移动端校对台可以展示作业返回的结构状态、修订状态及清单条目。以下完整页面是两项状态均为零的固定样例,按钮展开条目的归属信息。主机文件采集仍由上述工具执行,页面不通过按钮自行宣布构建完成。
@Entry
@Component
struct Index {
@State expanded: boolean = false;
build() {
Column({ space: 18 }) {
Text('多语发布校对').fontSize(26).fontWeight(FontWeight.Bold)
Text('发布资源清单').fontSize(20)
Text('结构检查 0').fontSize(18)
Text('修订检查 0').fontSize(18)
Text('可继续').fontSize(22).fontColor('#187A57')
Text('entry / en_US / notice').fontSize(18)
Text('按模块与目录归档').fontSize(16)
if (this.expanded) {
Text('模块:entry;目录:en_US;键:notice').fontSize(16)
}
Button('查看资源').width('100%').onClick(() => { this.expanded = !this.expanded; })
}.width('100%').height('100%').padding(24).backgroundColor('#FFFFFF')
}
}

从失败状态反推修复位置
将普通英文notice改成参数类型错误,结构检查应返回一;即使为同一次文字变化配置精确例外,汇总仍应阻断。再把基线文件写坏,修订检查返回二,汇总优先返回二。两个场景可以检验例外是否意外绕过结构规则,以及输入错误优先级是否正确。
旧清单的处理也要单独检查。先运行通过的输入生成清单,再制造缺键后运行,预期旧文件被移除且没有新文件。若失败后旧文件仍在,下游容易把之前的摘要当成本次资源。检查过程出错时,应先修复问题并重新运行,不手工复制旧输出补齐流水线。
最后对同一份资源重复运行,按身份排序后的清单应保持一致。只改一个资源文本时,对应条目的摘要应改变,其余条目保持;只交换源码JSON中的条目顺序,不应改变清单排列。这样才能让版本比较聚焦实际资源变化,而不是文件系统遍历顺序。
输出清单与输入合同各自版本化
清单格式使用独立的format字段,新增摘要算法或资源类型时应明确约定兼容方式。下游读到未知格式要停止处理,不能按旧字段猜测内容。资源目录和键名保持源码中的准确大小写,摘要则只针对文本值,身份变化会以新增和删除条目体现。
合同、基线和例外也会影响检查结论。固定源码工作区时,应把这些输入一起纳入相同修订,不能只冻结string.json而允许检查过程中替换例外清单。清单在本篇表达字符串内容集合,若需要完整追溯作业依据,可以在构建系统外层关联源码提交和检查配置版本。
这项关联应由发布流水线保存,不把构建作业编号写进用户可见文案。应用资源负责产品展示,检查产物负责工程追踪,二者混在一起会让资源文本带上无关信息,也容易把内部路径泄露到正式界面。
失败后重新运行要从同一入口开始
修复参数或基线后,应重新执行统一入口,而不是只重跑之前失败的那一步再手工拼接结果。资源修复可能同时改变结构报告与修订差异,旧的通过结果也可能已经不再适用。统一入口重新采集内容,并生成对应本次检查的清单,使结果之间保持一致。
若构建系统缓存检查结果,缓存键至少应覆盖资源内容、合同、基线、例外及工具版本。仅按工程目录路径缓存会把不同修订混在一起。当前脚本每次实际读取和执行,优先保持判断简单;后续引入缓存时,也应保留相同的输入归属与失效条件。
从缺键、参数签名、模块归属到组合覆盖,再到修订审阅与统一构建入口,校对链路已经形成可追踪的发布前检查。每项规则保持自身含义,退出码传递处理结果,清单固定本次资源身份与内容,后续打包便有了清晰且可核对的输入。
相关资料:资源分类与访问。
更多推荐

所有评论(0)