HarmonyOS开发:版本号管理

举报
Jack20 发表于 2026/06/24 16:52:53 2026/06/24
【摘要】 HarmonyOS开发:版本号管理核心要点:版本号不只是个数字——语义化版本号+自动递增+多模块一致性,让每次发布都有迹可循,让版本混乱不再发生。 背景与动机你有没有遇到过这些场景?场景一:测试同学说"3.2.1版本有个bug",你问"是3.2.1-hotfix1还是3.2.1-hotfix2?",对方一脸懵。场景二:线上出了问题,你想回滚到上一个版本,但不知道上一个版本号是多少,翻了半天...

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.02.0.0]
    D --> G[1.0.01.1.0]
    E --> H[1.0.01.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.03.3.03.4.0
热修复版本: 3.2.03.2.13.2.2
开发版本: 3.3.0-dev.13.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对版本号管理的影响:

  1. API版本升级:HarmonyOS 6对应API 16+,targetAPIVersion需要更新为16。旧版API编译的应用在新系统上以兼容模式运行,可能影响性能。

  2. versionCode范围扩展:HarmonyOS 6支持更大的versionCode值(最大支持到2147483647),可以使用时间戳作为versionCode。

  3. 多形态版本管理:HarmonyOS 6支持一次开发多端部署,不同设备形态(手机、平板、穿戴)可以有独立的版本号。

  4. 版本回滚API:AppGallery Connect新增了版本回滚API,可以通过版本号精确指定回滚目标。

  5. 强制更新机制:HarmonyOS 6支持通过versionCode强制用户更新到指定版本,用于安全修复场景。

总结

版本号管理看似简单,实则是发布流程的基石。版本号混乱,后面的灰度发布、热修复、线上监控全都会受影响。

维度 评价
学习难度 ⭐⭐ 语义化版本规范本身很简单,难的是在团队中严格执行
使用频率 ⭐⭐⭐⭐⭐ 每次发布都涉及版本号,是发布流程的必经环节
重要程度 ⭐⭐⭐⭐ 版本号混乱不会直接导致崩溃,但会让发布流程一团糟

几个关键提醒:

  • versionCode每次必递增,这是AppGallery的硬性要求,忘了就上传不了
  • versionName统一格式,三位数字+可选预发布标识,不允许其他格式
  • 多模块版本必须一致,CI中加检查,不一致就报错
  • 自动递增优于手动,脚本递增不会忘、不会错
  • Git Tag和版本号同步,代码仓库的Tag必须和构建产物版本号对应

版本号管好了,下一步就是灰度发布——不是所有用户同时收到新版本,而是逐步放量,把风险控制在最小范围。

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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