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

检查状态与发布资源清单

汇总状态之前先统一含义

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而允许检查过程中替换例外清单。清单在本篇表达字符串内容集合,若需要完整追溯作业依据,可以在构建系统外层关联源码提交和检查配置版本。

这项关联应由发布流水线保存,不把构建作业编号写进用户可见文案。应用资源负责产品展示,检查产物负责工程追踪,二者混在一起会让资源文本带上无关信息,也容易把内部路径泄露到正式界面。

失败后重新运行要从同一入口开始

修复参数或基线后,应重新执行统一入口,而不是只重跑之前失败的那一步再手工拼接结果。资源修复可能同时改变结构报告与修订差异,旧的通过结果也可能已经不再适用。统一入口重新采集内容,并生成对应本次检查的清单,使结果之间保持一致。

若构建系统缓存检查结果,缓存键至少应覆盖资源内容、合同、基线、例外及工具版本。仅按工程目录路径缓存会把不同修订混在一起。当前脚本每次实际读取和执行,优先保持判断简单;后续引入缓存时,也应保留相同的输入归属与失效条件。

从缺键、参数签名、模块归属到组合覆盖,再到修订审阅与统一构建入口,校对链路已经形成可追踪的发布前检查。每项规则保持自身含义,退出码传递处理结果,清单固定本次资源身份与内容,后续打包便有了清晰且可核对的输入。

相关资料:资源分类与访问。

Logo

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

更多推荐