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)