☰
Flutter鸿蒙发布流水线改造:flutter_release_x适配与CI/CD实践
2026/10/10 3:26:26 网站建设 项目流程

做 Flutter 的兄弟应该都有同感:把应用跑到鸿蒙上不算最难,真正头疼的是发布链路。之前我们用 flutter_release_x 管版本号、自动生成 CHANGELOG、打 Git tag、汇总 release notes,整个发布流水线跑得挺顺。一转到鸿蒙侧,问题立刻暴露:Gradle 体系不认了,APK 产物也没有了,原来那套发布脚本集体歇菜。这篇文章我就把 flutter_release_x 的鸿蒙化适配思路、CI/CD 流水线改造过程,以及踩过的坑完整拆开讲一遍,给正在做 Flutter 鸿蒙迁移的团队一个可以直接抄作业的参考。

这个内容适合两类人:一类是 Flutter 工程负责人,正在头疼鸿蒙侧构建产物和版本号管理;另一类是做 CI/CD 治理的研发效能工程师,需要把原有发布流水线平移到鸿蒙工具链。我会从整体设计讲到环境准备,再讲到核心代码改动和 DevOps 流水线搭建,最后是问题排查速查表,基本上覆盖适配全流程。

1. 内容整体设计与思路拆解

1.1 flutter_release_x 到底帮你做了什么

flutter_release_x 这个库,本质上是一个“发布流水线资产管理工具”。它把 Flutter 项目发布过程中最繁琐、最容易出人命的环节全部自动化了。我拆开来看,它主要管四件事。

第一件事是版本号自动递增。以前我们发版本靠人肉改 pubspec.yaml,哪天忘了改或者两个人同时改了,最后发出去的包版本号就是错的,线上问题排查直接抓瞎。flutter_release_x 支持语义化版本规则,执行一条命令就自动完成 major/minor/patch 的递增,同时把 build number 一起处理掉。

第二件事是 CHANGELOG 自动生成。它基于 Conventional Commits 规范,从 Git 提交记录里把 feat、fix、breaking change 这些类型的 commit 拉出来,按版本区间整理成 changelog 内容,格式统一、不需要编辑人肉回忆“这个版本到底改了什么”。

第三件事是 Git tag 与 release notes 生成。每次发版,它负责把版本号打到 tag 上,把 CHANGELOG、构建信息、产物清单汇总成 release notes,方便同步给 QA、运营或者客户。

第四件事是构建产物规整。它能把构建出来的 APK、AAB 甚至 IPA 等产物收集到统一的目录,按“应用名-版本号-构建号”的规范重命名,再连同 checksum 文件一起归档。发布资产整洁不乱,后面要回溯哪个包是谁在什么时候出的,一目了然。

这四件事在纯 Android/iOS 时代已经跑得很成熟了。但问题就在于,鸿蒙的构建体系完全不一样,这就引出了适配的核心命题。

1.2 鸿蒙化适配的本质:换胶水,不换内核

有人听到“鸿蒙化适配”,第一反应是把 Flutter 代码用 ArkTS 重写一遍。这是误解。无论底层是 OpenHarmony 的 Flutter 引擎还是其他渲染方案,你的 Dart 业务代码、状态管理、组件树都是可以原样保留的。真正要动的是 flutter_release_x 这个工具库里面的“平台胶水层”。

平台胶水层包括几个方面:构建命令从 gradle 换成了 hvigorw;产物类型从 APK/AAB 变成了 HAP;版本号写入的目标文件从 android/build.gradle 变成了 AppScope/app.json5;签名机制从 Android keystore 换成了鸿蒙的 hap-sign-tool 证书体系;还有 CI 里的环境变量、缓存目录、路径规则全部要跟着变。

换句话说,flutter_release_x 的 Dart 核心逻辑——版本解析、版本递增、Git 操作、CHANGELOG 数据提取——都是纯 Dart 实现,本身跨平台没有依赖。我们只需要把“通过配置路径定位构建文件并注入版本号”“执行构建命令并收集产物”这两个平台相关模块替换成鸿蒙实现,就能完成大部分适配。

所以整个适配工作可以分三条线并行:核心逻辑不动、平台层替换、流水线重写。这也是我在设计时定的一个原则:坚决不 fork 一套只能给鸿蒙用的分支,而是在主代码里用抽象接口隔离平台差异。后面接入新平台时,只需要新增一个 platform 适配器,主流程一行不改。

1.3 为什么选“改库”而不是“旁路脚本”

也有人问我,既然 flutter_release_x 影响面这么大,为什么不干脆在外围写一套鸿蒙发布脚本,继续用原库管 Flutter 侧,管鸿蒙的部分单独开一条流水线?我试过这个思路,最后放弃了,原因是太容易撕裂。

如果旁路脚本,就意味着版本号有两处事实来源:Flutter 侧的 pubspec.yaml 和鸿蒙侧的 app.json5 各有一份。旁路脚本从 pubspec.yaml 读出版本号,再写进 app.json5,逻辑上似乎可行,但多了一个同步环节,就意味着多一个可能失配的点。一旦两边版本号出现偏差,你根本说不清线上那个 HAP 包里的版本是哪个。flutter_release_x 原本最大的价值就是“单一事实源”,所有版本资产从同一个地方派生,这个价值在鸿蒙侧不能丢。

另一个原因更现实:旁路脚本的 CI 逻辑和主流水线是割裂的,参与维护的人要同时维护两套系统的约定,理解成本陡增。而直接在 flutter_release_x 里加一个 harmony 平台适配器,所有命令入口、配置文件路径、产物收集规则都集中在同一个 CLI 里,团队学习成本低,后续扩展也顺。至少在我们团队里,这个选型是经过三周线上实际跑量的,稳定性完全能打。

2. 核心细节解析与实操要点

2.1 版本号机制差异:pubspec.yaml 与 app.json5 的统一策略

先说鸿蒙侧版本号是怎么存的。在 DevEco Studio 工程里,版本信息集中在 AppScope 目录下的 app.json5 文件中,结构大概是这样的:

{ "app": { "bundleName": "com.example.demo", "versionCode": 1000000, "versionName": "1.0.0" } }

注意几个关键点:versionCode 是纯数字,官方推荐用七位数表示大版本、小版本、补丁版本,比如 1.2.3 对应 1002003;versionName 是展示用的字符串,规则跟版本号一致。而 Flutter 侧 pubspec.yaml 里的 version 字段格式是这样的:

version: 1.2.3+4

加号前面是语义化版本号,后面是 build number。这套格式和鸿蒙的 versionCode 规则完全不是一码事,所以适配的时候必须做格式转换。

我采用的策略是:保留 pubspec.yaml 作为唯一事实源,flutter_release_x 生成版本增量之后,先把语义化版本号解析出来,再把 build number 映射到 versionCode 的高位或者低位。具体来说,versionCode 的计算规则可以按官方推荐的七位数来,也可以按照自己团队的规则来。我们的规则是前三位大版本、中间三位小版本、最后一位补丁版本,加上 build number 放在 versionName 的 + 号后面。这样每个 HAP 包都能在系统中明确展示完整版本信息,也方便运营侧辨认。

这里有个很容易踩的坑:app.json5 不是标准的 JSON 文件,它支持注释和尾逗号,如果你用普通的 JSON 解析库去读,大概率会抛异常。flutter_release_x 原库处理 JSON 用的是 json_serializable 那套,直接套到 app.json5 上会翻车。我们最终在适配层专门封装了一个 json5 解析模块,基于 Dart 的 json5 包来做,才能稳定读写这个文件。

2.2 CHANGELOG 与 release notes 的鸿蒙化细节

CHANGELOG 生成逻辑基本不用改,因为它是从 Git 提交记录里提取结构化信息的,和平台没有关系。但 release notes 的内容组织方式需要调整。

原来的 release notes 会列出 APK 的下载地址、包大小、MD5、构建时间。鸿蒙化之后,这些字段要替换成 HAP 包的对应信息,同时要补充鸿蒙专属的元数据,比如 target API version、签名证书的指纹信息、以及它是 universal HAP 还是按设备形态拆分的 HAP。我们在适配时给 release notes 模板新增了一个 harmony 段落,里面专门放这些字段。

模板定下来之后,本质上就是字符串拼接逻辑,改起来很快。但有一个细节值得提醒:release notes 里建议不要只放产物清单,要把版本间的 diff 概要也带上,也就是从上一个 tag 到现在,改了哪些 feature、修了哪些 bug、有没有 breaking change。这样流水线的下游消费方——不管是人工审核还是自动发布系统——都能一眼判断这个包能不能上。

2.3 Git tag 生命周期:跨平台产物用同一套 tag

因为 Flutter 业务代码和鸿蒙外层工程通常在一个仓库里维护,所以 Git tag 的规则不用区分平台,继续沿用一个版本号对应一个 tag 的做法。但这里有个新增的复杂度:鸿蒙工程的 hvigor 构建配置、签名文件和模块配置通常都放在 .harmony 目录下,这些文件在构建时是会被 hvigorw 读取的,所以它们必须跟着 Flutter 代码一起进 tag。

我们在原有的 release 流程里额外加了一条校验:打 tag 之前,先检查 .harmony/AppScope/app.json5 是否存在、versionName 是否等于 pubspec.yaml 里的版本号、签名材料目录是否存在。这三项任一不满足,直接终止发布,防止发出一个“版本号对不上”的包。

这个校验逻辑看起来简单,但实际救了我们两次。有一次同事改了 pubspec.yaml 的版本号,但忘了同步 app.json5,流水线在打 tag 阶段直接报错拦住了,避免了在线上出现两个内容不一致的版本资产。

2.4 CI/CD 治理的本质差异:从 Gradle 到 hvigor 的心智迁移

鸿蒙侧的工具链和 Android 完全是两套心智模型。Android 是 Gradle + AGP,鸿蒙是 hvigor + DevEco 命令行工具。如果你只是把 CI 里的./gradlew assembleRelease换成一句hvigorw assembleHap,那太天真了,后面还有一堆坑。

hvigor 的构建脚本是 .js 或 .ts 后缀的 hvigorfile,构建配置依赖 oh-package.json5 管理。它有自己的依赖仓库,不会去读 Maven 或 pub.dev。这意味着在 CI 里构建鸿蒙产物时,需要保证 ohpm 仓库源可用,并且依赖缓存要单独建目录,不能和 pub 缓存混在一起。

另外,hvigorw 命令本身是一个包装脚本,从 DevEco Studio 安装目录里能找到。在 CI 环境里,最好把 DevEco 的命令行工具单独装到一个专用目录,配好DEVECO_SDK_HOME和OHOS_SDK_HOME两个环境变量,这两个环境变量分别指向 DevEco 工具目录和 OpenHarmony SDK 目录。很多 CI 里构建失败,原因都是这两个变量没配对。

所以我在设计 CI/CD 改造时,没有在原 Jenkinsfile 上东改一块西改一块,而是以 flutter_release_x 的 harmony 适配器为核心,重写了一套流水线,把构建、签名、归档、发布四个阶段全部显式拆分。后面我展开讲具体实现。

3. 实操过程与核心环节实现

3.1 环境准备:DevEco 命令行工具链安装

先明确一件事:flutter_release_x 本身是 Dart 包,跑在本地或 CI 里都行,但它的 harmony 适配器要能工作,前提是机器上有完整的鸿蒙构建环境。

我在 macOS 上的做法是:先装 DevEco Studio,然后在安装目录下的sdk和tools子目录里找命令行工具。关键的可执行文件有hvigorw(构建驱动)、ohpm(鸿蒙包管理器)、hapsigntool(签名工具)。这三个工具建议单独做个软链接,放进/usr/local/bin,方便在 Flutter 插件里直接调用。

环境变量配置建议放到 CI 的全局环境里:

export DEVECO_SDK_HOME=/opt/deveco/sdk export OHOS_SDK_HOME=/opt/deveco/sdk export PATH=$PATH:/opt/deveco/command-line-tools/bin

这里有个经验之谈:不要用 DevEco Studio 图形界面里自动配置的环境变量去套 CI。图形界面下 DevEco 会启动自己的 shell 环境,但 CI 的干净环境里少了这些配置,执行 hvigorw 时经常报“CommandNotFoundException”之类的错。我的做法是在流水线环境准备阶段专门加一个 step,显式 export 这些变量,并且用which hvigorw && hvigorw --version做一次前置探测,确保工具链可用再继续往下走。

3.2 版本号写入实现:从 build.gradle 切换到 app.json5

原版 flutter_release_x 更新版本号时,要定位到 android/build.gradle 里的 versionName 和 versionCode 字段,用正则替换。鸿蒙适配器改成读写 AppScope/app.json5。

我封装了一个 HarmonyVersionWriter 类,核心逻辑是这样的:

import 'package:json5/json5.dart'; class HarmonyVersionWriter { final String appJson5Path; Future<void> write(String versionName, int versionCode) async { final source = await File(appJson5Path).readAsString(); // json5 解析,保留注释和尾逗号 final data = Json5.decode(source) as Map<String, dynamic>; final app = data['app'] as Map<String, dynamic>; app['versionName'] = versionName; app['versionCode'] = versionCode; final updated = Json5.encode(data); await File(appJson5Path).writeAsString(updated); } }

这里强调几个细节:首先是Json5 的 encode 结果会非常紧凑,把原来 json5 文件里的注释清掉。这不算致命问题,但为了保险,我们建议在写入后单独跑一次hvigorw的配置解析命令验证文件没问题。其次,versionCode 的计算我放在了专门的工具函数里,避免在业务逻辑里散落魔法数字。

我给 versionCode 的映射规则是:大版本号乘一万,小版本号乘一百,补丁版本号乘一,再加上 build number 的低两位。比如 1.2.3+4 映射成 1020304。这套规则不一定适合所有团队,但关键是:规则一旦定了,就要在 flutter_release_x 的配置文件中显式声明,不要靠人肉记忆。

3.3 构建命令替换与产物收集

原版 flutter_release_x 的 Android 构建动作是执行./gradlew assembleRelease,产物路径在app/build/outputs/apk/release/。鸿蒙适配器换成执行:

hvigorw assembleHap --mode module -p product=release

具体参数要根据工程结构来。如果你用的是 DevEco 默认的工程结构,产物一般在entry/build/default/outputs/default/entry-default-signed.hap或者类似目录下。产物路径不要写死,最好通过扫描目录里的*.hap文件来定位,因为不同大版本 DevEco 输出的路径规则有变化。

在 flutter_release_x 的 harmony 适配器里,我实现了一个collectHarmonyArtifacts方法,逻辑是:

  1. 执行 hvigorw 构建,超时时间设为 15 分钟;
  2. 递归扫描构建输出目录,匹配*.hap文件;
  3. 按修改时间排序,取最新的那个作为主产物;
  4. 把产物复制到统一的发布目录,重命名为应用名-版本号-build号.hap;
  5. 计算 SHA256 并写入.sha256文件。

这一步搞定了,后面 release notes 里的下载地址和校验值就有数据来源了。

3.4 签名材料管理:鸿蒙的证书体系

鸿蒙应用签名比 Android 敏感得多。它需要四类文件:.p12私钥库、.cer证书文件、.p7bprofile 文件,以及一个material目录里放签名素材。签名操作一般由 DevEco 的自动签名流程完成,但 CI 环境里没有 IDE,必须手动调用。

我在 flutter_release_x 的新增命令里实现了一个signHarmonyHap选项,它接受四个参数:私钥库路径、证书路径、profile 路径、输出路径。底层执行的是 hapsigntool 的命令行接口,大致如下:

hapsigntool sign -privateKey material/xxx.p12 \ -certChain material/xxx.cer -profile material/xxx.p7b \ -inApp entry-default-unsigned.hap \ -out entry-default-signed.hap

签名环节有两点特别值得注意:一是私钥库的密码不能明文出现在流水线日志里,建议放到 CI 的 secret 变量中,执行时通过环境变量注入;二是签名之后必须做一次验签,确认 profile 对应的 bundleName 和 app.json5 里的 bundleName 一致,否则装到真机上会直接失败,而且报错信息相当隐晦。

3.5 在 GitHub Actions 中搭建鸿蒙发布流水线

我们把稳定版的发布流水线放在了 GitHub Actions 上。完整 workflow 大概长这样,我留在项目里做了脱敏处理,跑通的核心流程是这样的:

jobs: release_harmony: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Flutter uses: subosito/flutter-action@v2 - name: Setup DevEco CLI run: | # 下载并解压 DevEco command-line-tools 到 /opt/deveco export DEVECO_SDK_HOME=/opt/deveco/sdk export OHOS_SDK_HOME=/opt/deveco/sdk hvigorw --version - name: Install dependencies run: flutter pub get - name: Run release preparation run: | dart run flutter_release_x prepare \ --release major \ --platform harmony \ --app-json5 .harmony/AppScope/app.json5 - name: Build HAP run: | dart run flutter_release_x build \ --platform harmony \ --sign \ --keystore ${{ secrets.HARMONY_KEYSTORE }} \ --cert ${{ secrets.HARMONY_CERT }} \ --profile ${{ secrets.HARMONY_PROFILE }} - name: Collect artifacts run: dart run flutter_release_x collect --platform harmony - name: Create release run: dart run flutter_release_x publish --provider github

这套 workflow 下来,整个发布链路从“人肉改版本、手动构建、手动传包”变成了“一次触发、自动打 tag、自动出包、自动出 release notes”。我们跑了几十个版本,稳定性比原来的 Jenkins 脚本高了不止一个档次。

3.6 本地调试:适配器如何快速验证

在 CI 里反复试错成本很高,我建议先把适配器在本地跑通。我的做法是准备一个最小化的鸿蒙 demo 工程放到 test/fixtures 下面,flutter_release_x 的 harmony 适配器单测直接读写这个 fixture 工程里的 app.json5 和模拟产物目录。这样每次改代码,一条flutter test就能验证版本号写入是否正确、产物收集逻辑是否正常,不用真的跑一次完整构建。

同时我给 flutter_release_x 写了一个--dry-run参数,开启后所有写操作(改文件、打 tag、推远端)都会变成打印日志,方便你看清它到底要动哪些文件、执行哪些命令。这个参数在刚接入 CI 的时候特别有用,相当于给流水线加了个演示模式。

4. 常见问题与排查技巧实录

4.1 高频问题速查表

下面我整理了这段时间内团队遇到的高频问题,做成速查表,应该能帮你少走不少弯路。

症状可能原因解决方案
hvigorw: command not foundCI 环境没有安装 DevEco CLI 或 PATH 没配环境准备阶段显式 export PATH,并执行which hvigorw探测
app.json5 解析报错用标准 JSON 解析器解析了 json5 格式改用 Dart json5 包进行读写
versionCode 同步后构建失败versionCode 超出整数范围或格式异常检查映射规则,确保是纯整型
HAP 装到真机提示签名错误profile 的 bundleName 与应用实际 bundleName 不一致验签并核对 bundleName、profile 有效期
产物收集到 0 个 HAP 文件输出目录路径随 DevEco 版本变化改为扫描**/*.hap而非写死路径
release notes 里没有鸿蒙信息release notes 模板未更新 harmony 段落在模板里增加 HAP 构建信息字段
打 tag 失败提示“版本不一致”pubspec.yaml 与 app.json5 版本号不同步让 flutter_release_x 先写 app.json5 再打 tag

4.2 最容易翻车的三个隐藏细节

第一个是json5 序列化注释丢失问题。AppScope/app.json5 是 DevEco 的配置文件,如果团队里有人习惯在里面写注释说明某个字段的用途,你在写入新版本号之后,注释会全部消失。虽说不影响构建,但会让后来的维护者困惑。我这边最后的处理方式是:适配器默认保留一个版本写入记录注释,每次写入时把“上次写入时间、操作人、来源流水线”作为注释追加到文件头部,反而变成了一个人可读的审计痕迹。

第二个是hvigorw 的构建缓存问题。CI 里如果复用了旧机器的构建缓存,HAP 产物可能不会重新生成,版本号更新了但包里还是旧代码。我在构建前强制加了一步rm -rf .hvigor和rm -rf build双管齐下,确保每次都是干净构建。虽然构建时间长了一点,但彻底杜绝了“缓存包发上线”这种灾难。

第三个是CI 中 git 操作的 user.name 和 user.email 配置。flutter_release_x 打 tag 和生成 changelog 时需要读取 Git 作者信息,如果 CI 流水线的 git 用户没配置,生成出来的 changelog 作者会变成字符串 NULL,release notes 里出现一堆 NULL 名字,看着非常不专业。解决办法是在流水线里先执行git config user.name "release-bot",这个细节虽小,但对最终发布资产的专业度影响很大。

4.3 踩坑后的两点心得

第一个心得:鸿蒙化适配的测试要分两层。第一层是纯 Dart 单元测试,验证版本解析和映射规则;第二层是真实构建集成测试,必须跑一次完整的 hvigorw 构建和签名,验证工具链之间没有断点。只有第一层通过不代表适配完成,我们在集成测试里抓出了至少五个单测完全覆盖不到的路径问题、缓存问题、签名参数问题。

第二个心得:输出规范化,是团队协作的隐形基础设施。flutter_release_x 之所以好用,不只是因为能发版,而是它把“版本号是什么、HAP 放哪里、release notes 长什么样”这些模糊地带全部统一了。鸿蒙化适配也是如此,我觉得比技术实现更重要的,是所有参与者对“发布资产长什么样、从哪来、到哪去”形成统一预期。

最后分享一个经验:在鸿蒙化适配过程中,我一开始图快写了不少临时脚本,后来全部反悔重写。原因很简单,临时脚本只解决眼前问题,不沉淀到 flutter_release_x 主流程里,一个月后根本没人知道这条发布规则是怎么来的。最终沉淀下来的 harmony 适配器反而成了团队共识的载体。如果说有什么关键建议,那就是:适配鸿蒙不是一次迁移,而是把发布规则重新显式化一次,借这个机会把流程里所有凭经验、凭记忆的部分,都变成代码和配置里白纸黑字的约束。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询