DEV Community

Ticat Wolves
Ticat Wolves

Posted on

Automate Your Go Project: Best Practices & CI/CD with GitHub Actions

You've built something cool in Go (Golang) — maybe a library, a CLI tool, or an API.
Now you want to take it to the next level: package it properly and publish it so others can use it.

That's where Continuous Integration and Continuous Deployment (CI/CD) comes in. It takes your local code and turns it into a repeatable, automated release pipeline, making your Go project reliable and easy to maintain.

In this post, I'll walk you through how to build, test, and publish a Go package using GitHub Actions. We'll also use semantic versioning, linting, and a few good practices that help keep your releases clean and consistent.

What You'll Learn


Before we start, it'll help if you're already familiar with:

  • Basics of the Go programming language
  • How GitHub Actions work
  • What semantic versioning (semver) and semantic releases mean

By the end of this guide, you'll have a good understanding of how to automate the release process for your Go projects — the same way most open-source Go libraries are maintained.

Building a CI/CD-Ready Go Package


Here's a simple checklist before setting up automation:

  1. Code & Repo Setup

    • Start with solving a real problem.
    • Keep your project clean and organized - Take ref from here
  2. Documentation

    • Document everything clearly
    • What your package does - Mostly in README and in comments
    • How to install and use it - Mostly in README
    • Always add examples of How to use - Mostly in README and in comments
    • Go docs auto creates docs using comments that are written in your code
    • In addition, You can also create a doc.go in each package that can contains the docs
  3. Linting & Formatting

    • Before pushing changes, always format and lint your code
    • Use githooks like precommit for auto linting and formatting on commits
    • Or you can manually do it like
    // Here we have used ./... to work on every go file in ./ directory go fmt ./... // Here fmt is used to format all the go files that are avaliable go vet ./... // Here vet examines your code and reports suspicious constructs 

    It's a small step that saves you from bigger code review headaches later.

  4. Testing

    • Named your test files as *_test.go
    • Test your code before every commit using go test ./...
  5. Build

    • Build your module locally to make sure everything compiles go build ./...
    • This also helps confirm your code is production-ready before tagging a release.
  6. Release

    • Once linting ✅, testing ✅, and building ✅ are done — you're ready to release. In a CI/CD setup, releases are usually automated with semantic tags like v1.0.0.

Setting Up the Repository

Let's get hands-on.

  1. Create and initialize your repository:
mkdir <YOUR_PROJECT_NAME> git init mkdir src cd src go mod init github.com/<YOUR_USERNAME>/<YOUR_PROJECT_NAME> 
Enter fullscreen mode Exit fullscreen mode

Now, create your folder structure:

.github/workflows/*.yaml // GitHub actions workflow configurations .config/goreleaser.yaml // GoReleaser configuration src/ cmd/ main.go // entry point of your project doc.go // package documentation main_test.go // test cases for your code go.mod // Your Go mod file contains module name, go version and packages you are using README.md // Documentation about repository i.e. your project LICENSE // Optional if you want to add a LICENSE .gitignore // contains pattern and files that will be ignored by git .pre-commit-config.yaml // pre-commit configuration and hooks 
Enter fullscreen mode Exit fullscreen mode

If you'd like to skip setup, you can clone my sample repository: github.com/ticatwolves/go-project-skeleton

Automating Build, Test, and Release with GitHub Actions

Here's where GitHub Actions works.
You can automate almost everything — formatting, testing, tagging, and even publishing.

A typical workflow includes:

  • Running go fmt, go vet, and go test on every pull request
  • Automatically tagging releases with semantic versions
  • Building the package and pushing it to the Go proxy

Simple and easy to adapt workflow for each pull request

name: Lint & Format on: pull_request: branches: - main # Grant the jobs required permissions for OIDC to work permissions: id-token: write contents: write pull-requests: write jobs: Linter: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v5 with: go-version: 1.23 - name: Install dependencies working-directory: ./src run: go mod tidy - name: Run go fmt working-directory: ./src run: go fmt ./... - name: Run go vet working-directory: ./src run: go vet ./... - name: Update pull request uses: peter-evans/create-pull-request@v7 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: "chore: format code" title: "[AUTO] Format code" body: "This PR automatically formats the code." branch: ${{ github.head_ref }} base: ${{ github.base_ref }} labels: | automated pr GoLangCI: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: # Required to report new issues on the PR fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v5 with: go-version: 1.23 - name: Run golangci-lint uses: golangci/golangci-lint-action@v8 with: working-directory: ./src Test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: 1.23 - name: Test working-directory: ./src run: go test -v ./... 
Enter fullscreen mode Exit fullscreen mode

Simple and easy to adapt workflow for publishing changes to the package manager.

name: Release on: push: branches: [ "main" ] permissions: id-token: write contents: write jobs: release: 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: 1.23 - name: Test working-directory: ./src run: go test -v ./... - name: Go Semantic Release id: semrel uses: go-semantic-release/action@v1 with: github-token: ${{ secrets.GITHUB_TOKEN }} - name: Fetch tags created by semantic release if: steps.semrel.outputs.version != null && steps.semrel.outputs.version != '' run: git fetch --tags - name: GoReleaser if: steps.semrel.outputs.version != null || steps.semrel.outputs.version != '' uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser version: latest args: release --config .config/goreleaser.yaml --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 
Enter fullscreen mode Exit fullscreen mode

In the above workflow we are using GoReleaser So here is the config file goreleaser.yaml.

version: 2 builds: - dir: "./src" main: ./cmd/main.go env: - CGO_ENABLED=0 goos: - linux - windows - darwin goarch: - amd64 - arm64 release: github: owner: <YOUR_USERNAME> name: <YOUR_PROJECT_NAME> 
Enter fullscreen mode Exit fullscreen mode

Publishing on pkg.go.dev

One last step — making your module discoverable.

Go's official package discovery site, pkg.go.dev, doesn't automatically index new repos. After your first release (say, v1.0.0), visit: https://pkg.go.dev/github.com/YOUR_USERNAME/YOUR_PROJECT_NAME

Then click “Request indexing” or “Fetch now” to get it listed.

Once done, your package becomes publicly available for everyone to import:

go get github.com/YOUR_USERNAME/YOUR_REPO@v1.0.0

Let's Talk

How do you handle CI/CD for your Go projects?
Have you automated your release process yet, or are you still doing it manually?

Drop your thoughts in the comments — I'd love to learn how others are approaching this!

References & Further Reading

Top comments (0)