CI/CD for a Go App with golangci-lint and GoReleaser and GitHub Actions
Lint and test on PRs, then cross-compile and release with GoReleaser on a tag.
golangci-lint runs many Go linters in one pass, and GoReleaser cross-compiles and publishes release archives, checksums, and a changelog. This recipe lints and tests on every push and releases on a tag.
What the pipeline does
- set up Go with module caching
- lint with golangci-lint
- run go test
- on a tag, cross-compile with GoReleaser
- publish archives and checksums to the GitHub release
The workflow
Two jobs: a test job on every push and a release job that runs only on a v* tag and needs the test job. GoReleaser reads .goreleaser.yaml for build and archive config.
name: CI
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.23"
cache: true
- uses: golangci/golangci-lint-action@v6
- run: go test ./...
release:
needs: test
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: "1.23"
- uses: goreleaser/goreleaser-action@v6
with:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Caching and speed
setup-go cache: true restores the module and build cache; golangci-lint-action caches its analysis cache. GoReleaser cross-compiles many OS/arch targets, which is CPU heavy, so running on cheaper managed runners such as Latchkey (around 69% cheaper than GitHub-hosted) keeps releases affordable, and auto-retry covers a flaky upload.
Deploying
GoReleaser uploads archives, checksums, and a changelog to the GitHub release matching the tag. fetch-depth: 0 is required so it can build the changelog from history. Add brews or nfpms config to also publish a Homebrew tap or deb/rpm packages.
Key takeaways
- golangci-lint runs many linters in a single fast pass.
- Gate the release job on a v* tag and on the test job passing.
- GoReleaser needs fetch-depth: 0 to build the changelog.