HarmonyOS开发:持续交付CD流水线配置
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"
这段脚本有几个要点:
- 敏感信息从环境变量读取:签名密码绝不硬编码,从CI系统的凭据管理中注入
- 前置检查:签名文件和HAP文件都存在才继续,避免签名到一半才发现缺文件
- 签名后验证:签名完了立即验证,确保签名有效
进阶用法:多环境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.cn/your-app
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
这个配置的关键设计:
- 签名阶段并行:三个环境的签名同时进行,节省时间
- 环境隔离:每个环境有独立的签名、部署、验证流程
- 审批卡点:生产环境必须手动触发,防止误发布
- 分支策略: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.cn/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流水线的影响主要体现在以下几个方面:
-
新的签名算法:HarmonyOS 6推荐使用SHA384withECDSA签名算法,旧版SHA256withECDSA仍然兼容但建议升级。
-
AppGallery Connect API v2:HarmonyOS 6对应新版API,部分接口有变更,需要更新上传脚本。
-
多形态应用包:HarmonyOS 6支持一次开发多端部署,CD流水线需要处理不同设备形态的包(手机HAP、平板HAP、穿戴HAP等)。
-
应用沙箱增强:HarmonyOS 6的应用沙箱更严格,部署验证时需要检查权限声明是否完整。
-
分发策略API:AppGallery Connect新增了灰度发布API,可以在CD流水线中直接配置灰度规则,不再需要手动在控制台操作。
总结
CD流水线是CI的自然延伸——CI解决"能不能构建",CD解决"能不能发布"。两者缺一不可。
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐⭐ 需要掌握签名机制、AppGallery API、多环境管理、审批流程设计 |
| 使用频率 | ⭐⭐⭐⭐ 每次发布都会用到,但不如CI那么频繁 |
| 重要程度 | ⭐⭐⭐⭐⭐ 直接影响发布质量和效率,出问题就是线上事故 |
几个关键提醒:
- 签名文件是CD的命脉,管理不好整个流水线都跑不通。用CI凭据管理,绝不硬编码
- 审批卡点不是摆设,得配合变更报告让审批人真正审核
- 多环境配置要隔离,测试环境和生产环境的签名、部署方式完全不同
- 回滚方案必须提前准备,等出问题再想就晚了
- AppGallery API有坑,限流、接口变更、文档滞后,做好重试和兜底
CD流水线搭好了,你的发布流程就从"手工搬运"变成了"自动流水线"。下一步要做的,是优化构建本身的速度——毕竟构建太慢,CI/CD再流畅也白搭。
- 点赞
- 收藏
- 关注作者
评论(0)