๐ TL;DR
- Did you know I just came back from a vacation? ๐ด
- Built a simple Go CLI with Cobra
- Automated PR checks, linting, and testing with GitHub Actions
- Enforced Conventional Commits and semantic versioning
- Generated changelogs and cross-platform releases with Goreleaser
- Secured the pipeline using CodeQL and Dependabot
๐ด About My Vacation
I just got back from a much-needed vacation, and during that time, I finally sat down and read The Phoenix Project. If youโre not familiar with it, itโs a novel about IT, DevOps, and the chaos that can happen when delivery processes are broken.
Itโs part entertaining, part terrifying, and completely inspiring.
So when I returned to my Golang CLI projects, I decided to revisit and improve my CI/CD setup from the ground up. In this post, I will walk you through the stack I ended up with: a clean, modular, and production-ready CI/CD pipeline built entirely with GitHub Actions.
Letโs dive in.
๐ก What I Learned from The Phoenix Project
The story really drove home how much trouble teams can get into without:
- Automated security checks to catch vulnerabilities early
- Reliable, automated testing to prevent regressions
- Controlled deployments that minimize risk
- Formal change management to track and govern what gets released
Without these safeguards, teams face outages, firefighting, and lost time, problems that could have been avoided with better automation and process.
This reinforced for me that CI/CD is more than just running builds and tests. Itโs about embedding control, safety, and visibility into every step of software delivery.
๐ฆซ Create a Simple Golang Application
Iโve always been fascinated by how easy and powerful it is to build CLI applications in Go. With the help of Cobra, creating a structured, user-friendly command-line tool takes just a few lines of code.
main.go
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
func main() {
var rootCmd = &cobra.Command{
Use: "hello-cli",
Short: "A simple CLI that says hello",
Run: func(cmd *cobra.Command, args []string) {
cmd.Println("Hello, world!")
},
}
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
main_test.go
package main
import (
"bytes"
"strings"
"testing"
)
func TestHelloCLI(t *testing.T) {
var buf bytes.Buffer
var rootCmd = &cobra.Command{
Use: "hello-cli",
Short: "A simple CLI that says hello",
Run: func(cmd *cobra.Command, args []string) {
cmd.Println("Hello, world!")
},
}
rootCmd.SetOut(&buf)
rootCmd.SetArgs([]string{})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("Command failed: %v", err)
}
output := buf.String()
expected := "Hello, world!\n"
if strings.TrimSpace(output) != strings.TrimSpace(expected) {
t.Errorf("unexpected output:\n got: %q\n want: %q", output, expected)
}
}
go run main.go
Hello, world!
๐ It All Starts with a Pull Request
The first step in building a reliable CI/CD pipeline is to ensure consistency and quality at the source your pull requests. If every PR follows a standard format and passes basic checks, the rest of the pipeline becomes much more predictable and automated.
To enable automated versioning and changelog generation later in the pipeline, I decided to adopt Conventional Commits. These are structured commit messages that follow a simple pattern:
feat: add new controller endpoint
fix: resolve panic on nil pointer in handler
chore: update dependencies
The next step is to automate enforcement. Manual checks donโt scale.
The goal here is simple:
- โ Ensure PR Title follows Conventional Commits
- โ Ensure all code builds successfully
- โ Run tests automatically
- โ Enforce formatting and linting rules
.github/workflows/pr-checks.yml
name: Pull Request checks
on:
pull_request:
branches: [main]
jobs:
build:
name: ๐จ Build , Lint & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.24.3"
- name: Install dependencies
run: go mod tidy
- name: Build project
run: go build -v ./...
- name: Run tests
run: go test -v ./...
- name: Lint code
uses: golangci/golangci-lint-action@v8
with:
version: v2.1
- name: Lint Pull Request Title
uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
The initial results are promising, confirming our basic CI checks work as expected. Now, letโs push forward and expand the pipelineโs capabilities.
๐ Version please!
In The Phoenix Project, Bill Palmer, the VP of IT Operations, faces major headaches because developers are inconsistently managing versioning and leading to confusion, deployment failures, and firefighting. This real-world scenario highlights how crucial it is to properly version every code change.
With structured commit messages in place, we can now introduce semantic versioning and automate changelog generation directly within our CI/CD pipeline.
By analyzing commit history, we can:
- Determine the appropriate version bump (major, minor, or patch)
- Automatically generate a well-formatted
CHANGELOG.md
- Create and push a GitHub tag for the release
This not only enforces consistent versioning across releases, but also removes the manual overhead of maintaining changelogs and tagging code, a critical step toward reliable, reproducible deployments.
Once the project is ready for a new release, we can trigger an on-demand GitHub Actions workflow to automatically generate a new tag based on commit history.
.github/workflows/create-new-tag.yml
name: Release a new version
on: [workflow_dispatch]
jobs:
release-tag:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: "0"
- name: Tag repository
id: tagRepo
uses: anothrNick/github-tag-action@1.36.0
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
WITH_V: true
DEFAULT_BUMP: patch
We can now respond to new tag events by automatically generating and publishing a changelog.
.github/workflows/changelog.yml
name: Generate Changelog
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+
jobs:
changelog:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Generate and Commit Changelog
uses: requarks/changelog-action@v1
with:
token: ${{ secrets.GH_TOKEN }}
tag: ${{ github.ref_name }}
commitMessage: "docs(changelog): update for ${{ github.ref_name }}"
outputFile: "CHANGELOG.md"
- name: Create GitHub Release
uses: ncipollo/release-action@v1
with:
tag: ${{ github.ref_name }}
bodyFile: "CHANGELOG.md"
token: ${{ secrets.GH_TOKEN }}
As our CLI evolves, itโs important to demonstrate how feature development ties directly into our CI/CD workflow. Letโs implement a simple enhancement: a --name
flag that allows users to personalize the greeting message.
For example, running the following command:
go run main.go --name Giovanni
Should output:
Hello, Giovanni!
main.go
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var name string
var rootCmd = &cobra.Command{
Use: "hello-cli",
Short: "A simple CLI that says hello",
Run: func(cmd *cobra.Command, args []string) {
if name != "" {
cmd.Printf("Hello, %s!\n", name)
} else {
cmd.Println("Hello, world!")
}
},
}
func init() {
rootCmd.Flags().StringVar(&name, "name", "", "Custom name to include in greeting")
}
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
Once our pull request has been reviewed, approved, and successfully merged, ensuring all checks have passed, we can trigger the release workflow.
Aaaaaand, here we go! ๐
With everything in place, our pipeline now effortlessly handles releases, tagging, changelog updates and letting us focus on what truly matters: writing great code.
๐ฆ Stop running go run main.go
Weโre almost there but thereโs one crucial piece missing.
We donโt want to clone the entire project every time I want to run hello-cli
; I need to execute the binary directly. And hereโs the twist: I have a Raspberry Pi with an ARM processor, a Mac with Apple Silicon, and a tiny Linux box so I want my binary to run seamlessly everywhere!
To solve this challenge, Iโll be using Goreleaser a powerful tool that automates building and publishing binaries for multiple platforms. With a single configuration YAML file, Goreleaser simplifies cross-platform releases, ensuring our CLI runs smoothly on ARM, Apple Silicon, Linux, and more all without manual hassle.
.goreleaser.yml
version: 2
project_name: hello-cli
dist: bin
builds:
- id: hello-cli
main: main.go
goos:
- linux
- darwin
goarch:
- amd64
- arm64
- id: hello-cli-win
main: main.go
goos:
- windows
goarch:
- amd64
- arm64
archives:
- id: default
ids: [hello-cli]
formats: [tar.gz]
name_template: "hello-cli_{{ .Os }}_{{ .Arch }}"
files:
- LICENSE.txt
- README.md
- id: hello-cli-win
ids: [hello-cli-win]
formats: [zip]
name_template: "hello-cli_{{ .Os }}_{{ .Arch }}"
files:
- LICENSE.txt
- README.md
release:
github:
owner: gdegiorgio
name: golang-rock-solid-cicd
changelog:
use: git
.github/workflows/package-hello-cli.yml
name: Release Packages
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.24.3"
- name: Install GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
Thatโs really incredible, donโt you think? ๐คฏ
๐ Securing Your Application: CodeQL and Dependabot
No CI/CD pipeline is complete without a strong focus on security and dependency management. As your application grows, so does the attack surface and the complexity of keeping everything up to date. Fortunately, GitHub provides two powerful tools to help us stay ahead: CodeQL and Dependabot.
๐ง CodeQL: Automated Security Analysis
CodeQL is GitHubโs semantic code analysis engine. It allows you to query your codebase like a database, identifying security vulnerabilities and logic flaws before they make it to production.
Once integrated into your workflow, CodeQL will:
- Automatically scan your code
- Detect common vulnerability patterns like SQL injection, command injection, or unsafe deserialization
By running CodeQL scans regularly, you introduce a proactive layer of defense directly into your development cycle, catching bugs early and enforcing security hygiene with minimal effort.
.github/workflows/codeql.yml
name: "CodeQL"
on:
pull_request:
branches: [main]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [go]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
๐ Dependabot: Automated Dependency Updates
Modern applications rely on open-source libraries and keeping those up to date is essential. Dependabot helps you do exactly that by automatically scanning your go.mod
and other dependency files, opening pull requests when:
- A newer version is available
- A known vulnerability has been reported in a dependency
.github/dependabot.yml
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: daily
โ Wrap-Up: A Pipeline You Can Trust
In this journey, we started with a simple idea :
building a lightweight CLI tool in Go and used it as a foundation to construct a solid, secure, and automated CI/CD workflow using GitHub Actions.
By layering in tools and best practices like:
- PR-based checks for linting, testing, and compiling
- Semantic commit enforcement to drive meaningful versioning
- Automated changelog generation and release tagging
- Multi-platform binary publishing with Goreleaser
- Security scanning with CodeQL
- Dependency hygiene via Dependabot
โฆweโve established a pipeline that not only supports continuous delivery, but also builds trust through automation, clarity, and repeatability.
Whether youโre building a toy CLI or the next critical internal tool, think of this as a good foundation you can tweak and grow depending on what your project needs.
๐ References
-
๐๏ธย hello-cli GitHub Repository
The sample project used throughout this post. Explore the code, workflows, and automation setup.
-
๐ The Phoenix Project โ Gene Kim, Kevin Behr, George Spafford
A novel that inspired this postโs focus on flow, feedback, and continual improvement in software delivery.
-
๐งฐ GitHub Actions
Automate, customize, and execute software workflows directly in your GitHub repo.
-
๐ก๏ธ CodeQL
GitHubโs static analysis engine for discovering vulnerabilities in your code.
-
๐ Dependabot
Keeps your dependencies up-to-date and secure with automated pull requests.
-
๐ฆ Goreleaser
Build and release Go binaries for multiple platforms with ease.
-
๐ Cobra CLI Framework
A widely-used library for building powerful CLI applications in Go.
-
๐ Conventional Commits
A standardized format for writing commit messages that power semantic versioning.
Top comments (0)