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:
-
Code & Repo Setup
- Start with solving a real problem.
- Keep your project clean and organized - Take ref from here
-
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
-
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.
-
Testing
- Named your test files as *_test.go
- Test your code before every commit using
go test ./...
-
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.
-
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.
- 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>
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
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 ./...
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 }}
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>
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!
Top comments (0)