HarmonyOS开发:版本号管理
HarmonyOS开发:版本号管理
核心要点:版本号不只是个数字——语义化版本号+自动递增+多模块一致性,让每次发布都有迹可循,让版本混乱不再发生。
背景与动机
你有没有遇到过这些场景?
场景一:测试同学说"3.2.1版本有个bug",你问"是3.2.1-hotfix1还是3.2.1-hotfix2?",对方一脸懵。
场景二:线上出了问题,你想回滚到上一个版本,但不知道上一个版本号是多少,翻了半天Git记录才找到。
场景三:多模块项目,entry模块版本号是3.2.1,feature模块还是3.1.0,用户安装后功能对不上,直接崩溃。
这些问题归根结底都是一件事:版本号管理混乱。
版本号看起来简单,不就是递增的数字吗?但一旦涉及多人协作、多模块、多环境、热修复,版本号管理就变得非常复杂。没有规范,迟早乱套。
核心原理
语义化版本(Semantic Versioning,SemVer)是目前最通用的版本号规范,格式为 MAJOR.MINOR.PATCH:
flowchart LR
A[版本号变更] --> B{变更类型}
B -->|不兼容的API修改| C[MAJOR +1]
B -->|向后兼容的功能新增| D[MINOR +1]
B -->|向后兼容的Bug修复| E[PATCH +1]
C --> F[1.0.0 → 2.0.0]
D --> G[1.0.0 → 1.1.0]
E --> H[1.0.0 → 1.0.1]
I[预发布版本] --> J[1.0.0-alpha.1]
I --> K[1.0.0-beta.2]
I --> L[1.0.0-rc.1]
M[构建元数据] --> N[1.0.0+build.123]
classDef major fill:#FF7675,stroke:#D63031,color:#fff
classDef minor fill:#FDCB6E,stroke:#F0B429,color:#333
classDef patch fill:#55EFC4,stroke:#00B894,color:#333
classDef pre fill:#74B9FF,stroke:#0984E3,color:#fff
classDef meta fill:#A29BFE,stroke:#6C5CE7,color:#fff
classDef decision fill:#FFEAA7,stroke:#FDCB6E,color:#333
class B decision
class C,F major
class D,G minor
class E,H patch
class I,J,K,L pre
class M,N meta
HarmonyOS版本号规范:
| 字段 | 位置 | 说明 | 示例 |
|---|---|---|---|
| versionCode | build-profile.json5 | 整数,每次发布递增,用于系统判断版本新旧 | 100 |
| versionName | build-profile.json5 | 字符串,展示给用户的版本号 | “3.2.1” |
| minAPIVersion | build-profile.json5 | 最低兼容API版本 | 12 |
| targetAPIVersion | build-profile.json5 | 目标API版本 | 14 |
versionCode和versionName的区别:
versionCode是给系统看的,必须是递增整数,AppGallery用它判断哪个版本更新versionName是给用户看的,可以是任意字符串,展示在应用信息页
两者必须同步更新,但规则不同:versionCode每次发布+1,versionName按语义化规则变更。
代码实战
基础用法:HarmonyOS版本号配置
版本号在build-profile.json5中配置,这是鸿蒙项目的标准位置。
// build-profile.json5 - 版本号配置
{
"app": {
"products": [
{
"name": "default",
"signingConfig": "default",
// 版本号配置
"versionCode": 100, // 必须是正整数,每次发布递增
"versionName": "3.2.1", // 展示给用户的版本号
"minAPIVersion": 12, // 最低兼容API 12(HarmonyOS 4.0)
"targetAPIVersion": 14, // 目标API 14(HarmonyOS 5.0)
"compatibleSdkVersion": "5.0.0(12)", // 兼容SDK版本
"runtimeSdkVersion": "5.0.0(14)", // 运行时SDK版本
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": ["default"]
}
]
}
]
}
在代码中读取版本号:
// entry/src/main/ets/utils/VersionUtil.ets
// 版本号工具类
import { bundleManager } from '@kit.AbilityKit';
export class VersionUtil {
private static cachedVersionName: string = '';
private static cachedVersionCode: number = 0;
/**
* 获取应用版本名称(如 "3.2.1")
*/
static async getVersionName(): Promise<string> {
if (this.cachedVersionName) {
return this.cachedVersionName;
}
try {
const bundleInfo = await bundleManager.getBundleInfoForSelf(
bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
);
this.cachedVersionName = bundleInfo.versionName;
return this.cachedVersionName;
} catch (error) {
console.error('获取版本名称失败:', error);
return 'unknown';
}
}
/**
* 获取应用版本号(如 100)
*/
static async getVersionCode(): Promise<number> {
if (this.cachedVersionCode > 0) {
return this.cachedVersionCode;
}
try {
const bundleInfo = await bundleManager.getBundleInfoForSelf(
bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
);
this.cachedVersionCode = bundleInfo.versionCode;
return this.cachedVersionCode;
} catch (error) {
console.error('获取版本号失败:', error);
return 0;
}
}
/**
* 比较版本号大小
* 返回: 1表示v1>v2, -1表示v1<v2, 0表示相等
*/
static compareVersions(v1: string, v2: string): number {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const p1 = parts1[i] || 0;
const p2 = parts2[i] || 0;
if (p1 > p2) return 1;
if (p1 < p2) return -1;
}
return 0;
}
/**
* 格式化版本信息用于展示
*/
static async getFormattedVersion(): Promise<string> {
const name = await this.getVersionName();
const code = await this.getVersionCode();
return `v${name} (${code})`;
}
}
进阶用法:版本号自动递增
手动改版本号容易忘、容易错。用脚本自动递增,CI流水线每次构建自动更新。
# bump_version.py - 版本号自动递增脚本
import re
import sys
import json
import argparse
from pathlib import Path
class VersionBumper:
"""鸿蒙项目版本号自动递增工具"""
def __init__(self, project_root: str):
self.project_root = Path(project_root)
self.build_profile_path = self.project_root / 'build-profile.json5'
if not self.build_profile_path.exists():
raise FileNotFoundError(f"build-profile.json5 不存在: {self.build_profile_path}")
def read_version(self) -> dict:
"""读取当前版本号"""
content = self.build_profile_path.read_text(encoding='utf-8')
# 解析json5(简化处理,去除注释)
# 生产环境建议用json5库
content_clean = re.sub(r'//.*?\n', '\n', content) # 去除单行注释
content_clean = re.sub(r'/\*.*?\*/', '', content_clean, flags=re.DOTALL) # 去除多行注释
# 提取versionCode和versionName
version_code_match = re.search(r'"versionCode"\s*:\s*(\d+)', content_clean)
version_name_match = re.search(r'"versionName"\s*:\s*"([^"]+)"', content_clean)
if not version_code_match or not version_name_match:
raise ValueError("无法从build-profile.json5中解析版本号")
return {
'versionCode': int(version_code_match.group(1)),
'versionName': version_name_match.group(1)
}
def bump_version(self, bump_type: str, pre_release: str = None) -> dict:
"""
递增版本号
Args:
bump_type: 'major' | 'minor' | 'patch' | 'code'
pre_release: 预发布标识,如 'alpha.1', 'beta.2'
"""
current = self.read_version()
old_version_name = current['versionName']
old_version_code = current['versionCode']
# 解析版本号
version_parts = old_version_name.split('-')[0].split('.')
major = int(version_parts[0])
minor = int(version_parts[1]) if len(version_parts) > 1 else 0
patch = int(version_parts[2]) if len(version_parts) > 2 else 0
# 根据类型递增
if bump_type == 'major':
major += 1
minor = 0
patch = 0
elif bump_type == 'minor':
minor += 1
patch = 0
elif bump_type == 'patch':
patch += 1
elif bump_type == 'code':
# 只递增versionCode,不改versionName
pass
new_version_name = f"{major}.{minor}.{patch}"
if pre_release:
new_version_name += f"-{pre_release}"
new_version_code = old_version_code + 1
# 写入文件
self._update_build_profile(new_version_code, new_version_name)
print(f"版本号更新: {old_version_name}({old_version_code}) → {new_version_name}({new_version_code})")
return {
'oldVersionName': old_version_name,
'oldVersionCode': old_version_code,
'newVersionName': new_version_name,
'newVersionCode': new_version_code
}
def _update_build_profile(self, version_code: int, version_name: str):
"""更新build-profile.json5中的版本号"""
content = self.build_profile_path.read_text(encoding='utf-8')
# 替换versionCode
content = re.sub(
r'"versionCode"\s*:\s*\d+',
f'"versionCode": {version_code}',
content
)
# 替换versionName
content = re.sub(
r'"versionName"\s*:\s*"[^"]*"',
f'"versionName": "{version_name}"',
content
)
self.build_profile_path.write_text(content, encoding='utf-8')
def main():
parser = argparse.ArgumentParser(description='鸿蒙版本号管理工具')
parser.add_argument('--bump', choices=['major', 'minor', 'patch', 'code'],
required=True, help='版本号递增类型')
parser.add_argument('--pre-release', type=str, default=None,
help='预发布标识 (如 alpha.1, beta.2)')
parser.add_argument('--project-root', type=str, default='.',
help='项目根目录路径')
args = parser.parse_args()
bumper = VersionBumper(args.project_root)
result = bumper.bump_version(args.bump, args.pre_release)
# 输出结果供CI使用
print(f"::set-output name=new_version_name::{result['newVersionName']}")
print(f"::set-output name=new_version_code::{result['newVersionCode']}")
if __name__ == '__main__':
main()
CI中集成版本号自动递增:
# .gitlab-ci.yml - 版本号自动递增
bump_version:
stage: prepare
script:
# PATCH版本自动递增(每次合并到main分支)
- python3 scripts/bump_version.py --bump patch --project-root .
- git config user.name "CI Bot"
- git config user.email "ci-bot@your-company.com"
- git add build-profile.json5
- git commit -m "chore: bump version to $(grep versionName build-profile.json5 | head -1)"
- git push origin main
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
changes:
- entry/src/**/*
完整示例:多模块版本一致性管理
多模块项目最大的版本管理难题:各模块版本号不一致。entry模块3.2.1,feature模块3.1.0,用户安装后功能对不上。
// scripts/version_sync.ets - 多模块版本一致性检查工具
// 在CI中运行,确保所有模块版本号一致
interface ModuleVersion {
moduleName: string;
versionCode: number;
versionName: string;
}
class VersionSyncChecker {
private projectRoot: string;
constructor(projectRoot: string) {
this.projectRoot = projectRoot;
}
/**
* 检查所有模块版本一致性
*/
checkVersionConsistency(): { consistent: boolean; modules: ModuleVersion[]; errors: string[] } {
const modules: ModuleVersion[] = [];
const errors: string[] = [];
// 1. 读取主模块版本号(作为基准)
const mainVersion = this.readModuleVersion('entry');
if (!mainVersion) {
errors.push('无法读取entry模块版本号');
return { consistent: false, modules, errors };
}
modules.push({
moduleName: 'entry',
versionCode: mainVersion.versionCode,
versionName: mainVersion.versionName
});
// 2. 读取其他模块版本号
const featureModules = this.findFeatureModules();
for (const moduleName of featureModules) {
const version = this.readModuleVersion(moduleName);
if (!version) {
errors.push(`无法读取${moduleName}模块版本号`);
continue;
}
modules.push({
moduleName,
versionCode: version.versionCode,
versionName: version.versionName
});
// 3. 比较版本号
if (version.versionCode !== mainVersion.versionCode) {
errors.push(
`${moduleName}模块versionCode(${version.versionCode})与entry模块(${mainVersion.versionCode})不一致`
);
}
if (version.versionName !== mainVersion.versionName) {
errors.push(
`${moduleName}模块versionName(${version.versionName})与entry模块(${mainVersion.versionName})不一致`
);
}
}
return {
consistent: errors.length === 0,
modules,
errors
};
}
/**
* 读取模块版本号
*/
private readModuleVersion(moduleName: string): { versionCode: number; versionName: string } | null {
// 从module.json5读取版本号
// 实际实现需要读取文件并解析
return null;
}
/**
* 发现所有功能模块
*/
private findFeatureModules(): string[] {
// 扫描项目目录,发现feature模块
return [];
}
}
多模块版本同步脚本(Python版,在CI中运行):
# sync_module_versions.py - 多模块版本号同步工具
import json
import re
from pathlib import Path
class ModuleVersionSync:
"""多模块版本号同步工具"""
def __init__(self, project_root: str):
self.project_root = Path(project_root)
def sync_versions(self) -> dict:
"""将entry模块的版本号同步到所有子模块"""
# 1. 读取entry模块版本号(基准)
entry_profile = self.project_root / 'entry' / 'oh-package.json5'
entry_version = self._read_version(entry_profile)
if not entry_version:
return {'success': False, 'error': '无法读取entry模块版本号'}
print(f"📋 基准版本: {entry_version['versionName']} ({entry_version['versionCode']})")
# 2. 找到所有子模块
synced_modules = []
failed_modules = []
for module_dir in self.project_root.iterdir():
if not module_dir.is_dir():
continue
oh_package = module_dir / 'oh-package.json5'
if not oh_package.exists():
continue
if module_dir.name == 'entry':
continue # 跳过entry自身
# 3. 同步版本号
try:
self._update_version(oh_package, entry_version)
synced_modules.append(module_dir.name)
print(f" ✅ {module_dir.name}: 版本号已同步")
except Exception as e:
failed_modules.append({'module': module_dir.name, 'error': str(e)})
print(f" ❌ {module_dir.name}: 同步失败 - {e}")
return {
'success': len(failed_modules) == 0,
'baseVersion': entry_version,
'syncedModules': synced_modules,
'failedModules': failed_modules
}
def _read_version(self, file_path: Path) -> dict | None:
"""读取模块版本号"""
if not file_path.exists():
return None
content = file_path.read_text(encoding='utf-8')
version_match = re.search(r'"version"\s*:\s*"([^"]+)"', content)
if version_match:
return {'versionName': version_match.group(1)}
return None
def _update_version(self, file_path: Path, version: dict):
"""更新模块版本号"""
content = file_path.read_text(encoding='utf-8')
content = re.sub(
r'"version"\s*:\s*"[^"]*"',
f'"version": "{version["versionName"]}"',
content
)
file_path.write_text(content, encoding='utf-8')
if __name__ == '__main__':
sync = ModuleVersionSync('.')
result = sync.sync_versions()
if result['success']:
print(f"\n✅ 版本号同步完成,共同步 {len(result['syncedModules'])} 个模块")
else:
print(f"\n❌ 版本号同步失败")
for fail in result['failedModules']:
print(f" {fail['module']}: {fail['error']}")
踩坑与注意事项
坑1:versionCode忘了递增
AppGallery要求新版本的versionCode必须大于旧版本,否则无法上传。手动改很容易忘。
解决方案:CI流水线自动递增。
# CI中自动递增versionCode
python3 scripts/bump_version.py --bump code
# 或者用sed直接修改
CURRENT_CODE=$(grep '"versionCode"' build-profile.json5 | grep -o '[0-9]*')
NEW_CODE=$((CURRENT_CODE + 1))
sed -i "s/\"versionCode\": $CURRENT_CODE/\"versionCode\": $NEW_CODE/" build-profile.json5
坑2:热修复版本号冲突
线上3.2.0出了bug,你发了3.2.1修复。但3.2.1的开发版已经在测试了,两个3.2.1撞车了。
解决方案:热修复版本用额外的标识区分。
正常版本: 3.2.0 → 3.3.0 → 3.4.0
热修复版本: 3.2.0 → 3.2.1 → 3.2.2
开发版本: 3.3.0-dev.1 → 3.3.0-dev.2
或者使用versionCode来区分:热修复的versionCode单独递增,不受开发版本影响。
坑3:多模块版本号不同步
entry模块改了版本号,feature模块忘了改,发布后功能异常。
解决方案:CI中增加版本一致性检查。
# .gitlab-ci.yml
check_version_consistency:
stage: validate
script:
- python3 scripts/sync_module_versions.py --check-only
rules:
- if: '$CI_PIPELINE_SOURCE == "push"'
坑4:versionName格式不统一
有人写"3.2.1",有人写"v3.2.1",有人写"3.2.1.0",格式混乱导致比较逻辑出错。
解决方案:统一规范,CI中校验格式。
import re
def validate_version_name(version_name: str) -> bool:
"""校验版本号格式是否符合规范"""
# 必须是 MAJOR.MINOR.PATCH 格式,可选预发布标识
pattern = r'^\d+\.\d+\.\d+(-[a-zA-Z]+\.\d+)?$'
return bool(re.match(pattern, version_name))
# 测试
assert validate_version_name("3.2.1") == True
assert validate_version_name("3.2.1-alpha.1") == True
assert validate_version_name("v3.2.1") == False # 不允许v前缀
assert validate_version_name("3.2") == False # 必须三位
坑5:Git Tag和版本号不一致
代码仓库打了tag v3.2.1,但build-profile.json5里写的还是3.2.0。
解决方案:从Git Tag自动同步版本号。
#!/bin/bash
# sync_version_from_tag.sh - 从Git Tag同步版本号
# 获取最新的版本Tag
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null)
if [ -z "$LATEST_TAG" ]; then
echo "没有找到版本Tag"
exit 0
fi
# 去掉v前缀
VERSION=${LATEST_TAG#v}
echo "最新Tag: $LATEST_TAG, 版本号: $VERSION"
# 更新build-profile.json5
sed -i "s/\"versionName\": \"[^\"]*\"/\"versionName\": \"${VERSION}\"/" build-profile.json5
echo "✅ 版本号已同步: $VERSION"
HarmonyOS 6适配说明
HarmonyOS 6对版本号管理的影响:
-
API版本升级:HarmonyOS 6对应API 16+,
targetAPIVersion需要更新为16。旧版API编译的应用在新系统上以兼容模式运行,可能影响性能。 -
versionCode范围扩展:HarmonyOS 6支持更大的versionCode值(最大支持到2147483647),可以使用时间戳作为versionCode。
-
多形态版本管理:HarmonyOS 6支持一次开发多端部署,不同设备形态(手机、平板、穿戴)可以有独立的版本号。
-
版本回滚API:AppGallery Connect新增了版本回滚API,可以通过版本号精确指定回滚目标。
-
强制更新机制:HarmonyOS 6支持通过versionCode强制用户更新到指定版本,用于安全修复场景。
总结
版本号管理看似简单,实则是发布流程的基石。版本号混乱,后面的灰度发布、热修复、线上监控全都会受影响。
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐ 语义化版本规范本身很简单,难的是在团队中严格执行 |
| 使用频率 | ⭐⭐⭐⭐⭐ 每次发布都涉及版本号,是发布流程的必经环节 |
| 重要程度 | ⭐⭐⭐⭐ 版本号混乱不会直接导致崩溃,但会让发布流程一团糟 |
几个关键提醒:
- versionCode每次必递增,这是AppGallery的硬性要求,忘了就上传不了
- versionName统一格式,三位数字+可选预发布标识,不允许其他格式
- 多模块版本必须一致,CI中加检查,不一致就报错
- 自动递增优于手动,脚本递增不会忘、不会错
- Git Tag和版本号同步,代码仓库的Tag必须和构建产物版本号对应
版本号管好了,下一步就是灰度发布——不是所有用户同时收到新版本,而是逐步放量,把风险控制在最小范围。
- 点赞
- 收藏
- 关注作者
评论(0)