Distribuindo uma CLI em Go com GoReleaser
E aí, pessoal!
Você terminou de escrever uma CLI em Go. Funciona na sua máquina. Agora alguém pede: “onde baixo isso?”
Se a resposta for “clone o repo e roda go install”, você perdeu metade da audiência na hora. Usuário que não conhece Go não sabe o que fazer com isso. Usuário que conhece Go não quer fazer isso pra cada ferramenta nova que instala.
Neste post você vai construir uma CLI real, configurar o GoReleaser e ter um pipeline que gera binários para Linux, macOS e Windows automaticamente a cada tag no GitHub. Com um passo a mais, publica no Homebrew também.
A CLI que vamos distribuir
Vamos construir o chk, uma ferramenta que checa o status de endpoints HTTP e mostra código de status e latência. Simples o suficiente para o post não se perder na lógica da ferramenta, útil o suficiente para você querer instalar de verdade.
1
2
3
$ chk https://api.github.com https://google.com
200 45ms https://api.github.com
200 112ms https://google.com
Criando o projeto
1
2
3
mkdir chk && cd chk
go mod init github.com/seuusuario/chk
go get github.com/spf13/cobra@latest
Estrutura do projeto:
1
2
3
4
5
6
chk/
main.go
cmd/
root.go
go.mod
go.sum
Implementando a CLI com Cobra
1
2
3
4
5
6
7
8
9
10
11
// main.go
package main
import "github.com/seuusuario/chk/cmd"
var version = "dev"
func main() {
cmd.SetVersion(version)
cmd.Execute()
}
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
// cmd/root.go
package cmd
import (
"fmt"
"net/http"
"os"
"time"
"github.com/spf13/cobra"
)
var (
timeout int
Version string
)
var rootCmd = &cobra.Command{
Use: "chk [urls...]",
Short: "Checa o status de endpoints HTTP",
Args: cobra.MinimumNArgs(1),
Run: run,
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
func SetVersion(v string) {
Version = v
rootCmd.Version = v
}
func init() {
rootCmd.Flags().IntVarP(&timeout, "timeout", "t", 5, "timeout em segundos")
}
func run(cmd *cobra.Command, args []string) {
client := &http.Client{
Timeout: time.Duration(timeout) * time.Second,
}
for _, url := range args {
start := time.Now()
resp, err := client.Get(url)
elapsed := time.Since(start)
if err != nil {
fmt.Printf("ERR %-8s %s\n", elapsed.Round(time.Millisecond), url)
continue
}
resp.Body.Close()
fmt.Printf("%-3d %-8s %s\n", resp.StatusCode, elapsed.Round(time.Millisecond), url)
}
}
Testa localmente:
1
2
go run . https://api.github.com
# 200 44ms https://api.github.com
O problema do release manual
Sem GoReleaser, o processo de release de uma CLI Go para três plataformas é assim:
1
2
3
4
5
6
7
8
9
10
11
12
13
GOOS=linux GOARCH=amd64 go build -o chk-linux-amd64 .
GOOS=darwin GOARCH=amd64 go build -o chk-darwin-amd64 .
GOOS=darwin GOARCH=arm64 go build -o chk-darwin-arm64 .
GOOS=windows GOARCH=amd64 go build -o chk-windows-amd64.exe .
tar -czf chk-linux-amd64.tar.gz chk-linux-amd64
tar -czf chk-darwin-amd64.tar.gz chk-darwin-amd64
# ...
# criar release no GitHub
# fazer upload de cada arquivo
# escrever changelog na mão
# atualizar formula do Homebrew
Isso antes de qualquer automação. Com GoReleaser, o mesmo resultado sai de um git tag.
Instalando o GoReleaser
1
go install github.com/goreleaser/goreleaser/v2@latest
Gera a config inicial:
1
goreleaser init
Configurando o .goreleaser.yaml
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
# .goreleaser.yaml
version: 2
before:
hooks:
- go mod tidy
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
ldflags:
- -s -w -X main.version=
archives:
- formats:
- tar.gz
name_template: "__"
format_overrides:
- goos: windows
formats:
- zip
checksum:
name_template: "checksums.txt"
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
- "^ci:"
Testa o build local sem publicar:
1
goreleaser build --snapshot --clean
O resultado fica em dist/:
1
2
3
4
5
dist/
chk_linux_amd64/chk
chk_darwin_amd64/chk
chk_darwin_arm64/chk
chk_windows_amd64/chk.exe
GitHub Actions para release automático
Cria o arquivo .github/workflows/release.yml:
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
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: stable
- uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --clean
env:
GITHUB_TOKEN: $
O GITHUB_TOKEN já está disponível automaticamente no GitHub Actions. Não precisa criar nenhum secret manualmente.
Publicando no Homebrew
Para que usuários de macOS e Linux possam instalar com brew install, você precisa de um repositório de fórmulas separado.
Cria um repositório público no GitHub chamado homebrew-tap.
Adiciona a seção no .goreleaser.yaml:
1
2
3
4
5
6
7
8
9
10
brews:
- name: chk
repository:
owner: seuusuario
name: homebrew-tap
homepage: "https://github.com/seuusuario/chk"
description: "Checa o status de endpoints HTTP"
commit_author:
name: goreleaserbot
email: bot@goreleaser.com
Para o GoReleaser conseguir escrever nesse repositório, cria um Personal Access Token com permissão repo e adiciona como secret no repositório:
1
2
3
Settings > Secrets and variables > Actions > New repository secret
Nome: HOMEBREW_TAP_TOKEN
Valor: <seu token>
Atualiza o workflow para passar o token:
1
2
3
env:
GITHUB_TOKEN: $
HOMEBREW_TAP_TOKEN: $
E referencia no .goreleaser.yaml:
1
2
3
brews:
- repository:
token: ""
Fazendo o primeiro release
Com tudo configurado, o release inteiro sai de dois comandos:
1
2
git tag v0.1.0
git push origin v0.1.0
O GitHub Actions dispara, o GoReleaser roda e em alguns minutos você tem:
- binários para Linux, macOS (amd64 + arm64) e Windows
checksums.txtpara verificação de integridade- GitHub Release criado automaticamente com changelog gerado a partir dos commits
- fórmula do Homebrew atualizada no repositório
homebrew-tap
Quem usa macOS ou Linux instala assim:
1
brew install seuusuario/tap/chk
Quem não usa Homebrew baixa o binário direto do GitHub Releases e coloca no PATH.
O que você ganha de graça
Com essa configuração, cada novo git tag gera:
- build para 6 combinações de OS e arquitetura (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64, windows/arm64)
- arquivos comprimidos (.tar.gz para Unix, .zip para Windows)
checksums.txtcom hash SHA256 de cada arquivo- GitHub Release com changelog baseado nos commits desde a última tag
- fórmula Homebrew atualizada automaticamente
O changelog é gerado a partir dos commits. Se você seguir Conventional Commits (feat:, fix:, docs:), ele agrupa automaticamente por categoria.
Conclusão
GoReleaser elimina o trabalho manual de release que ninguém quer fazer. O setup inicial leva menos de uma hora e a partir daí cada versão nova sai com um git tag.
Para uma CLI Go que você quer distribuir de verdade, esse pipeline é o mínimo razoável. Quem usa macOS instala pelo Homebrew. Quem usa Linux pega o binário direto. E você não precisa tocar em nada disso depois que está configurado.
