Go CI/CD Workflows
Generated by golang-ci-library, which is composed by all three Go service archetypes:
golang-rest-service-archetypegolang-grpc-service-archetypegolang-graphql-service-archetype
The protocol has no effect on the workflow file - all three render the same two files. It does affect what the build action generates at run time, since golang-build guards its code generation on what exists in the repository.
Build Workflow
.github/workflows/build.yaml, shown as rendered for a project named billing-service:
name: Build
on:
push:
branches: ["**"]
pull_request:
permissions:
contents: write
id-token: write
env:
IMAGE_NAME: billing-service
APPLICATION_NAME: billing-service
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: p6m-actions/golang-setup@v1
- name: Cut Patch Version
if: github.ref_name == 'main'
id: cut-patch
uses: p6m-actions/golang-cut-tag@v1
with:
version-level: patch
- name: Build
uses: p6m-actions/golang-build@v1
# ... shared container build, release and dispatch steps
Step Chain
| # | Step | Scope | Runs on |
|---|---|---|---|
| 1 | Checkout with fetch-depth: 0 | Go-specific variant | Always |
| 2 | Go setup | Go | Always |
| 3 | Cut Patch Version | Go | main |
| 4 | Build | Go | Always |
| 5 | Login to Artifactory Container Registry | Shared | main |
| 6 | Set up Docker Buildx | Shared | Always |
| 7 | Build and Publish Docker Image | Shared | Always |
| 8 | Make Artifacts | Shared | main |
| 9 | Create Github release | Shared | main |
| 10 | Update Application Manifest | Shared | main |
Go is the outlier of the six in three ways: it needs the full git history, it never commits a version bump, and its build step generates code that is not committed.
Language-Specific Steps
Checkout with Full History
- uses: actions/checkout@v4
with:
fetch-depth: 0
Every other language reads the current version out of a manifest file. A Go module has no version field, so golang-cut-tag derives the next version from the latest git tag - the tags are the version history. A shallow clone would hide them and every build would try to cut v0.0.1.
Go Setup
- uses: p6m-actions/golang-setup@v1
p6m-actions/golang-setup installs the Go toolchain with module and build caching, plus the protobuf toolchain when it is needed.
| Input | Default | Passed by the workflow |
|---|---|---|
go-version | (empty) | (default) - taken from go-version-file instead |
go-version-file | go.mod | (default) - go.mod is the single source of truth, so the toolchain version is never stated twice |
cache | true | (default) |
cache-dependency-path | go.sum then go.mod | (default) - a freshly rendered project has no go.sum yet, so go.mod is the fallback cache key |
install-protoc | auto | (default) - installs protoc and the Go gRPC plugins only when a proto/ directory exists |
protoc-gen-go-version | v1.36.5 | (default) |
protoc-gen-go-grpc-version | v1.5.1 | (default) |
install-dependencies | false | (default) - no separate go mod download |
working-directory | . | (default) |
| Output | Description |
|---|---|
go-version | The installed Go version |
cache-hit | Whether the module/build cache was restored |
protoc-installed | true when the protobuf toolchain was installed |
install-protoc: auto is what makes one workflow serve three protocolsThe gRPC archetype renders a proto/ directory, so the toolchain installs. The REST and GraphQL archetypes do not, so the step is a no-op. Contrast with Rust, which installs protoc unconditionally.
protoc-gen-go generates bindings against a specific runtime library version. If CI and the production Dockerfile install different plugin versions, the mismatch shows up as a link error at container build time, not as a warning here.
Cut Patch Version
- name: Cut Patch Version
if: github.ref_name == 'main'
id: cut-patch
uses: p6m-actions/golang-cut-tag@v1
with:
version-level: patch
p6m-actions/golang-cut-tag computes the next semantic version from the latest tag and creates a new tag. It never writes a file and never creates a commit.
| Input | Required | Default | Notes |
|---|---|---|---|
version-level | No | patch | patch, minor, or major, relative to the latest tag |
version-file | No | .version-line | Optional file holding <prefix><major>.<minor>, e.g. v1.2. When present it acts as a floor you control; when absent the latest tag alone decides. Read-only - the action never writes it |
prefix | No | (inferred) | Inferred from the version file when one exists, otherwise v - the prefix go get expects |
pre-release | No | false | When true, appends an epoch timestamp, e.g. v1.2.5-1782043845 |
working-directory | No | . | Must be inside the git repo |
skip-push | No | false | Skips pushing the tag |
| Output | Description |
|---|---|
version | The new version without prefix, e.g. 1.2.5 |
tag | The created git tag with prefix, e.g. v1.2.5 |
base-version | The release version without any pre-release suffix |
is-pre-release | true when a pre-release tag was created |
Two consequences follow from "tag only, no commit":
- No
token-exchange, no[skip ci]hook. Go is the only language whose cut-tag action skips the OIDC token exchange. The tag is pushed with the job's ownGITHUB_TOKEN, which by design does not trigger further workflow runs - so there is no loop to break. .version-lineis the only manual lever. To move a Go service from the1.xline to2.x, either run the Cut Tag workflow withmajor, or commit a.version-linefile containingv2.0as a floor.
Build
- name: Build
uses: p6m-actions/golang-build@v1
p6m-actions/golang-build generates code, tidies the module, then builds and tests. No inputs are overridden, so all defaults apply:
| Input | Default | What runs |
|---|---|---|
run-codegen | true | protoc when proto/ exists, gqlgen when gqlgen.yml exists |
run-tidy | true | go mod tidy |
run-build | true | go build ./... |
run-test | true | go test ./... |
run-format-check | false | gofmt -l . - off |
run-vet | false | go vet ./... - off |
run-lint | false | golangci-lint run - off |
archive-coverage | false | Coverage is not uploaded |
working-directory | . | Repository root |
| Output | Description |
|---|---|
build-status | success or failure |
Two defaults are worth understanding:
Code generation runs inside the build step. Go services import code that is generated rather than committed - protobuf bindings under gen/ for gRPC, the gqlgen server for GraphQL. A fresh clone does not compile until that code exists. Generating it here, in the step that needs it, is what keeps the CI path and the container build path from disagreeing about what a fresh clone contains.
go mod tidy runs on every build. A freshly rendered project ships without a go.sum, because producing one requires the toolchain and rendering does not have it. run-tidy materializes it on the first CI run; on a checkout that already has one it is a no-op.
Unlike Rust, the Go pipeline does not gate on formatting, go vet, or golangci-lint. golangci-lint in particular needs a .golangci.yml that the archetypes do not render. To turn any of them on, pass the corresponding run-* input in your build.yaml:
- name: Build
uses: p6m-actions/golang-build@v1
with:
run-format-check: "true"
run-vet: "true"
Cut Tag Workflow
.github/workflows/cut-tag.yaml:
name: Cut Tag
on:
workflow_dispatch:
inputs:
version-level:
description: "Version bump level"
required: true
default: "patch"
type: choice
options:
- patch
- minor
- major
permissions:
id-token: write
contents: write
jobs:
cut-tag:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- uses: p6m-actions/golang-setup@v1
- uses: p6m-actions/golang-cut-tag@v1
with:
version-level: ${{ inputs.version-level }}
| Input | version-level - patch, minor, or major |
| Outputs | A git tag. No commit, so nothing changes in the working tree |
Version File
There is no version file. The version is whatever the latest v-prefixed git tag says:
git tag --list 'v*' --sort=-v:refname | head -1
# v1.4.2
The optional .version-line file, if you commit one, sets a floor on the major and minor line:
v2.0
Related
- CI/CD Workflows - the shared steps and the CD handoff
- Versioning - the platform-wide semantic versioning model
- gRPC Design - the protobuf conventions the codegen step consumes