Day 054 — GitHub Actions CI for Go¶
Month 2 · Week 4 · ⬅ Day 053 · Day 055 ➡ · Journal index
🎯 Learning Objective¶
Run the week's gate (fmt-check, vet, race tests, build) automatically on every push and pull request with GitHub Actions, across multiple Go versions, with module caching.
📚 Topics¶
- Workflow anatomy:
on,jobs,steps,usesvsrun actions/setup-go(built-in module cache) + a build matrix- Failing the build on unformatted code; uploading coverage as an artifact
📖 Reading / Sources¶
📝 Notes¶
- A workflow is a YAML file in
.github/workflows/. Top level:name,on(triggers likepush/pull_request), andjobs. Each job runs on a freshruns-onVM; steps run in order, and a failed step fails the job (mirroringmake ci). → links to [[github-actions]] and [[tooling]]. - Two kinds of step:
uses:runs a published action (e.g.actions/checkout@v4),run:runs shell. The first step is almost alwaysactions/checkout@v4to get your code onto the runner. actions/setup-go@v5installs the toolchain and, withcache: true(the default when ago.sumexists), caches the module download and build cache keyed ongo.sum— big speedup on repeat runs. Pin the version viago-versionorgo-version-file: go.mod.- Matrix builds run the job once per combination:
strategy.matrix.go: ['1.22', '1.23'](× OS) catches version- and platform-specific breakage.${{ matrix.go }}interpolates the value. - Make CI stricter than local:
go vet ./...,go test -race ./..., and a formatting gate —test -z "$(gofmt -l .)"exits non-zero if any file is unformatted (gofmt -llists offenders; empty output = clean).go testalready runs a vet subset, but an explicitvetstep documents intent. - Don't
go get/go installyour linters ad hoc in CI — use the maintainedgolangci/golangci-lint-action, which caches and version-pins the binary. - Persist outputs with
actions/upload-artifact(e.g.cover.out) or post to a coverage service; gate merges with a required status check in branch protection so red CI blocks the PR.
💻 Code Examples¶
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
go: ['1.22', '1.23']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
cache: true
- name: gofmt check
run: test -z "$(gofmt -l .)" || (gofmt -l . && exit 1)
- run: go vet ./...
- run: go test -race ./...
- run: go build ./...
No runnable example: CI configuration is YAML executed by GitHub's runners, not Go you
go run. The steps wrap the samegofmt/vet/test/buildused all week.
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
Add a ci.yml running fmt-check/vet/race-test across Go 1.22 & 1.23 |
✅ | exercises/month-02/week-4 |
Make CI fail on unformatted code via gofmt -l |
✅ | exercises/month-02/week-4 |
🐛 Mistakes Made¶
- Wrote a
gofmtcheck asgofmt -l .alone — it exits 0 even when it lists files, so CI stayed green. Wrapped it intest -z "$(gofmt -l .)"to fail on non-empty output. - Forgot
actions/checkoutand every step ran against an empty workspace (no Go files). It must be the first step.
❓ Open Questions¶
- Where's the line between CI doing too much (slow PRs) vs too little? (Keep PR CI to fmt/vet/test/build + lint; push slow integration/e2e and cross-builds to a nightly or release workflow.)
🧠 Active Recall (answer without looking)¶
- Q: Why isn't
gofmt -l .alone a valid CI formatting gate?A
gofmt -l only lists unformatted files and exits 0 regardless, so the step passes even when files are unformatted. Wrap it: test -z "$(gofmt -l .)" fails when the list is non-empty.
2. Q: What does a build matrix give you, and how do you reference a value? A
It runs the job once per combination of the listed values (e.g. each Go version × OS), catching version/platform-specific breakage. Reference a value with ${{ matrix.<key> }}.
🪶 Feynman Reflection¶
GitHub Actions is a robot that, on every push, spins up a clean machine, checks out your code, installs Go, and runs your checklist top to bottom. If any item fails, the whole run goes red and (with branch protection) blocks the merge. It's the same make ci gate I run locally, but enforced for everyone so "works on my machine" can't slip into main.
🕳️ Knowledge Gaps¶
- Reusable/composite workflows, OIDC for cloud deploys, and caching tuning beyond
setup-go's defaults — defer until a deploy pipeline is needed.
✅ Summary¶
I can write a Go CI workflow that checks out, sets up Go with caching, enforces formatting, and runs vet + race tests + build across a version matrix on every push/PR.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 055: build the Week 4 capstone — a stdlib-only URL shortener — and back it with the week's exercises and CI.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: docs(journal): github actions CI for Go (day 054)