Skip to content
Latchkey

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.

.github/workflows/ci.yml
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.

Related guides

Run this faster and cheaper on Latchkey managed runners. Start free →