HarmonyOS开发:持续交付CD流水线配置

举报
Jack20 发表于 2026/06/24 16:48:46 2026/06/24
【摘要】 HarmonyOS开发:持续交付CD流水线配置📌 核心要点:CD流水线让鸿蒙应用从"构建完成"到"部署上线"全程自动化——自动打包签名、多环境分发、审批卡点,把发布从"手工活"变成"流水线"。 背景与动机CI搞定了,代码提交能自动构建了。然后呢?构建出来的HAP文件躺在CI服务器的归档目录里,接下来你得手动拷贝、手动签名、手动上传到测试平台、手动通知测试同学……一套流程下来,半小时起步。...

HarmonyOS开发:持续交付CD流水线配置

📌 核心要点:CD流水线让鸿蒙应用从"构建完成"到"部署上线"全程自动化——自动打包签名、多环境分发、审批卡点,把发布从"手工活"变成"流水线"。

背景与动机

CI搞定了,代码提交能自动构建了。然后呢?

构建出来的HAP文件躺在CI服务器的归档目录里,接下来你得手动拷贝、手动签名、手动上传到测试平台、手动通知测试同学……一套流程下来,半小时起步。如果测试环境、预发环境、生产环境都要部署一遍?一天就这么搭进去了。

更要命的是手动操作容易出错。你有没有把debug签名的包发到生产环境?有没有忘了改版本号就发布了?有没有测试环境跑的是上周的包自己还不知道?

持续交付(Continuous Delivery,CD)就是把这些"手动搬运"的活全部自动化。CI负责"代码→构建产物",CD负责"构建产物→可发布状态"。两者合在一起,才是完整的DevOps闭环。

鸿蒙项目的CD有几个特殊挑战:签名流程比Android更严格(需要.p12证书+.p7b Profile双文件),AppGallery Connect的API对接和Google Play完全不同,多设备形态(手机、平板、穿戴)的包需要分别处理。

核心原理

CD流水线的核心逻辑:构建产物经过签名、验证、审批后,自动部署到目标环境

flowchart TB
    A[CI构建产物] --> B[自动签名]
    B --> C{环境选择}
    C -->|测试环境| D[部署到测试设备]
    C -->|预发环境| E[部署到预发环境]
    C -->|生产环境| F{审批卡点}
    
    F -->|审批通过| G[上传AppGallery]
    F -->|审批拒绝| H[打回修改]
    
    G --> I[提交审核]
    I --> J[审核通过]
    J --> K[正式发布]
    
    D --> L[自动化验证]
    E --> L
    L --> M{验证结果}
    M -->|通过| N[进入下一环境]
    M -->|失败| O[告警+阻断]
    
    classDef input fill:#6C5CE7,stroke:#5B4BC9,color:#fff
    classDef process fill:#00B894,stroke:#00A381,color:#fff
    classDef decision fill:#FDCB6E,stroke:#F0B429,color:#333
    classDef success fill:#55EFC4,stroke:#00B894,color:#333
    classDef fail fill:#FF7675,stroke:#D63031,color:#fff
    classDef env fill:#74B9FF,stroke:#0984E3,color:#fff
    
    class A input
    class B,D,E,G,I,J,K,N process
    class C,F,M decision
    class L success
    class H,O fail
    class D,E env

CD和CI的关键区别:

维度 CI CD
触发方式 代码提交自动触发 CI完成后自动触发或手动触发
核心目标 确保代码能构建通过 确保产物能安全部署
关键环节 编译、测试 签名、审批、部署
人为干预 尽量少 生产环境需要审批卡点
回滚策略 回退代码 回退到上一版本包

代码实战

基础用法:自动化打包与签名

CD的第一步是把构建产物变成可安装的包。鸿蒙应用的签名需要两个文件:签名证书(.p12)和签名Profile(.p7b)。

#!/bin/bash
# auto_sign.sh - 鸿蒙应用自动签名脚本

set -e  # 任何命令失败立即退出

# ===== 配置区 =====
# 从环境变量或CI凭据中获取,不硬编码
SIGN_CERT_PATH="${SIGN_CERT_PATH:?签名证书路径未设置}"
SIGN_CERT_PASSWORD="${SIGN_CERT_PASSWORD:?签名证书密码未设置}"
SIGN_PROFILE_PATH="${SIGN_PROFILE_PATH:?签名Profile路径未设置}"

# 构建产物路径
HAP_FILE="entry/build/default/outputs/default/entry-default-unsigned.hap"
OUTPUT_DIR="release-output"

# ===== 签名流程 =====
echo "===== 开始自动签名 ====="

# 1. 检查签名文件是否存在
if [ ! -f "$SIGN_CERT_PATH" ]; then
    echo "❌ 签名证书不存在: $SIGN_CERT_PATH"
    exit 1
fi

if [ ! -f "$SIGN_PROFILE_PATH" ]; then
    echo "❌ 签名Profile不存在: $SIGN_PROFILE_PATH"
    exit 1
fi

# 2. 检查HAP文件是否存在
if [ ! -f "$HAP_FILE" ]; then
    echo "❌ HAP文件不存在: $HAP_FILE"
    exit 1
fi

# 3. 创建输出目录
mkdir -p "$OUTPUT_DIR"

# 4. 使用hapsigner工具签名
# hapsigner是HarmonyOS SDK自带的签名工具
HAPSIGNER_PATH="${HARMONYOS_SDK_HOME}/toolchains/hapsigner"

java -jar "${HAPSIGNER_PATH}/hapsigntoolv2.jar" sign-app \
    -keyAlias "release" \
    -keyPwd "${SIGN_CERT_PASSWORD}" \
    -certFile "${SIGN_CERT_PATH}" \
    -profileFile "${SIGN_PROFILE_PATH}" \
    -inFile "${HAP_FILE}" \
    -outFile "${OUTPUT_DIR}/entry-signed.hap" \
    -signAlg SHA256withECDSA \
    -mode localSign

# 5. 验证签名
java -jar "${HAPSIGNER_PATH}/hapsigntoolv2.jar" verify-app \
    -inFile "${OUTPUT_DIR}/entry-signed.hap"

echo "✅ 签名完成: ${OUTPUT_DIR}/entry-signed.hap"

这段脚本有几个要点:

  1. 敏感信息从环境变量读取:签名密码绝不硬编码,从CI系统的凭据管理中注入
  2. 前置检查:签名文件和HAP文件都存在才继续,避免签名到一半才发现缺文件
  3. 签名后验证:签名完了立即验证,确保签名有效

进阶用法:多环境CD流水线

实际项目中至少有三个环境:测试、预发、生产。每个环境的签名配置、部署目标都不同。

# .gitlab-ci.yml - 多环境CD配置
stages:
  - build
  - sign
  - deploy-dev
  - deploy-staging
  - approve-production
  - deploy-production

variables:
  # 不同环境的签名配置
  DEV_CERT: "dev-cert-config"
  STAGING_CERT: "staging-cert-config"
  PROD_CERT: "prod-cert-config"

# ===== 构建阶段 =====
build:
  stage: build
  script:
    - ohpm install --all
    - ./hvigorw assembleHap --no-daemon
  artifacts:
    paths:
      - entry/build/default/outputs/default/

# ===== 签名阶段 =====
sign_dev:
  stage: sign
  needs: [build]
  script:
    - echo "使用开发签名..."
    - bash scripts/auto_sign.sh dev
  artifacts:
    paths:
      - release-output/entry-dev-signed.hap

sign_staging:
  stage: sign
  needs: [build]
  script:
    - echo "使用预发签名..."
    - bash scripts/auto_sign.sh staging
  artifacts:
    paths:
      - release-output/entry-staging-signed.hap

sign_prod:
  stage: sign
  needs: [build]
  script:
    - echo "使用生产签名..."
    - bash scripts/auto_sign.sh prod
  artifacts:
    paths:
      - release-output/entry-prod-signed.hap

# ===== 测试环境部署 =====
deploy_dev:
  stage: deploy-dev
  needs: [sign_dev]
  script:
    - echo "部署到测试环境..."
    # 使用hdc安装到测试设备
    - hdc install -r release-output/entry-dev-signed.hap
    # 触发自动化测试
    - bash scripts/run_smoke_test.sh
  environment:
    name: development
    url: https://dev.your-app.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'

# ===== 预发环境部署 =====
deploy_staging:
  stage: deploy-staging
  needs: [sign_staging]
  script:
    - echo "部署到预发环境..."
    - hdc install -r release-output/entry-staging-signed.hap
    - bash scripts/run_regression_test.sh
  environment:
    name: staging
    url: https://staging.your-app.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "release/*"'

# ===== 生产审批卡点 =====
approve_production:
  stage: approve-production
  needs: [sign_prod]
  script:
    - echo "等待生产环境审批..."
  when: manual  # 手动触发,即审批卡点
  allow_failure: false
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

# ===== 生产环境部署 =====
deploy_production:
  stage: deploy-production
  needs: [approve_production]
  script:
    - echo "部署到生产环境..."
    # 上传到AppGallery Connect
    - python3 scripts/upload_to_appgallery.py \
        --hap release-output/entry-prod-signed.hap \
        --env production
  environment:
    name: production
    url: https://appgallery.huawei.com/your-app
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

这个配置的关键设计:

  1. 签名阶段并行:三个环境的签名同时进行,节省时间
  2. 环境隔离:每个环境有独立的签名、部署、验证流程
  3. 审批卡点:生产环境必须手动触发,防止误发布
  4. 分支策略:develop分支→测试环境,release分支→预发环境,main分支→生产环境

完整示例:AppGallery Connect自动上传

生产环境部署的核心是上传到华为应用市场。AppGallery Connect提供了API,可以实现自动上传。

# upload_to_appgallery.py - 自动上传HAP到AppGallery Connect
import os
import sys
import json
import time
import hashlib
import requests

class AppGalleryUploader:
    """华为AppGallery Connect上传工具"""
    
    def __init__(self, client_id: str, client_secret: str):
        self.client_id = client_id
        self.client_secret = client_secret
        self.base_url = "https://connect-api.cloud.huawei.com/api"
        self.token = None
    
    def get_access_token(self) -> str:
        """获取API访问令牌"""
        url = f"{self.base_url}/oauth2/v1/token"
        payload = {
            "grant_type": "client_credentials",
            "client_id": self.client_id,
            "client_secret": self.client_secret
        }
        resp = requests.post(url, json=payload)
        resp.raise_for_status()
        data = resp.json()
        
        if data.get("code") != 0:
            raise Exception(f"获取Token失败: {data.get('msg')}")
        
        self.token = data["access_token"]
        print(f"✅ 获取Token成功,有效期: {data.get('expires_in')}秒")
        return self.token
    
    def get_app_info(self, package_name: str) -> dict:
        """获取应用信息"""
        url = f"{self.base_url}/pd/v1/app/info"
        headers = {"Authorization": f"Bearer {self.token}"}
        params = {"packageName": package_name}
        
        resp = requests.get(url, headers=headers, params=params)
        resp.raise_for_status()
        return resp.json()
    
    def upload_hap(self, app_id: str, hap_path: str) -> str:
        """上传HAP文件"""
        # 1. 获取上传URL
        url = f"{self.base_url}/pd/v1/upload"
        headers = {"Authorization": f"Bearer {self.token}"}
        payload = {
            "appId": app_id,
            "suffix": "hap",
            "contentType": "application/octet-stream"
        }
        
        resp = requests.post(url, headers=headers, json=payload)
        resp.raise_for_status()
        upload_info = resp.json()
        
        if upload_info.get("code") != 0:
            raise Exception(f"获取上传URL失败: {upload_info.get('msg')}")
        
        upload_url = upload_info["data"]["uploadUrl"]
        object_key = upload_info["data"]["objectKey"]
        
        # 2. 上传文件
        file_size = os.path.getsize(hap_path)
        print(f"📦 上传HAP文件: {hap_path} ({file_size / 1024 / 1024:.1f}MB)")
        
        with open(hap_path, 'rb') as f:
            files = {'file': (os.path.basename(hap_path), f, 'application/octet-stream')}
            resp = requests.post(upload_url, files=files)
            resp.raise_for_status()
        
        print(f"✅ HAP上传成功,objectKey: {object_key}")
        return object_key
    
    def submit_review(self, app_id: str, object_key: str) -> dict:
        """提交审核"""
        url = f"{self.base_url}/pd/v1/submit"
        headers = {"Authorization": f"Bearer {self.token}"}
        payload = {
            "appId": app_id,
            "releaseType": 1,  # 1=全量更新
            "remark": f"自动发布 - {time.strftime('%Y-%m-%d %H:%M')}",
            "files": [{"fileName": "entry.hap", "fileDest": "/entry.hap", "objectKey": object_key}]
        }
        
        resp = requests.post(url, headers=headers, json=payload)
        resp.raise_for_status()
        return resp.json()


def main():
    # 从环境变量读取配置
    client_id = os.environ.get("AG_CLIENT_ID")
    client_secret = os.environ.get("AG_CLIENT_SECRET")
    package_name = os.environ.get("AG_PACKAGE_NAME")
    hap_path = sys.argv[1] if len(sys.argv) > 1 else "release-output/entry-prod-signed.hap"
    
    if not all([client_id, client_secret, package_name]):
        print("❌ 缺少必要的环境变量: AG_CLIENT_ID, AG_CLIENT_SECRET, AG_PACKAGE_NAME")
        sys.exit(1)
    
    # 执行上传流程
    uploader = AppGalleryUploader(client_id, client_secret)
    
    # 1. 获取Token
    uploader.get_access_token()
    
    # 2. 获取应用信息
    app_info = uploader.get_app_info(package_name)
    app_id = app_info["data"]["appId"]
    print(f"📱 应用ID: {app_id}")
    
    # 3. 上传HAP
    object_key = uploader.upload_hap(app_id, hap_path)
    
    # 4. 提交审核
    result = uploader.submit_review(app_id, object_key)
    if result.get("code") == 0:
        print(f"🎉 提交审核成功!")
    else:
        print(f"❌ 提交审核失败: {result.get('msg')}")
        sys.exit(1)


if __name__ == "__main__":
    main()

踩坑与注意事项

坑1:签名Profile和设备不匹配

开发Profile只能在开发设备上用,发布Profile只能在正式设备上用。搞混了直接安装失败。

解决方案:不同环境严格使用对应类型的Profile。

# 检查Profile类型
java -jar hapsigntoolv2.jar verify-app -inFile your-app.hap -outCertChain cert.cer
# 查看证书中的Profile类型字段

坑2:AppGallery API限流

华为的API有调用频率限制,频繁上传会被限流。

解决方案:合理控制上传频率,失败后指数退避重试。

import time

def upload_with_retry(uploader, app_id, hap_path, max_retries=3):
    """带重试的上传"""
    for attempt in range(max_retries):
        try:
            return uploader.upload_hap(app_id, hap_path)
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            wait_time = 2 ** attempt  # 指数退避: 1s, 2s, 4s
            print(f"上传失败,{wait_time}秒后重试... 错误: {e}")
            time.sleep(wait_time)

坑3:多模块HAP的打包顺序

鸿蒙多模块项目(Entry + Feature + Shared库),打包顺序很重要。Shared库必须先构建,Feature模块依赖Shared库。

解决方案:在hvigorw构建命令中指定正确的模块顺序。

# 先构建shared库
./hvigorw assembleHar --mode module -p module=shared@default --no-daemon

# 再构建feature模块
./hvigorw assembleHap --mode module -p module=feature@default --no-daemon

# 最后构建entry
./hvigorw assembleApp --mode module -p module=entry@default --no-daemon

坑4:审批流程形同虚设

很多团队虽然配了审批卡点,但审批人根本不看就点通过,审批等于没有。

解决方案:审批前自动生成变更报告,让审批人有据可依。

def generate_release_notes():
    """自动生成发布说明"""
    # 获取最近一次tag到现在的提交记录
    commits = os.popen("git log $(git describe --tags --abbrev=0)..HEAD --oneline").read()
    
    # 获取变更文件列表
    changed_files = os.popen("git diff --name-only $(git describe --tags --abbrev=0)..HEAD").read()
    
    release_notes = f"""
    ## 发布说明
    ### 提交记录
    {commits}
    
    ### 变更文件
    {changed_files}
    
    ### 构建信息
    - 构建时间: {time.strftime('%Y-%m-%d %H:%M:%S')}
    - 构建编号: {os.environ.get('BUILD_NUMBER', 'N/A')}
    - Git提交: {os.popen('git rev-parse HEAD').read().strip()[:8]}
    """
    return release_notes

坑5:回滚方案缺失

CD流水线部署失败后没有回滚方案,只能手动处理,慌乱中容易出错。

解决方案:每次部署前自动备份上一版本的包,失败时一键回滚。

# 部署前备份当前版本
backup_dir="backups/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$backup_dir"
cp release-output/*.hap "$backup_dir/"

# 部署失败时回滚
if [ $? -ne 0 ]; then
    echo "❌ 部署失败,开始回滚..."
    latest_backup=$(ls -td backups/*/ | head -1)
    hdc install -r "$latest_backup/entry-signed.hap"
    echo "✅ 已回滚到上一版本"
fi

HarmonyOS 6适配说明

HarmonyOS 6对CD流水线的影响主要体现在以下几个方面:

  1. 新的签名算法:HarmonyOS 6推荐使用SHA384withECDSA签名算法,旧版SHA256withECDSA仍然兼容但建议升级。

  2. AppGallery Connect API v2:HarmonyOS 6对应新版API,部分接口有变更,需要更新上传脚本。

  3. 多形态应用包:HarmonyOS 6支持一次开发多端部署,CD流水线需要处理不同设备形态的包(手机HAP、平板HAP、穿戴HAP等)。

  4. 应用沙箱增强:HarmonyOS 6的应用沙箱更严格,部署验证时需要检查权限声明是否完整。

  5. 分发策略API:AppGallery Connect新增了灰度发布API,可以在CD流水线中直接配置灰度规则,不再需要手动在控制台操作。

总结

CD流水线是CI的自然延伸——CI解决"能不能构建",CD解决"能不能发布"。两者缺一不可。

维度 评价
学习难度 ⭐⭐⭐⭐⭐ 需要掌握签名机制、AppGallery API、多环境管理、审批流程设计
使用频率 ⭐⭐⭐⭐ 每次发布都会用到,但不如CI那么频繁
重要程度 ⭐⭐⭐⭐⭐ 直接影响发布质量和效率,出问题就是线上事故

几个关键提醒:

  • 签名文件是CD的命脉,管理不好整个流水线都跑不通。用CI凭据管理,绝不硬编码
  • 审批卡点不是摆设,得配合变更报告让审批人真正审核
  • 多环境配置要隔离,测试环境和生产环境的签名、部署方式完全不同
  • 回滚方案必须提前准备,等出问题再想就晚了
  • AppGallery API有坑,限流、接口变更、文档滞后,做好重试和兜底

CD流水线搭好了,你的发布流程就从"手工搬运"变成了"自动流水线"。下一步要做的,是优化构建本身的速度——毕竟构建太慢,CI/CD再流畅也白搭。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。