Skip to content

Day 151 — GitHub Actions: build, test, scan

Month 6 · Week 2 · ⬅ Day 150 · Day 152 ➡ · Journal index

🎯 Learning Objective

Build a CI pipeline that, on every push/PR, builds the module, runs vetting and the race-enabled test suite with coverage, and scans for known vulnerabilities with govulncheck.

📚 Topics

  • GitHub Actions: workflow, jobs, steps, triggers, matrix
  • Go CI essentials: go vet, go test -race -cover, module/build caching
  • Supply-chain: govulncheck, go vet, dependency review

📖 Reading / Sources

📝 Notes

  • A workflow (.github/workflows/ci.yml) holds jobs that run on triggers (on: [push, pull_request]). Each job runs on a fresh runner; steps within a job share a filesystem → [[github-actions]].
  • go test -race runs the data-race detector — it instruments memory access and catches unsynchronized concurrent access at runtime. CI is the right place since it slows tests ~2–10x. Pair with -cover/-coverprofile.
  • Run go vet ./... in CI: it flags suspicious-but-compiling code (bad Printf verbs, lost context.Cancel, copying locks). Cheap, high-signal → [[go-vet]].
  • govulncheck ./... checks your imports and call graph against the Go vuln DB. It's low-noise: it reports a CVE only if your code actually reaches the vulnerable symbol, not merely depends on the module → [[govulncheck]].
  • Cache the module + build cache (actions/setup-go does this when cache: true) keyed on go.sum, so CI doesn't re-download/recompile everything each run.
  • A matrix runs the same job across Go versions / OSes in parallel. fail-fast: false lets every cell finish so you see all failures.
  • Pin actions and use least-privilege permissions: (default contents: read); request only what a job needs. Don't expose secrets to PRs from forks.

💻 Code Examples

# .github/workflows/ci.yml
name: ci
on:
  push: { branches: [main] }
  pull_request:
permissions:
  contents: read            # least privilege
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix: { go: ["1.22", "1.23"] }
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}
          cache: true                 # caches module + build cache by go.sum
      - run: go vet ./...
      - run: go test -race -coverprofile=cover.out ./...
      - run: go tool cover -func=cover.out | tail -1
  vulncheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with: { go-version: "1.22" }
      - run: go install golang.org/x/vuln/cmd/govulncheck@latest
      - run: govulncheck ./...

Locally, the same gate is just three commands:

// run before every push (a Makefile target or git pre-push hook):
//   go vet ./...
//   go test -race -cover ./...
//   govulncheck ./...

🏋️ Exercises / Practice

Exercise Status Link
Make every week-2 package pass go test -race exercises/month-06/week-2/

🐛 Mistakes Made

  • Ran tests without -race locally; CI's -race job caught a real data race in a shared counter. The detector is only as good as the code paths exercised, so coverage matters.
  • Gave the workflow broad permissions: write-all out of habit. Tightened to contents: read — CI doesn't need write.

❓ Open Questions

  • Should govulncheck fail the build or just warn? (Fail on reachable HIGH/CRITICAL; otherwise the gate is theater.)

🧠 Active Recall (answer without looking)

  1. Q: How does govulncheck avoid the false-positive noise of plain dependency scanners?
    A

It performs call-graph (reachability) analysis: it reports a vulnerability only if your code actually calls the affected symbol, not merely because a vulnerable module is in go.sum. 2. Q: Why run go test -race in CI rather than relying on local runs?

A

The race detector adds significant CPU/memory overhead, so devs often skip it locally. CI runs it consistently across the full matrix and catches races that only surface under different scheduling/load.

🪶 Feynman Reflection

CI is a robot that, every time you push, rebuilds your code on a clean machine and runs the checks you'd otherwise forget: vet for sloppiness, -race for concurrency bugs, govulncheck for known-bad dependencies you actually use. Green means "safe to merge"; it's an automated second pair of eyes.

🕳️ Knowledge Gaps

  • Reusable workflows / composite actions to share CI across repos — revisit during capstone.

✅ Summary

I can write a GitHub Actions pipeline that vets, race-tests with coverage, caches dependencies, and gates merges on govulncheck, all with least-privilege permissions.

⏭️ Next Steps / Prep for Tomorrow

  • Day 152: feed secrets and config into all this without leaking them.

Time spent Difficulty Confidence
95 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: docs(journal): github actions build/test/scan (day 151)