鸿蒙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 离线签名流程

另一种常见做法是“构建未签名包 + 单独签名”的离线签名流程:

  1. 构建未签名HAP:通过构建参数输出unsigned包

    hvigor assembleRelease -Psigned=false
    
  2. 签名Job拉取证书和密码:从CI安全库获取私钥和profile

  3. 调用签名工具签名

    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
    
  4. 验签:校验签名块与证书链

    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控制台配置灰度发布:

  1. 进入“我的应用”→选择应用→“版本管理”
  2. 点击“创建灰度版本”
  3. 设置灰度规则:
    • 按比例:如10%用户获取新版本
    • 按设备:指定机型(如Mate60、Pura70)
    • 按地域:指定国家/地区
    • 按用户标签:如“活跃用户”“新用户”
  4. 设置监控指标和扩量策略:
    {
      "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 核心要点回顾

  1. CI/CD工具选择

    • Jenkins适合复杂流程、多项目管理,Pipeline功能强大
    • GitLab CI与代码仓库紧密集成,配置简单,适合中小团队
  2. 自动化签名

    • 证书私钥绝对禁止进入代码仓库
    • 通过CI机密变量注入密码,实现安全自动化签名
    • 离线签名流程进一步隔离证书权限
  3. 版本管理

    • 版本code与构建号关联,确保单调递增
    • Git Tag驱动版本发布,实现可追溯
  4. 灰度发布

    • 支持按比例、设备、地域、用户标签分发
    • 设置监控指标,异常时自动回滚
    • 真正的稳定上线,不是永不出错,而是出错能优雅退场
  5. A/B测试

    • 远程配置动态调整实验参数
    • 端侧根据用户哈希稳定分流
    • 数据埋点驱动产品决策

6.2 最佳实践清单

类别 检查项 说明
代码管理 Git-flow分支模型 主分支保护,仅允许Merge Request
构建 使用hvigor命令行 不依赖IDE,确保可重复构建
签名 证书与密码分离 密码使用CI机密变量
版本 版本号自动递增 与构建号或Git Tag关联
测试 单元测试自动化 失败则终止流水线
产物 归档所有构建产物 保留至少30天
通知 构建结果多渠道通知 钉钉/企业微信/邮件
灰度 设置回滚阈值 崩溃率>5%自动回滚

6.3 未来展望

随着鸿蒙生态的持续发展,CI/CD领域也在不断演进:

  • 云原生构建:基于容器的构建环境,实现完全一致的构建结果
  • AI辅助测试:智能生成测试用例,自动发现兼容性问题
  • 全链路观测:从代码提交到用户使用,全程可观测、可追溯
  • 声明式发布:通过YAML描述发布策略,实现GitOps式应用交付
Logo

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

更多推荐