Go 写 CLI 有个很舒服的地方:交叉编译比较简单。
1
2
3
| GOOS=linux GOARCH=amd64 go build -o dist/app-linux-amd64
GOOS=darwin GOARCH=arm64 go build -o dist/app-darwin-arm64
GOOS=windows GOARCH=amd64 go build -o dist/app-windows-amd64.exe
|
命令本身不难,但真到发布的时候,事情会变多:多平台构建、文件命名、压缩包、checksum、GitHub Release、changelog、tag 和版本号。
这些东西手写脚本当然也能做,但维护起来不太舒服。GoReleaser 适合解决的就是这类问题:把 Go 项目的发布流程配置化。
我对 GoReleaser 的理解不是“它帮我编译 Go 程序”,而是“它帮我把发布动作固定下来”。一次发布最容易出错的地方往往不是 go build,而是漏传一个平台、checksum 忘了生成、tag 和 changelog 对不上、Release 里残留了上一次的产物。
初始化配置
本地可以直接安装:
1
2
3
| go install github.com/goreleaser/goreleaser/v2@latest
goreleaser --version
|
在 Go CLI 项目根目录执行:
它会生成 .goreleaser.yaml。刚开始不要急着把配置写复杂。一个简单 CLI 项目,我通常先这样配:
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
| version: 2
project_name: mycli
before:
hooks:
- go mod tidy
builds:
- id: mycli
main: ./cmd/mycli
binary: mycli
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
archives:
- id: mycli
formats:
- tar.gz
format_overrides:
- goos: windows
formats:
- zip
name_template: >-
{{ .ProjectName }}_
{{ .Version }}_
{{ .Os }}_
{{ .Arch }}
checksum:
name_template: "checksums.txt"
snapshot:
version_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
|
main 是入口路径。很多 CLI 项目入口会放在 cmd/mycli,这里不要写错。
CGO_ENABLED=0 是为了尽量减少对系统动态库的依赖。不是所有项目都适合关 CGO,比如依赖 SQLite CGO 驱动的时候就要注意。
本地先跑 snapshot
第一次配 GoReleaser,不建议直接发布。可以先跑 snapshot:
1
| goreleaser release --snapshot --clean
|
这个命令不会创建正式 GitHub Release,更适合本地检查构建产物。
如果这里都构建不过,先别放到 CI 里折腾。大部分问题本地就能看出来,比如入口路径不对、CGO 配置不对、包名冲突、依赖下载失败。
我会把 snapshot 当成发布前演练。它的价值是让你在没有污染 GitHub Release 的情况下,先看到最终产物大概长什么样。
给 CLI 注入版本号
CLI 工具一般会有一个 version 命令,至少要能看到当前版本。
1
2
3
4
5
6
7
8
9
10
11
12
13
| package main
import "fmt"
var (
version = "dev"
commit = "none"
date = "unknown"
)
func main() {
fmt.Printf("version=%s commit=%s date=%s\n", version, commit, date)
}
|
然后在 GoReleaser 里通过 ldflags 注入:
1
2
3
4
5
6
7
8
9
10
11
| builds:
- id: mycli
main: ./cmd/mycli
binary: mycli
env:
- CGO_ENABLED=0
ldflags:
- -s -w
- -X main.version={{ .Version }}
- -X main.commit={{ .Commit }}
- -X main.date={{ .Date }}
|
这里有个细节:-X main.version=... 的包路径要和变量所在包匹配。如果变量不在 main 包,就不能照抄这个路径。
这个坑很常见。很多项目会把版本变量放到 internal/version,这时 -X main.version=... 看起来配置了,实际上没有注入成功。发布前一定要跑一下二进制的 version 命令,而不是只看构建成功。
接入 GitHub Actions
正式发布可以用 GitHub Actions 触发 tag。
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
| name: release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: stable
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
fetch-depth: 0 很重要。GoReleaser 需要读取 tag、commit 历史来生成版本和 changelog,浅克隆有时候会导致信息不完整。
触发条件是 tag:
1
2
| git tag v0.1.0
git push origin v0.1.0
|
发布前我一般会固定跑:
1
2
3
| go test ./...
goreleaser check
goreleaser release --snapshot --clean
|
GoReleaser 帮我省掉的不是一条 go build 命令,而是发布时那一串容易遗漏的小事:多平台构建、压缩包、checksum、Release 上传、版本号和 changelog。
如果项目依赖 CGO,我会先只发布自己能验证的平台。不要为了看起来完整,一上来就把 Linux、macOS、Windows、amd64、arm64 全打开。纯 Go 项目可以激进一点,CGO 项目要保守一点。
参考资料