zhangguanzhang's Blog

git工作流下golang项目的version信息该如何处理

字数统计: 1.8k阅读时长: 8 min
2020/04/27

来探讨下git工作流下golang项目的version信息该如何处理比较符合标准

之前自己写的几个简单的小工具都是没有把version信息注入到编译出来的文件里,发布版本多了后也无法溯源了.想着有必要看下这方面的知识了.

观察现有的一些

之前我博客docker panic那次事故就是根据docker info里的containerd的commit id找到了相关的代码来回溯.我们先看看各个项目的version信息

  • docker:

    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
    $ docker version
    Client: Docker Engine - Community
    Version: 19.03.2
    API version: 1.40
    Go version: go1.12.8
    Git commit: 6a30dfc
    Built: Thu Aug 29 05:28:55 2019
    OS/Arch: linux/amd64
    Experimental: false

    Server: Docker Engine - Community
    Engine:
    Version: 19.03.2
    API version: 1.40 (minimum version 1.12)
    Go version: go1.12.8
    Git commit: 6a30dfc
    Built: Thu Aug 29 05:27:34 2019
    OS/Arch: linux/amd64
    Experimental: false
    containerd:
    Version: 1.2.10
    GitCommit: b34a5c8af56e510852c35414db4c1f4fa6172339
    runc:
    Version: 1.0.0-rc8+dev
    GitCommit: 3e425f80a8c931f88e6d94a8c831b9d5aa481657
    docker-init:
    Version: 0.18.0
    GitCommit: fec3683
  • etcd:

    1
    2
    3
    4
    5
    $ etcd --version
    etcd Version: 3.3.13
    Git SHA: 98d3084
    Go Version: go1.10.8
    Go OS/Arch: linux/amd64
  • k8s:

    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
    $ kubectl version -o json
    {
    "clientVersion": {
    "major": "1",
    "minor": "13",
    "gitVersion": "v1.13.12",
    "gitCommit": "a8b52209ee172232b6db7a6e0ce2adc77458829f",
    "gitTreeState": "clean",
    "buildDate": "2019-10-15T12:12:15Z",
    "goVersion": "go1.11.13",
    "compiler": "gc",
    "platform": "linux/amd64"
    },
    "serverVersion": {
    "major": "1",
    "minor": "13",
    "gitVersion": "v1.13.12",
    "gitCommit": "a8b52209ee172232b6db7a6e0ce2adc77458829f",
    "gitTreeState": "clean",
    "buildDate": "2019-10-15T12:04:30Z",
    "goVersion": "go1.11.13",
    "compiler": "gc",
    "platform": "linux/amd64"
    }
    }

如何注入

对比下都有如下信息:

  • version
  • go version
  • arch info and os
  • git commit id
  • build date

github上一些star比较多的项目的makefile都是类似的如下内容

1
2
3
4
5
6
7
8
9
10
11
BUILD_VERSION   := $(shell cat version)
BUILD_TIME := $(shell date "+%F %T")
COMMIT_SHA1 := $(shell git rev-parse HEAD)

all:
gox -osarch="darwin/amd64 linux/386 linux/amd64" \
-output="dist/{{.Dir}}_{{.OS}}_{{.Arch}}" \
-tags="containers_image_openpgp" \
-ldflags "-X 'github.com/xxxx/xxxxx/cmd.version=${BUILD_VERSION}' \
-X 'github.com/xxxx/xxxx/cmd.buildTime=${BUILD_TIME}' \
-X 'github.com/xxxx/xxxx/cmd.commit=${COMMIT_SHA1}'"

搜索了下资料是通过go build -ldflags注入变量的,例如下面:

1
2
3
4
5
6
7
8
9
10
11
12
 $cat>test.go<<EOF
package main

var version string
func main(){
print(version)
}
EOF

$ go build -ldflags '-X main.version=v0.0.12' test.go
$ ./test # 下面是输出
v0.0.12

包名.变量名形式注入到编译过程里,可以覆盖掉原有变量的值,变量首字母可以不用大写也会注入.知道了实现方法,来思考下如何优雅.

市面上很多都是单独整了个version文件,直接从里面cat的.github上不少人这样version或者go文件里写版本号,xxx-dev,测试通过后再改下commit推上去,这样会多一个无用的commit

实际都是master代码测试完美了后发布tag,tag触发release.可以根据tag号做版本号来外部注入.总的来说就是k8s这方面最规范.去看它的源码.

从k8s源码学习

查看了下源码结构找到了核心部分staging/src/k8s.io/component-base/version/version.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func Get() apimachineryversion.Info {
// These variables typically come from -ldflags settings and in
// their absence fallback to the settings in ./base.go
return apimachineryversion.Info{
Major: gitMajor,
Minor: gitMinor,
GitVersion: gitVersion,
GitCommit: gitCommit,
GitTreeState: gitTreeState,
BuildDate: buildDate,
GoVersion: runtime.Version(),
Compiler: runtime.Compiler,
Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
}
}

以及文件staging/src/k8s.io/component-base/version/base.go里等待被注入的变量.查看了下makefile的逻辑,复杂的逻辑和变量处理是仍shell脚本里处理的,因为makefile并不是一个功能强的脚本语言.
脚本hack/lib/init.sh比较先执行,里面有执行

1
2
3
4
# The root of the build/dist directory
KUBE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)"
...
source "${KUBE_ROOT}/hack/lib/version.sh"

version脚本里逻辑可能对于非运维看比较麻烦,简单说下上面version信息:

  • Major就是大版本号,例如1.18.2就是1
  • Minor就是小版本号,例如1.18.2就是18
  • GitVersion就是release的版本,如果你是master代码下载编译则是最新的$release-dirty字样
  • GitCommit是取编译时候的git commit id
  • GitTreeState是你代码有没有commit,修改了代码没有commit则是dirty,你提issue社区人员看到这个字段值是dirty后不会过多理你来浪费时间
  • 其余几个没啥可说的

个人实现

我简单说下现在我推荐的逻辑判断:

  • 获取项目的目录,也就是上面的KUBE_ROOT设置下命令别名git,让git只对项目目录生效:
    1
    git=(git --work-tree "${KUBE_ROOT}") #后面使用git命令用:"${git[@]}"
  • 获取构建时间
    1
    BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
  • 获取当前HEAD的commit id
    1
    HEAD=$("${git[@]}" rev-parse "HEAD^{commit}")
  • tag名不为空的时候(指定编译之前的tag版本传入tag变量),或者ci是tag触发的.判断下该tag名存在否
    1
    2
    3
    4
    5
    6
    if [ -n "$TAG" ]; then
    TAG_COMMITID=$("${git[@]}" rev-parse $TAG 2>/dev/null)
    if [ "$?" -ne 0 ];then
    echo no such tag: $TAG
    exit 1
    fi
  • 否则tag为空下也就是master的代码,获取下离master最近的也就是最新的的tag号:
    1
    2
    3
    4
    else #默认取最新的tag
    TAG_COMMITID=$("${git[@]}" rev-list --tags --max-count=1)
    TAG=$("${git[@]}" describe --tags ${TAG_COMMITID})
    fi
  • 取tag号
    1
    2
    "${git[@]}" checkout $TAG 2>/dev/null
    BUILD_VERSION=${TAG}
  • 设置git tree state,没提交代码则是dirty
    1
    2
    3
    4
    5
    if [ -z "$("${git[@]}" status --porcelain 2>/dev/null)" ];then
    GIT_TREE_STATE='clean'
    else
    GIT_TREE_STATE='dirty'
    fi
  • 对比tag的commit id是否和head一致,这步是针对master的ci触发的version设置为上一个$tag-dirty的值
    1
    2
    3
    4
    5
    6
    7
    if [ "${HEAD}" != "${TAG_COMMITID}" ];then
    #tag的基础上改动,所以tag版本号-dirty
    BUILD_VERSION+="-dirty"
    COMMIT_ID=${HEAD}
    else
    COMMIT_ID=${TAG_COMMITID}
    fi
  • 最后git切回去,因为前面都是取变量的值,不会动代码
    1
    "${git[@]}" checkout $HEAD 2>/dev/null

版本号信息可以输出到一个gitignore的文件里,编译的时候读取文件里的值拼接成go build的参数.我个人实现可能不是最优,我个人的整个细节性都在我github上
https://github.com/zhangguanzhang/gonelist
入口脚本是 https://github.com/zhangguanzhang/gonelist/blob/master/build/build.sh

1
2
3
4
5
6
# 使用master的代码构建
bash build/build.sh build

# 自己编译指定tag版本的话
export TAG_NUM=xxx # 如果是tag推送,上面的逻辑处理就是tag的值不用声明,自己编译最新的 tag release 也不用声明变量
bash build/build.sh release

版本信息的打印我是一行一条,方便脚本来做升级比对的正则提取,花样多的话可以像k8s那样支持 yaml 和 json 格式输出啥的

2021/02/02 版本号的部分可以直接这样获取,下面是切到 1.16.0后修改文件,然后提交

1
2
3
4
5
6
7
8
9
10
11
$ git log
commit 25625cffd7066dded1157eebef7032bd43ca025e (HEAD)
Author: zhangguanzhang <zhangguanzhang@qq.com>
Date: Mon Feb 1 22:06:17 2021 -0500

add-dnsredir

commit 4e61de426bcf2300e2dee2388094312996454e28 (tag: 1.16.0)

$ git describe --tags --always --dirty
1.16.0-1-g25625cf
CATALOG
  1. 1. 观察现有的一些
  2. 2. 如何注入
  3. 3. 从k8s源码学习
  4. 4. 个人实现