项目版本号自动化实践

背景

项目组基于Cronet自研的产品每次对外发布对应一个版本号,在此之前版本号都是手动由Release Engineer进行填写,并在代码中用到了版本号的地方进行相应修改。

现状
版本号分为4位,前两位对应一次发布,第三位为patch(从1开始),第四位没有作用,默认为1。例如1.5.2.1,即表示我们2016.12月份发布的1.5版本,其中进行了一次patch,因此第三位为2。

版本号由Release Engineer在组内确认完后,由于目前部分模块均在代码中写死了版本号,需要去手动调整后再commit一次,才能发布。

总之,这块存在两个问题。

  1. 我们对四位版本号的使用不合理,实际上只用到了其中三位。
  2. 每次发布时,为了版本号需要人工去调整代码,这显然是很难忍受的。

目标
为了提高效率,我们将现状和需求分析了下,分为两个具体要做的事:

  • 提供统一的版本号配置管理,只需修改版本号配置,便可应用到整个代码库。
  • 尽量将版本号做成自动化无需人工干预的。

解决方案

1.明确四位版本号的每一位用途
可参考chromium对于四位版本号的解释
chromium/version-numbers
概括来说:
四位版本号由 MAJOR.MINOR.BUILD.PATCH组成
MAJOR and Minor: 每次版本发布时update,这个跟我们现在是一样的。
BUILD:在trunk上(应该可以理解为代码库里主干和所有分支)有构建的时候,永久性自增1
PATCH:在构建分支上进行编译,则自增1

参考chromium做法,再结合我们自身需求。我们可以有如下规则:
版本号前两位MAJOR.MINOR对应一次发布,无需变化。
使用第三位作为BUILD,任何一次在agile上触发的编译均会将BUILD永久性+1,初始值为1024(看了一下我们已有的编译已经超过了1000次)。
使用第四位作为PATCH,每个MAJOR.MINOR都有自己的PATCH,初始值为1,任何一次该版本的agile编译,将PATCH+1。

2.版本号统一配置管理,与具体代码解耦
我们参考了chromium原生的版本号宏的生成方案,通过读取一个配置文件中的版本号,在编译时生成一个包含#define CRONET_VERSION 1.6.x.x类似这种宏的头文件,所有需要用到版本号的代码只需获取该宏即可。
具体可参考代码库中:baidu/version.gni
这样做完后,RE无需去手动修改代码,而只要调整配置文件,即可全局生效。

3.自动化版本号
在第一步问题解决后,还是不太能说服大家这个有多好用,我们发现版本号的BUILD & PATCH都跟编译次数相关,如果能将其做成自动化的,就更好了。

这块自动化的做法是:
我们自维护的编译server维护了一块数据存储BUILD号 以及 MAJOR.MINOR对应的PATCH,每次编译开始前,根据存储的数据将代码中的版本号配置文件修改,在编译时就生效了。版本文件在编译成功后会作为编译产物之一返回,方便大家确认版本。

这是编译server与版本号相关代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Author:
# Created Time: 2017/03/12 08:42
# File Name: version.py
"""
Description: it is used to generate version for TurboNet.
"""
# Copyright (c) 2017 Baidu.com, Inc. All Rights Reserved
#
import os
import sys
import sqlite3
from config import BUILD_VERSION
class CroNetVersion(object):
"""Summary of class here
Generate build/patch id for CroNet.
"""
def __init__(self):
self.dbfile = os.path.split(os.path.realpath(__file__))[0] + '/CroNetVersion.db'
print 'self.dbfile path: ' + self.dbfile
self.db = sqlite3.connect(self.dbfile)
cursor = self.db.cursor()
try:
create_build_cmd = '''
CREATE TABLE IF NOT EXISTS build
(
ID INTEGER,
BUILD_NUM INTEGER
);
'''
cursor.execute(create_build_cmd)
create_patch_cmd = '''
CREATE TABLE IF NOT EXISTS patch
(
ID TEXT,
PATCH_NUM INTEGER
);
'''
cursor.execute(create_patch_cmd)
cursor.execute("SELECT BUILD_NUM FROM build")
lens = len(cursor.fetchall())
if lens == 0:
insert_sql = '''
INSERT INTO build(ID, BUILD_NUM) values (?, ?)
'''
cursor.execute(insert_sql, (1, BUILD_VERSION))
self.db.commit()
except Exception as e:
print 'Create table failed: ' + str(e.message)
result = cursor.execute("SELECT BUILD_NUM FROM build")
print 'The initial build id is: ' + str(result.fetchone()[0])
cursor.close()
def get_build_num(self):
"""Summary of function here
Get the last build_num.
"""
query_sql = 'SELECT BUILD_NUM FROM build WHERE ID = 1'
cursor = self.db.cursor()
result = cursor.execute(query_sql).fetchone()
cursor.close()
return result[0]
def update_build_num(self):
"""Summary of function here
Update the build_num.
"""
build_num = self.get_build_num()
update_sql = 'UPDATE build SET BUILD_NUM = ? WHERE ID = 1'
data = int(build_num) + 1
cursor = self.db.cursor()
cursor.execute(update_sql, (data, ))
self.db.commit()
cursor.close()
def get_patch_num(self, id):
"""Summary of function here
Get the PATCH_NUM according to it's id.
Return the patch num if it is existed, else return 0
"""
query_sql = 'SELECT PATCH_NUM FROM patch WHERE ID = ?'
cursor = self.db.cursor()
result = cursor.execute(query_sql, (id, )).fetchone()
cursor.close()
if result:
return result[0]
else:
return 1
def update_patch_num(self, id):
"""Summary of function here
Update the patch_num.
"""
patch_num = self.get_patch_num(id)
data = int(patch_num) + 1
cursor = self.db.cursor()
if patch_num > 1:
update_sql = 'UPDATE patch SET PATCH_NUM = ? WHERE ID = ?'
cursor.execute(update_sql, (data, id))
else:
insert_sql = 'INSERT INTO patch(ID, PATCH_NUM) values (?, ?)'
cursor.execute(insert_sql, (id, data))
self.db.commit()
cursor.close()
'''
def test():
tv = CroNetVersion()
print tv.get_build_num()
tv.update_build_num()
print tv.get_build_num()
print tv.get_patch_num('2.0')
tv.update_patch_num('2.0')
print tv.get_patch_num('2.0')
tv.update_patch_num('2.0')
print tv.get_patch_num('2.0')
tv.update_patch_num('2.0')
print tv.get_patch_num('2.0')
if __name__ == "__main__":
test()
'''

最终效果

version_header
会自动生成该头文件,任何需要使用到版本号的代码,仅需要include该头文件即可使用统一版本号。

收益总结

  • 标准化了四位版本号的使用,之前只用到了前三位的,这是不正规的。

  • 如果按照严格标准,可以将我们对代码库的修改文件次数由N*M(N代表一个版本内所有commit数目,M代表使用了版本号的文件数,目前M=3)降低到 1次。

  • 统一后,版本号采用宏来定义,通过编译依赖和头文件获取,看起来比较优雅。