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

来探讨下git工作流下golang项目的version信息该如何处理比较符合标准
之前自己写的几个简单的小工具都是没有把version信息注入到编译出来的文件里,发布版本多了后也无法溯源了.想着有必要看下这方面的知识了.

观察现有的一些

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

  • docker:

    $ 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:

    $ etcd --version
    etcd Version: 3.3.13
    Git SHA: 98d3084
    Go Version: go1.10.8
    Go OS/Arch: linux/amd64
    

  • k8s:

    $ 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
都是类似的如下内容

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
注入变量的,例如下面:

cat>test.go<<EOF
package main

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

go build -ldflags '-X main.a=tttt' test.go
./test # 下面是输出
tttt

包名.变量名
形式注入到编译过程里,可以覆盖掉原有变量的值,变量首字母可以不用大写也会注入.知道了实现方法,来思考下如何优雅.
市面上很多都是单独整了个version文件,直接从里面cat的.实际都是master代码测试完美了后发布tag,tag触发release.可以根据tag号做版本号来外部注入.总的来说就是k8s这方面最规范.去看它的源码.

从k8s源码学习

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

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
比较先执行,里面有执行

# 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社区人员看到后不会过多理你来浪费时间
  • 其余几个没啥可说的

个人实现

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

  • 获取项目的目录,也就是上面的 KUBE_ROOT
    设置下命令别名git,让git只对项目目录生效:

    git=(git --work-tree "${KUBE_ROOT}") #后面使用git命令用:"${git[@]}"
    

  • 获取构建时间

    BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
    

  • 获取当前HEAD的commit id

    HEAD=$("${git[@]}" rev-parse "HEAD^{commit}")
    

  • tag名不为空的时候(指定编译之前的tag版本传入tag变量),或者ci是tag触发的.判断下该tag名存在否

    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号:

    else #默认取最新的tag
      TAG_COMMITID=$("${git[@]}" rev-list --tags --max-count=1)
      TAG=$("${git[@]}" describe --tags ${TAG_COMMITID})
    fi
    

  • 取tag号

    "${git[@]}" checkout $TAG 2>/dev/null
    BUILD_VERSION=${TAG}
    

  • 设置git tree state,没提交代码则是dirty

    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
    的值

    if [ "${HEAD}" != "${TAG_COMMITID}" ];then
      #tag的基础上改动,所以tag版本号-dirty
      BUILD_VERSION+="-dirty"
      COMMIT_ID=${HEAD}
    else
      COMMIT_ID=${TAG_COMMITID}
    fi
    

  • 最后git切回去,因为前面都是取变量的值,不会动代码

    "${git[@]}" checkout $HEAD 2>/dev/null
    

整个细节性都在我github上

https://github.com/zhangguanzhang/gonelist

入口脚本是 https://github.com/zhangguanzhang/gonelist/blob/master/build/build.sh

# 使用master的代码构建
bash build/build.sh build

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