本地用 DevEco Studio 点一下 Build,三秒就出包了。推到 CI 流水线,直接红。最开始以为是网络问题,重试了几次,还是红。翻 CI 日志一看,报错信息和本地完全不一样——本地根本不会报这个错。

这种问题最磨人:代码是你的,本地跑得好好的,一到 CI 就挂。而且每次排查都像开盲盒,这次是 SDK 版本不对,下次是签名文件找不到,再下次是依赖缓存出了问题。后来花了时间把环境统一、配置锁死、检查规则写进脚本,CI 才真正稳下来。

这篇复盘一次真实的 CI 构建事故,讲讲排查顺序、常见坑和怎么把一次性修复变成长期规则。

一、本地正常,CI 第一次就失败

第一次接 CI 的时候,我以为把代码推上去,流水线自动跑一下 build 命令就行了。结果第一次跑就挂了,报错是 SDK 版本不匹配。

本地 DevEco Studio 用的是我机器上装的 SDK,CI 环境里是另一个版本。本地的 node 版本和 CI 镜像里的也不一样。这两个不一样,构建结果当然不一样。

这个阶段最容易犯的错是:看到 CI 报错就去改代码。但本地明明能编译,改代码有什么用?问题根本不在代码,在环境。先把环境对齐了再说,别一上来就动工程配置。

二、先排环境,再排工程配置

排查顺序很重要,先从最外层的环境开始,逐层往里走。

第一步看 CI 镜像里的 Node、Hvigor、SDK 版本和本地 DevEco Studio 用的是不是一致。DevEco Studio 自带了一套 Node 和 Hvigor,但 CI 环境通常用全局安装的版本,两边对不上就会出各种莫名其妙的错误。

第二步看环境变量。本地 DevEco Studio 构建时会自动设置一些环境变量,比如 SDK 路径、签名文件路径这些。CI 环境里这些变量得手动配,漏了任何一个都会构建失败。我之前遇到过一次,CI 日志报"找不到签名文件",查了半天才发现是环境变量里的 KEYSTORE_PATH 没配对。

第三步看依赖。CI 上拉的依赖版本和本地 lock 文件里的是不是一致。如果 oh-package.json5 里用了范围版本号(比如 ^1.0.0),CI 上每次拉到的小版本可能不一样,今天能跑明天就挂。

这个排查顺序的核心思路是:本地能跑说明代码本身没问题,问题一定出在本地和 CI 的差异上。先把所有外部依赖的差异找出来,再去看工程配置。

三、依赖锁定和 Hvigor 参数为什么必须统一

环境版本对齐以后,下一个坑是依赖和构建参数。

依赖一定要锁定。oh-package-lock.json5 必须提交到仓库里,CI 构建时必须用 `--frozen-lockfile` 之类的参数确保不自动升级依赖。不锁版本的话,CI 上每次构建拉到的依赖版本都可能不同,出了问题根本没法复现。

Hvigor 的构建参数也要统一。本地 DevEco Studio 点 Build 时用的参数,和 CI 命令行跑的参数可能不一样。比如本地默认是 Debug 构建,CI 可能默认 Release;本地开了增量编译,CI 上可能是全量。这些差异都会导致构建结果不同。

下面这段配置放在 build-profile.json5 里,统一 Debug 和 Release 的构建参数。关键是把签名配置和混淆开关在这里集中管理,不要散落在多个地方。

{
  "app": {
    "signingConfigs": [
      {
        "name": "default",
        "type": "HarmonyOS",
        "material": {
          "certpath": "./cert/release-cert.p12",
          "storePassword": "store-password-placeholder",
          "keyAlias": "debugKey",
          "keyPassword": "key-password-placeholder",
          "profile": "./cert/release-profile.p7b"
        }
      }
    ],
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "compatibleSdkVersion": "5.0.0(12)",
        "runtimeOS": "HarmonyOS"
      }
    ]
  },
  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry",
      "targets": [
        {
          "name": "default",
          "applyToProducts": ["default"]
        }
      ]
    }
  ]
}

这段配置要解决的问题:签名路径和 SDK 版本在这里统一声明,CI 和本地读同一份配置,不会出现本地用 DevEco 自动签名、CI 上找不到签名文件的情况。

实际使用时要注意:certpath 里的证书文件不能提交到公开仓库,应该放在 CI 的密钥管理里,构建时动态写入。上面代码里的 storePassword 和 keyPassword 是占位符,真实项目里应该从环境变量读取,不能硬编码在配置文件里。compatibleSdkVersion 要和 CI 镜像里安装的 SDK 版本对应,版本号写错了 CI 就会报 SDK 不匹配。

四、签名和 Debug/Release 差异怎么处理

签名是 CI 上最容易出问题的环节。本地 DevEco Studio 会自动用调试签名构建,所以本地 Build 从来不报签名错误。但 CI 上跑 Release 构建的时候,需要正式签名证书,证书没配好就直接挂。

Debug 和 Release 的差异还要注意:混淆开关、资源压缩、代码优化这些,Release 下通常是开的,Debug 下是关的。Release 开了混淆以后,某些反射调用会出问题——本地 Debug 跑着没问题,CI Release 一编就报错。这种问题排查起来特别花时间,因为你本地默认跑的是 Debug,复现不了。

我的做法是在 CI 上同时跑 Debug 和 Release 两次构建,确保两种模式都能过。这样 Release 模式下的问题在 CI 上就能暴露出来,不用等发到应用市场才发现。

五、最后把一次性修复变成长期构建规则

每次 CI 挂了以后手动排查一遍,修好了下次又出别的问题。后来把排查过程中确认有效的检查项写进了 CI 脚本里,每次构建自动跑一遍,问题在出包之前就拦住了。

脚本里做了这几件事:检查 Node 和 Hvigor 版本是否符合要求;检查 lock 文件是否最新;构建前清理旧缓存;Debug 和 Release 分别构建一次;构建完成后检查输出 hap 文件是否存在。

下面这段是 CI 构建脚本的核心部分,放在 pipeline 的 build 阶段。它先做环境检查,再用锁定的依赖构建,最后验证产出物。

#!/bin/bash
set -e

echo "=== Environment Check ==="
node -v
hvigorw --version

echo "=== Install Dependencies (frozen lockfile) ==="
ohpm install --frozen-lockfile

echo "=== Build Debug ==="
hvigorw assembleHap --mode project -p product=default -p buildMode=debug

echo "=== Build Release ==="
hvigorw assembleHap --mode project -p product=default -p buildMode=release

echo "=== Verify Output ==="
ls -la entry/build/default/outputs/default/*.hap
echo "=== Build Done ==="

这段脚本要解决的问题:每次 CI 构建都用同样的流程——冻结依赖版本、分别构建 Debug 和 Release、最后验证产出物存在。环境检查的输出会打进 CI 日志,出问题时一眼就能看到版本对不对。

实际使用时要注意:--frozen-lockfile 是关键,如果 lock 文件和 oh-package.json5 不一致它会直接报错,强制开发者同步更新。hvigorw 的具体参数需要对照当前版本的命令行文档,不同版本参数可能有差异。产出物验证那一步不能省——有时候构建"成功"了但输出目录里没有 hap 文件,这种问题只有检查了才知道。

CI 构建这件事,本质上是在把"我本地能跑"变成"任何人在任何环境都能跑"。环境对齐、依赖锁定、参数统一、自动检查,做到这几点以后,CI 才不会天天出幺蛾子。

Logo

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

更多推荐