Skip to content

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, uses vs run
  • 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 like push/pull_request), and jobs. Each job runs on a fresh runs-on VM; steps run in order, and a failed step fails the job (mirroring make 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 always actions/checkout@v4 to get your code onto the runner.
  • actions/setup-go@v5 installs the toolchain and, with cache: true (the default when a go.sum exists), caches the module download and build cache keyed on go.sum — big speedup on repeat runs. Pin the version via go-version or go-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 gatetest -z "$(gofmt -l .)" exits non-zero if any file is unformatted (gofmt -l lists offenders; empty output = clean). go test already runs a vet subset, but an explicit vet step documents intent.
  • Don't go get/go install your linters ad hoc in CI — use the maintained golangci/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 same gofmt/vet/test/build used 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 gofmt check as gofmt -l . alone — it exits 0 even when it lists files, so CI stayed green. Wrapped it in test -z "$(gofmt -l .)" to fail on non-empty output.
  • Forgot actions/checkout and 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)

  1. 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)