鸿蒙APP CI/CD流水线搭建:从代码提交到上架全自动
持续集成(CI):开发人员频繁地将代码合并到主分支,通过自动化工具运行构建和测试,验证新代码是否破坏现有功能持续交付(CD):将通过测试的应用版本自动化部署到测试环境或生产环境。
鸿蒙APP CI/CD流水线搭建:从代码提交到上架全自动
在鸿蒙应用开发领域,持续集成与持续部署(CI/CD)已成为推动项目高效进展、保障应用质量的核心动力。随着鸿蒙生态的蓬勃发展,应用功能日益复杂,用户对应用的更新速度与稳定性要求不断攀升——但现实中,很多团队仍然停留在“开发人员本地打包→压缩包传群→测试人员手动安装”的原始阶段。
这种模式的痛点显而易见:
- 重复性操作:每次迭代需打开DevEco Studio手动打包,效率低下
- 环境不一致:不同开发者的本地环境差异导致“我这边能跑,你那不行”
- 签名混乱:测试签名和正式签名混用,甚至出现过用测试证书上线的惨案
- 发布延迟:无法实时响应代码提交,需人工触发打包,影响测试与发布节奏
而CI/CD流水线正是解决这些问题的核心方案。本文将手把手教你搭建从代码提交到应用上架的全自动化流水线,涵盖Jenkins/GitLab CI集成、自动化签名与版本管理、灰度发布与A/B测试配置三大核心模块。
一、鸿蒙CI/CD整体架构
1.1 什么是CI/CD?
- 持续集成(CI):开发人员频繁地将代码合并到主分支,通过自动化工具运行构建和测试,验证新代码是否破坏现有功能
- 持续交付(CD):将通过测试的应用版本自动化部署到测试环境或生产环境
1.2 鸿蒙应用构建的核心工具
鸿蒙应用构建离不开两个核心工具:
| 工具 | 作用 | 说明 |
|---|---|---|
| hvigorw | 鸿蒙官方构建工具 | 替代Gradle,用于编译、打包HAP/APP,支持命令行调用 |
| ohpm | 鸿蒙包管理器 | 管理项目依赖,类似npm |
hvigorw是搭建流水线时的核心——少了它,流水线就无法正常运行。它让我们可以使用命令行工具来调用Hvigor任务进行构建,不再依赖DevEco Studio开发工具。
1.3 流水线整体流程
[代码提交] → [触发CI] → [环境准备] → [依赖安装] → [代码检查] → [单元测试]
↓
[构建打包] → [自动化签名] → [产物归档] → [灰度发布] → [全量上架]
↓
[通知反馈](邮件/钉钉/企业微信)
二、Jenkins/GitLab CI集成方案
2.1 环境准备:搭建自动化构建基础
在开始配置CI/CD工具前,需先准备好基础环境。推荐使用Linux(Ubuntu 20.04+)或macOS(鸿蒙打包对Windows兼容性稍弱)。
2.1.1 必备工具清单
| 工具名称 | 作用说明 | 推荐版本 |
|---|---|---|
| JDK | 鸿蒙构建的基础Java环境 | 11(LTS) |
| HarmonyOS SDK | 鸿蒙应用开发与打包核心工具集 | API 10+ |
| Node.js | 用于运行构建脚本 | 18+ |
| hvigor | 鸿蒙官方构建工具 | 3.1.0+ |
| ohpm | 鸿蒙包管理器 | 随SDK安装 |
| Git | 代码版本控制 | 2.30+ |
| Jenkins/GitLab | CI/CD核心工具 | Jenkins 2.400+ / GitLab 15.0+ |
2.1.2 环境变量配置
所有工具安装完成后,需配置全局环境变量,确保CI系统能调用相关命令:
# 配置 JDK
export JAVA_HOME=/usr/lib/jvm/jdk-11-oracle
export PATH=$JAVA_HOME/bin:$PATH
# 配置鸿蒙 SDK(关键:指向DevEco Studio中的SDK路径)
export HARMONYOS_SDK_HOME=~/DevEcoStudio/sdk
export PATH=$HARMONYOS_SDK_HOME/toolchains/hvigor/bin:$PATH
# 配置 hvigor
export HVIGOR_HOME=~/DevEcoStudio/sdk/toolchains/hvigor
export PATH=$HVIGOR_HOME/bin:$PATH
# 配置 ohpm
export OHPM_HOME=~/DevEcoStudio/sdk/toolchains/ohpm
export PATH=$OHPM_HOME/bin:$PATH
配置完成后,执行source /etc/profile生效,并通过以下命令验证环境是否正常:
java -version # 应输出JDK 11版本信息
hvigor -v # 应输出hvigor版本信息
ohpm -v # 应输出ohpm版本信息
2.2 Jenkins集成方案
Jenkins是目前业界主流的CI/CD工具,通过插件扩展实现丰富的功能。
2.2.1 Jenkins插件安装
登录Jenkins(默认地址:http://服务器IP:8080),进入「系统管理 → 插件管理 → 可选插件」,安装以下插件:
- Git Plugin:用于拉取Git仓库代码
- Pipeline Plugin:支持通过Jenkinsfile定义自动化流程(推荐)
- Huawei Cloud Pipeline Plugin:可选,简化与华为云服务的集成
2.2.2 配置全局工具
进入「系统管理 → 全局工具配置」,将本地安装的JDK、hvigor关联到Jenkins:
配置JDK:
- 取消「自动安装」,选择「手动安装」
- 「名称」填写
JDK_11 - 「JAVA_HOME」填写本地JDK路径(如
/usr/lib/jvm/jdk-11-oracle)
配置hvigor:
- 取消「自动安装」,选择「手动安装」
- 「名称」填写
hvigor_3.1 - 「HVIGOR_HOME」填写本地hvigor路径(如
~/DevEcoStudio/sdk/toolchains/hvigor)
2.2.3 Jenkins Pipeline脚本
在项目根目录创建Jenkinsfile,定义完整的CI/CD流程:
pipeline {
agent any
// 环境变量
environment {
// 从Jenkins凭据中获取签名密码
RELEASE_STORE_PWD = credentials('harmony-release-store-pwd')
RELEASE_KEY_PWD = credentials('harmony-release-key-pwd')
// 版本号相关
VERSION_CODE = "${env.BUILD_NUMBER}"
VERSION_NAME = "1.0.${env.BUILD_NUMBER}"
}
stages {
stage('代码检出') {
steps {
checkout scm
}
}
stage('安装依赖') {
steps {
sh 'ohpm install'
}
}
stage('代码静态检查') {
steps {
sh 'hvigor lint'
}
}
stage('单元测试') {
steps {
sh 'hvigor test'
}
post {
always {
junit '**/build/reports/tests/*.xml'
}
}
}
stage('版本号更新') {
steps {
script {
// 更新app.json5中的版本号
sh """
sed -i 's/"code": [0-9]*/"code": ${VERSION_CODE}/' entry/src/main/module.json5
sed -i 's/"name": "[0-9.]*"/"name": "${VERSION_NAME}"/' entry/src/main/module.json5
"""
}
}
}
stage('构建Release包') {
steps {
sh 'hvigor clean'
sh 'hvigor assembleRelease'
}
}
stage('自动化签名') {
steps {
script {
// 调用签名脚本
sh """
./scripts/sign_hap.sh \
--input=build/outputs/hap/release/entry-default-signed.hap \
--output=build/outputs/signed/entry-release-signed.hap
"""
}
}
}
stage('产物归档') {
steps {
archiveArtifacts artifacts: 'build/outputs/signed/*.hap', fingerprint: true
}
}
stage('上传到分发平台') {
when {
branch 'main'
}
steps {
script {
// 调用API上传到测试分发平台
sh """
curl -X POST https://distribution.example.com/upload \
-F "file=@build/outputs/signed/entry-release-signed.hap" \
-F "version=${VERSION_NAME}" \
-F "branch=${env.BRANCH_NAME}"
"""
}
}
}
stage('通知') {
steps {
script {
// 发送钉钉/企业微信通知
def message = "构建成功:${VERSION_NAME}\n下载地址:https://distribution.example.com/download/${VERSION_NAME}"
sh """
curl -X POST https://qyapi.weixin.qq.com/cgi-bin/webhook/send \
-H 'Content-Type: application/json' \
-d '{"msgtype":"text","text":{"content":"${message}"}}'
"""
}
}
}
}
post {
failure {
// 构建失败通知
emailext (
to: 'team@example.com',
subject: "构建失败:${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: "请查看日志:${env.BUILD_URL}"
)
}
}
}
2.3 GitLab CI集成方案
对于使用GitLab进行代码管理的团队,GitLab CI是更轻量级的选择。
2.3.1 .gitlab-ci.yml配置
在项目根目录创建.gitlab-ci.yml文件:
stages:
- build
- test
- package
- deploy
variables:
HARMONY_SDK_HOME: "/opt/DevEcoStudio/sdk"
JAVA_HOME: "/usr/lib/jvm/java-11-openjdk"
# 缓存ohpm依赖
cache:
paths:
- oh-packages/
before_script:
- export PATH=$HARMONY_SDK_HOME/toolchains/hvigor/bin:$PATH
- export PATH=$HARMONY_SDK_HOME/toolchains/ohpm/bin:$PATH
- ohpm config set registry https://repo.harmonyos.com/ohpm/
# 安装依赖
install_deps:
stage: build
script:
- ohpm install
artifacts:
paths:
- oh-packages/
expire_in: 1 hour
# 代码检查
lint:
stage: test
script:
- hvigor lint
dependencies:
- install_deps
# 单元测试
unit_test:
stage: test
script:
- hvigor test
artifacts:
reports:
junit: '**/build/reports/tests/*.xml'
dependencies:
- install_deps
# 构建Release包
build_release:
stage: package
script:
- hvigor clean
- hvigor assembleRelease
artifacts:
paths:
- build/outputs/hap/release/
expire_in: 1 week
only:
- tags
- main
dependencies:
- install_deps
# 签名
sign:
stage: package
script:
- |
# 从GitLab CI变量中获取密码
hvigor sign \
-PstorePassword=$RELEASE_STORE_PWD \
-PkeyPassword=$RELEASE_KEY_PWD \
-PbuildMode=release
artifacts:
paths:
- build/outputs/signed/
dependencies:
- build_release
only:
- tags
- main
# 部署到测试环境
deploy_test:
stage: deploy
script:
- |
curl -F "file=@build/outputs/signed/entry-release-signed.hap" \
-F "version=$CI_COMMIT_TAG" \
https://test-server.example.com/upload
only:
- tags
dependencies:
- sign
# 创建GitHub Release(如果是公开项目)
release:
stage: deploy
image: registry.gitlab.com/gitlab-org/release-cli:latest
script:
- echo "Creating release $CI_COMMIT_TAG"
release:
tag_name: $CI_COMMIT_TAG
description: 'Release $CI_COMMIT_TAG'
assets:
links:
- name: 'HarmonyOS App HAP'
url: 'https://gitlab.com/yourproject/-/jobs/artifacts/$CI_COMMIT_TAG/raw/build/outputs/signed/entry-release-signed.hap?job=sign'
only:
- tags
2.4 两种方案的对比与选择
| 维度 | Jenkins | GitLab CI |
|---|---|---|
| 适用场景 | 复杂流程、多项目集中管理 | 与GitLab仓库紧密集成 |
| 维护成本 | 需单独维护Jenkins服务器 | 无需额外维护(SaaS版) |
| 插件生态 | 丰富,2000+插件 | 相对有限,但常用功能都有 |
| 学习曲线 | 较陡峭 | 较平缓,YAML配置直观 |
| 权限管理 | 基于项目和角色的RBAC | 与GitLab仓库权限一致 |
选择建议:
- 已有Jenkins基础设施的团队 → 继续使用Jenkins
- 团队规模小、希望轻量级方案 → GitLab CI
- 需要复杂工作流编排(如并行构建、动态参数)→ Jenkins Pipeline更强大
三、自动化签名与版本管理
3.1 鸿蒙签名机制详解
鸿蒙应用的签名体系通常包含两层:
- 应用级签名(App-Level Cert):用于标识开发者身份
- 模块级签名(HAP Cert):用于验证每个功能模块的完整性
签名不仅仅是“盖章”,而是整个应用信任链的起点。缺少签名或签名错误的应用无法安装到真机,更无法上架应用市场。
3.1.1 签名所需的文件
| 文件类型 | 说明 | 示例 |
|---|---|---|
| .p12文件 | 包含私钥的证书文件 | release.p12 |
| .cer文件 | 数字证书(公钥) | release.cer |
| .profile文件 | 应用权限配置文件 | release.profile |
调试与发布用的Profile与证书通常分开。调试包能装在测试设备,上架必须用发布证书+发布Profile。
3.1.2 签名证书生成
# 生成密钥对和证书请求
keytool -genkeypair -alias release_key \
-keyalg RSA -keysize 2048 -validity 3650 \
-keystore release.p12 \
-storetype pkcs12
# 导出证书请求文件(.csr)
keytool -certreq -alias release_key \
-keystore release.p12 \
-file release.csr
然后登录鸿蒙开发者平台,上传.csr文件生成profile文件。
3.2 工程中配置签名
3.2.1 签名配置分离
在项目hvigor/ohos/signingConfigs.js中配置签名信息:
// signingConfigs.js
module.exports = {
debug: {
storeFile: 'signing/debug/debug.p12',
storePassword: process.env.DEBUG_STORE_PWD,
keyAlias: 'debug_key',
keyPassword: process.env.DEBUG_KEY_PWD,
profile: 'signing/debug/debug-profile.p7b'
},
release: {
storeFile: 'signing/release/release.p12',
storePassword: process.env.RELEASE_STORE_PWD,
keyPassword: process.env.RELEASE_KEY_PWD,
keyAlias: 'release_key',
profile: 'signing/release/release-profile.p7b'
}
};
在hvigorfile.js中引用:
// hvigorfile.js
const signing = require('./ohos/signingConfigs');
export default {
system: {
// ...其他配置
},
app: {
signingConfigs: signing,
// 构建时指定使用release签名
buildTypes: {
release: {
signingConfig: 'release'
}
}
}
}
关键原则:密码等敏感信息绝对不能硬编码在代码仓库中。通过环境变量注入,CI系统使用“机密变量”管理。
3.2.2 多版本签名管理
不同版本可使用不同的keystore:
demoApp_dev.jks→ 测试demoApp_pre.jks→ 预发布demoApp_prod.jks→ 正式版
3.3 CI环境下的签名实践
3.3.1 密码管理方案
在Jenkins中,使用“Credentials”插件管理密码:
// Jenkinsfile中引用凭据
environment {
RELEASE_STORE_PWD = credentials('harmony-release-store-pwd')
RELEASE_KEY_PWD = credentials('harmony-release-key-pwd')
}
stage('签名') {
steps {
sh """
hvigor sign \
-PstorePassword=${RELEASE_STORE_PWD} \
-PkeyPassword=${RELEASE_KEY_PWD} \
-PbuildMode=release
"""
}
}
在GitLab CI中,使用“CI/CD Variables”:
# 在GitLab UI中设置变量
# Settings → CI/CD → Variables
# 添加 RELEASE_STORE_PWD 和 RELEASE_KEY_PWD(均设置为Masked)
sign:
script:
- |
hvigor sign \
-PstorePassword=$RELEASE_STORE_PWD \
-PkeyPassword=$RELEASE_KEY_PWD \
-PbuildMode=release
3.3.2 离线签名流程
另一种常见做法是“构建未签名包 + 单独签名”的离线签名流程:
-
构建未签名HAP:通过构建参数输出unsigned包
hvigor assembleRelease -Psigned=false -
签名Job拉取证书和密码:从CI安全库获取私钥和profile
-
调用签名工具签名:
hap-sign-tool sign-app \ -keyAlias release_key \ -keyPwd $RELEASE_KEY_PWD \ -keystore release.p12 \ -keystorePwd $RELEASE_STORE_PWD \ -appCertFile release.cer \ -profileFile release.profile \ -inFile unsigned.hap \ -outFile signed.hap -
验签:校验签名块与证书链
hap-sign-tool verify -inFile signed.hap
优点:开发机无需接触发布证书,证书权限集中、可审计。
3.4 版本号自动化管理
版本号管理是CI/CD中容易出错但又至关重要的环节。常见策略:
3.4.1 语义化版本 + 构建号
// Jenkins Pipeline中
environment {
// 使用构建号作为版本code
VERSION_CODE = "${env.BUILD_NUMBER}"
// 版本名:主版本.次版本.构建号
VERSION_NAME = "1.0.${env.BUILD_NUMBER}"
}
stage('更新版本号') {
steps {
// 更新module.json5中的版本号
sh """
sed -i 's/"versionCode": [0-9]*/"versionCode": ${VERSION_CODE}/' entry/src/main/module.json5
sed -i 's/"versionName": "[0-9.]*"/"versionName": "${VERSION_NAME}"/' entry/src/main/module.json5
"""
}
}
3.4.2 Git Tag驱动版本
# GitLab CI中,使用tag作为版本名
variables:
VERSION_NAME: $CI_COMMIT_TAG
VERSION_CODE: $CI_PIPELINE_IID
before_script:
- |
if [ -n "$CI_COMMIT_TAG" ]; then
echo "Tag构建,版本名:$CI_COMMIT_TAG"
# 提取版本code(假设tag格式为 v1.2.3)
VERSION_CODE=$(echo $CI_COMMIT_TAG | sed 's/v//' | awk -F. '{print $1*10000 + $2*100 + $3}')
fi
3.4.3 版本自动递增脚本
#!/bin/bash
# scripts/update_version.sh
MODULE_JSON="entry/src/main/module.json5"
# 读取当前版本code
CURRENT_CODE=$(grep -o '"versionCode": [0-9]*' $MODULE_JSON | grep -o '[0-9]*')
NEW_CODE=$((CURRENT_CODE + 1))
# 读取当前版本name
CURRENT_NAME=$(grep -o '"versionName": "[0-9.]*"' $MODULE_JSON | grep -o '[0-9.]*')
# 递增最后一位
NEW_NAME=$(echo $CURRENT_NAME | awk -F. '{$NF = $NF + 1;} 1' OFS=.)
# 更新文件
sed -i "s/\"versionCode\": $CURRENT_CODE/\"versionCode\": $NEW_CODE/" $MODULE_JSON
sed -i "s/\"versionName\": \"$CURRENT_NAME\"/\"versionName\": \"$NEW_NAME\"/" $MODULE_JSON
echo "版本已更新:$CURRENT_NAME($CURRENT_CODE) → $NEW_NAME($NEW_CODE)"
3.5 技术难点与解决方案
| 难点 | 描述 | 解决思路 |
|---|---|---|
| 签名钥匙安全 | 私钥泄露=全盘崩塌 | 使用CI机密变量管理,禁止进代码库 |
| 版本冲突 | 多分支打包易错 | Git Tag + Build ID严格管理 |
| 多模块依赖 | 模块间版本需兼容 | 统一版本管理文件,CI自动检查依赖 |
| 渠道包一致性 | 资源差异难排查 | 构建后生成哈希校验文件 |
四、灰度发布与A/B测试配置
4.1 灰度发布概念与价值
灰度发布是指按特定规则(如比例、设备类型、用户分组)逐步放量新版本的过程。其核心价值在于:
- 降低风险:小范围验证,发现问题及时回滚
- 收集反馈:在影响全量用户前获取真实数据
- 平滑升级:避免突发流量或兼容性问题
真正的“稳上线”,不是永不出错,而是出错能优雅退场。
4.2 鸿蒙元服务灰度发布机制
鸿蒙的“元服务”(原子化服务)支持灰度发布,核心能力包括:
- 精准控制:支持按比例(10%、30%、100%)或标签分发
- 动态调整:实时监控指标,支持手动/自动扩量
- 跨设备协同:在手机、平板等多端同步状态
4.2.1 AppGallery Connect灰度配置
在AGC控制台配置灰度发布:
- 进入“我的应用”→选择应用→“版本管理”
- 点击“创建灰度版本”
- 设置灰度规则:
- 按比例:如10%用户获取新版本
- 按设备:指定机型(如Mate60、Pura70)
- 按地域:指定国家/地区
- 按用户标签:如“活跃用户”“新用户”
- 设置监控指标和扩量策略:
{ "rollout": { "percent": 10, "regions": ["CN", "EU"], "devices": ["Mate60", "Pura70"], "metrics": { "crash_rate_threshold": 0.05, "launch_success_rate": 0.99 } } }
4.2.2 自动回滚机制
当检测到异常指标超过阈值时,自动触发回滚:
// 伪代码:灰度监控服务
class GrayMonitor {
checkMetrics(version, metrics) {
if (metrics.crashRate > 0.05 || metrics.signatureFail > 100) {
this.rollbackTo(version, 'v2.3.1');
}
}
rollbackTo(failedVersion, targetVersion) {
// 通知AGC停止灰度
agc.grayControl.stop(failedVersion);
// 将流量切回旧版本
agc.distribution.setDefault(targetVersion);
// 发送告警
alert(`灰度版本 ${failedVersion} 已回滚`);
}
}
4.3 A/B测试配置
A/B测试是通过对比两个或多个版本的用户行为数据,验证功能/设计假设的科学方法。
4.3.1 实验配置管理
通过远程配置服务(如AGC Remote Config)管理实验参数:
{
"experiment_id": "home_banner_v2",
"version": "A",
"target_metric": "click_through_rate",
"goal": 0.15,
"variants": {
"A": {
"ui_component": "BannerV1",
"background_color": "#FFFFFF"
},
"B": {
"ui_component": "BannerV2",
"background_color": "#F0F0F0"
}
},
"distribution": {
"strategy": "random",
"ratio": { "A": 0.5, "B": 0.5 }
}
}
4.3.2 端侧实验分组判定
// ExperimentManager.ts
import remoteConfig from '@ohos.remoteConfig';
export class ExperimentManager {
static async getVariant(screenName: string): Promise<string> {
// 拉取远程配置
const config = await remoteConfig.getValue('experiment_config');
const experiment = config.experiments.find(exp => exp.screen === screenName);
if (!experiment) return 'control'; // 默认对照组
// 根据用户ID哈希分配分组(确保同一用户始终看到同一版本)
const userId = await this.getUserId();
const hash = this.hashString(userId + experiment.experimentId);
const ratio = experiment.distribution.ratio;
return hash < ratio.A ? 'A' : 'B';
}
private static hashString(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash) % 1000000;
}
private static async getUserId(): Promise<string> {
// 从本地存储获取用户ID
return await AppStorage.get('userId') || 'anonymous';
}
}
4.3.3 动态组件渲染
// HomePage.ets
import { ExperimentManager } from './ExperimentManager';
@Entry
@Component
struct HomePage {
@State bannerVariant: string = 'A';
aboutToAppear() {
ExperimentManager.getVariant('home_screen').then(variant => {
this.bannerVariant = variant;
});
}
build() {
Column() {
if (this.bannerVariant === 'A') {
BannerV1()
} else {
BannerV2()
}
// 其他页面内容
List() { /* ... */ }
}
.width('100%')
.height('100%')
}
}
4.3.4 数据埋点与采集
// analytics.ts
import analytics from '@ohos.analytics';
export class Analytics {
static trackExperiment(experimentId: string, variant: string, event: string) {
analytics.logEvent({
name: 'experiment_click',
params: {
experiment_id: experimentId,
variant: variant,
event: event,
timestamp: Date.now()
}
});
}
static trackScreenView(screenName: string, variant: string) {
analytics.logEvent({
name: 'screen_view',
params: {
screen: screenName,
variant: variant,
timestamp: Date.now()
}
});
}
}
// 在组件中使用
BannerV1({
onClick: () => {
Analytics.trackExperiment('home_banner_v2', 'A', 'banner_click');
// 跳转逻辑
}
})
4.4 灰度与A/B测试的最佳实践
4.4.1 实验流程闭环
[实验设计] → [代码实现] → [远程配置] → [灰度发布] → [数据采集]
↓
[效果分析] → [决策] → [全量发布或终止]
↑ ↓
[反馈优化] ← [存档]
4.4.2 关键指标监控
| 指标 | 说明 | 阈值 |
|---|---|---|
| 崩溃率 | 实验版本崩溃次数/总用户数 | <0.05 |
| 点击率(CTR) | 点击次数/曝光次数 | 需显著提升 |
| 转化率(CVR) | 目标行为次数/点击次数 | 需显著提升 |
| 页面停留时长 | 用户在实验页面的平均时长 | 需提升或持平 |
| 启动成功率 | 应用成功启动的比例 | >0.99 |
4.4.3 默认降级策略
在代码中务必为每个特性开关提供硬编码的default逻辑,防止在极端脱网场景下出现UI空白。
function getFeatureEnabled(featureName: string): boolean {
try {
// 尝试从远程配置获取
return remoteConfig.getBoolean(featureName);
} catch (error) {
// 网络异常或配置缺失时使用默认值
console.warn('使用默认配置', error);
return DEFAULT_FEATURE_FLAGS[featureName] || false;
}
}
五、实战:全流程流水线配置示例
5.1 项目结构
harmony-app/
├── .gitlab-ci.yml # GitLab CI配置
├── Jenkinsfile # Jenkins Pipeline配置
├── hvigorfile.js # hvigor配置文件
├── oh-packages/ # ohpm依赖缓存
├── scripts/
│ ├── sign.sh # 签名脚本
│ ├── update_version.sh # 版本更新脚本
│ └── upload_to_pgy.sh # 上传到分发平台
├── signing/ # 签名文件(不提交到Git)
│ ├── debug/
│ │ ├── debug.p12
│ │ └── debug-profile.p7b
│ └── release/
│ ├── release.p12
│ └── release-profile.p7b
└── entry/ # 主模块
└── src/main/
└── module.json5 # 模块配置(含版本号)
5.2 完整GitLab CI配置
# .gitlab-ci.yml
stages:
- prepare
- test
- build
- sign
- deploy
- notify
variables:
HARMONY_SDK_HOME: "/opt/DevEcoStudio/sdk"
JAVA_HOME: "/usr/lib/jvm/java-11-openjdk"
CACHE_KEY: "ohpm-${CI_COMMIT_REF_SLUG}"
cache:
key: ${CACHE_KEY}
paths:
- oh-packages/
before_script:
- export PATH=$HARMONY_SDK_HOME/toolchains/hvigor/bin:$PATH
- export PATH=$HARMONY_SDK_HOME/toolchains/ohpm/bin:$PATH
- ohpm config set registry https://repo.harmonyos.com/ohpm/
install:
stage: prepare
script:
- ohpm install
artifacts:
paths:
- oh-packages/
expire_in: 1 day
lint:
stage: test
script:
- hvigor lint
dependencies:
- install
unit_test:
stage: test
script:
- hvigor test
artifacts:
reports:
junit: '**/build/reports/tests/*.xml'
dependencies:
- install
build_debug:
stage: build
script:
- hvigor clean
- hvigor assembleDebug
artifacts:
paths:
- build/outputs/hap/debug/
expire_in: 1 week
except:
- tags
build_release:
stage: build
script:
- hvigor clean
- hvigor assembleRelease
artifacts:
paths:
- build/outputs/hap/release/entry-default-unsigned.hap
expire_in: 1 week
only:
- tags
dependencies:
- install
sign_release:
stage: sign
script:
- |
hap-sign-tool sign-app \
-keyAlias release_key \
-keyPwd $RELEASE_KEY_PWD \
-keystore signing/release/release.p12 \
-keystorePwd $RELEASE_STORE_PWD \
-appCertFile signing/release/release.cer \
-profileFile signing/release/release-profile.p7b \
-inFile build/outputs/hap/release/entry-default-unsigned.hap \
-outFile build/outputs/signed/entry-release-signed.hap
- hap-sign-tool verify -inFile build/outputs/signed/entry-release-signed.hap
artifacts:
paths:
- build/outputs/signed/entry-release-signed.hap
expire_in: 1 month
only:
- tags
dependencies:
- build_release
deploy_test:
stage: deploy
script:
- |
curl -F "file=@build/outputs/signed/entry-release-signed.hap" \
-F "version=$CI_COMMIT_TAG" \
-F "branch=$CI_COMMIT_REF_NAME" \
$TEST_DEPLOY_URL
only:
- tags
dependencies:
- sign_release
create_release:
stage: deploy
image: registry.gitlab.com/gitlab-org/release-cli:latest
script:
- echo "Creating release $CI_COMMIT_TAG"
release:
tag_name: $CI_COMMIT_TAG
description: 'Release $CI_COMMIT_TAG'
assets:
links:
- name: 'HarmonyOS App HAP'
url: 'https://yourdomain.com/download/${CI_COMMIT_TAG}/entry-release-signed.hap'
only:
- tags
dependencies:
- sign_release
notify:
stage: notify
script:
- |
curl -X POST $DINGTALK_WEBHOOK \
-H 'Content-Type: application/json' \
-d "{
\"msgtype\": \"markdown\",
\"markdown\": {
\"title\": \"构建通知\",
\"text\": \"### HarmonyOS应用构建完成\n- 版本:$CI_COMMIT_TAG\n- 分支:$CI_COMMIT_REF_NAME\n- 状态:成功\n- 下载:[HAP包]($CI_JOB_URL/artifacts/file/build/outputs/signed/entry-release-signed.hap)\"
}
}"
only:
- tags
dependencies:
- sign_release
5.3 Jenkins Pipeline完整配置
// Jenkinsfile
pipeline {
agent {
label 'harmonyos-builder'
}
parameters {
choice(
name: 'BUILD_TYPE',
choices: ['debug', 'release'],
description: '构建类型'
)
string(
name: 'VERSION_NAME',
defaultValue: '',
description: '版本名称(留空自动生成)'
)
}
environment {
// 从凭据获取
RELEASE_STORE_PWD = credentials('harmony-release-store-pwd')
RELEASE_KEY_PWD = credentials('harmony-release-key-pwd')
DEBUG_STORE_PWD = credentials('harmony-debug-store-pwd')
DEBUG_KEY_PWD = credentials('harmony-debug-key-pwd')
// 版本信息
VERSION_CODE = "${env.BUILD_NUMBER}"
VERSION_NAME = params.VERSION_NAME ?: "1.0.${env.BUILD_NUMBER}"
// 产物路径
UNSIGNED_HAP = "build/outputs/hap/${params.BUILD_TYPE}/entry-default-unsigned.hap"
SIGNED_HAP = "build/outputs/signed/entry-${params.BUILD_TYPE}-signed.hap"
}
stages {
stage('检出代码') {
steps {
checkout scm
}
}
stage('安装依赖') {
steps {
sh 'ohpm install'
}
}
stage('代码质量检查') {
when {
expression { params.BUILD_TYPE == 'release' }
}
steps {
sh 'hvigor lint'
sh 'hvigor test'
}
post {
always {
junit '**/build/reports/tests/*.xml'
}
}
}
stage('更新版本号') {
steps {
script {
sh """
sed -i 's/"versionCode": [0-9]*/"versionCode": ${VERSION_CODE}/' entry/src/main/module.json5
sed -i 's/"versionName": "[0-9.]*"/"versionName": "${VERSION_NAME}"/' entry/src/main/module.json5
"""
}
}
}
stage('构建应用') {
steps {
sh "hvigor clean"
sh "hvigor assemble${params.BUILD_TYPE.capitalize()} -Psigned=false"
}
}
stage('签名') {
steps {
script {
if (params.BUILD_TYPE == 'release') {
sh """
hap-sign-tool sign-app \
-keyAlias release_key \
-keyPwd ${RELEASE_KEY_PWD} \
-keystore signing/release/release.p12 \
-keystorePwd ${RELEASE_STORE_PWD} \
-appCertFile signing/release/release.cer \
-profileFile signing/release/release-profile.p7b \
-inFile ${UNSIGNED_HAP} \
-outFile ${SIGNED_HAP}
"""
} else {
sh """
hap-sign-tool sign-app \
-keyAlias debug_key \
-keyPwd ${DEBUG_KEY_PWD} \
-keystore signing/debug/debug.p12 \
-keystorePwd ${DEBUG_STORE_PWD} \
-appCertFile signing/debug/debug.cer \
-profileFile signing/debug/debug-profile.p7b \
-inFile ${UNSIGNED_HAP} \
-outFile ${SIGNED_HAP}
"""
}
}
}
}
stage('验证签名') {
steps {
sh "hap-sign-tool verify -inFile ${SIGNED_HAP}"
}
}
stage('上传产物') {
steps {
archiveArtifacts artifacts: SIGNED_HAP, fingerprint: true
script {
// 上传到蒲公英(示例)
if (params.BUILD_TYPE == 'debug') {
sh """
curl -F "file=@${SIGNED_HAP}" \
-F "_api_key=${PGYER_API_KEY}" \
https://www.pgyer.com/apiv2/app/upload
"""
}
}
}
}
stage('灰度发布') {
when {
expression { params.BUILD_TYPE == 'release' && env.BRANCH_NAME == 'main' }
}
steps {
script {
// 调用AGC API创建灰度版本
sh """
curl -X POST https://connect-api.cloud.huawei.com/api/publish/gray \
-H "Authorization: Bearer ${AGC_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"packagePath": "${SIGNED_HAP}",
"versionCode": ${VERSION_CODE},
"versionName": "${VERSION_NAME}",
"grayRules": {
"percent": 10,
"regions": ["CN"]
}
}'
"""
}
}
}
}
post {
success {
emailext (
to: 'team@example.com',
subject: "构建成功:${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: """
构建信息:
- 版本:${VERSION_NAME} (${VERSION_CODE})
- 类型:${params.BUILD_TYPE}
- 产物:${env.BUILD_URL}artifact/${SIGNED_HAP}
"""
)
}
failure {
emailext (
to: 'team@example.com',
subject: "构建失败:${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: "请查看日志:${env.BUILD_URL}console"
)
}
}
}
六、总结
本文从鸿蒙应用CI/CD的实际需求出发,系统讲解了从代码提交到应用上架的全自动化流程:
6.1 核心要点回顾
-
CI/CD工具选择:
- Jenkins适合复杂流程、多项目管理,Pipeline功能强大
- GitLab CI与代码仓库紧密集成,配置简单,适合中小团队
-
自动化签名:
- 证书私钥绝对禁止进入代码仓库
- 通过CI机密变量注入密码,实现安全自动化签名
- 离线签名流程进一步隔离证书权限
-
版本管理:
- 版本code与构建号关联,确保单调递增
- Git Tag驱动版本发布,实现可追溯
-
灰度发布:
- 支持按比例、设备、地域、用户标签分发
- 设置监控指标,异常时自动回滚
- 真正的稳定上线,不是永不出错,而是出错能优雅退场
-
A/B测试:
- 远程配置动态调整实验参数
- 端侧根据用户哈希稳定分流
- 数据埋点驱动产品决策
6.2 最佳实践清单
| 类别 | 检查项 | 说明 |
|---|---|---|
| 代码管理 | Git-flow分支模型 | 主分支保护,仅允许Merge Request |
| 构建 | 使用hvigor命令行 | 不依赖IDE,确保可重复构建 |
| 签名 | 证书与密码分离 | 密码使用CI机密变量 |
| 版本 | 版本号自动递增 | 与构建号或Git Tag关联 |
| 测试 | 单元测试自动化 | 失败则终止流水线 |
| 产物 | 归档所有构建产物 | 保留至少30天 |
| 通知 | 构建结果多渠道通知 | 钉钉/企业微信/邮件 |
| 灰度 | 设置回滚阈值 | 崩溃率>5%自动回滚 |
6.3 未来展望
随着鸿蒙生态的持续发展,CI/CD领域也在不断演进:
- 云原生构建:基于容器的构建环境,实现完全一致的构建结果
- AI辅助测试:智能生成测试用例,自动发现兼容性问题
- 全链路观测:从代码提交到用户使用,全程可观测、可追溯
- 声明式发布:通过YAML描述发布策略,实现GitOps式应用交付
更多推荐



所有评论(0)