ci: add Go test+vet+build workflow for PRs and main#8
Conversation
Before this, the only workflow on the repo was release.yaml (tag- triggered). Merges went in on local-test-passed trust, which is fine with one contributor but will not scale. The new workflow runs on every PR and push-to-main: - go vet ./internal/... - go build ./internal/... - go test -race -count=1 ./internal/... Scoped to ./internal/... rather than ./... because internal/web/embed.go has //go:embed all:dist and the frontend build is not part of this job; a single stub file is created so the embed directive compiles. Building the frontend and wiring it into CI is a separate concern. Go version comes from go.mod (1.22) via setup-go go-version-file. -race enabled because the per-app deploy mutex in handlers.go is the obvious concurrency surface we want exercised. -count=1 disables the test cache so flakes surface on every run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Go | ||
| uses: actions/setup-go@v5 | ||
| with: | ||
| go-version-file: go.mod |
There was a problem hiding this comment.
Actions pinned to mutable version tags
actions/checkout@v4 and actions/setup-go@v5 use mutable major-version tags. If either upstream repo were compromised and a tag re-pointed to malicious code, this workflow would execute it with write access to the runner (even though permissions: contents: read limits GitHub API access, the runner filesystem and secrets are still exposed). GitHub's own hardening guide recommends pinning third-party actions to a full commit SHA.
| uses: actions/checkout@v4 | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| - name: Checkout | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 |
- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
| go: | ||
| name: Go test + vet + build | ||
| runs-on: ubuntu-latest |
There was a problem hiding this comment.
No concurrency group or job timeout
Without a concurrency group, every push to an open PR queues a new run while old ones are still running, burning CI minutes needlessly. A timeout-minutes guard also prevents a hung test or infinite loop from consuming all available runner time.
| go: | |
| name: Go test + vet + build | |
| runs-on: ubuntu-latest | |
| go: | |
| name: Go test + vet + build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 |
And add at the workflow level (before jobs:):
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Summary
Adds
.github/workflows/ci.yaml— Go test + vet + build, gated on every PR and push-to-main. Before this,release.yaml(tag-triggered) was the only workflow; merges relied on local-test-passed trust.What the workflow runs
./internal/..., not./.... The top-level./...pulls ininternal/web/embed.gowhich has//go:embed all:dist— the embed fails without a frontend build. The workflow creates a stubinternal/web/dist/index.htmlso the embed compiles, but wiring the actual frontend build into CI is a separate concern (follow-up).go.mod(1.22) viasetup-go'sgo-version-file— no hardcoded version to drift.-racebecause the per-app deploy mutex (handlers.go) is the obvious concurrency surface; we want it exercised.-count=1disables the test cache so flakes surface on every run.Why separate from the auto-rollback PR
PR #7 is already through three review passes and architect-signed-off. Tacking CI onto it would re-open scope. Simpler to merge them independently — PR #7 first (feature), then this one (gate subsequent PRs).
Verification
Ran the exact three commands locally against
origin/main:go vet ./internal/...→ cleango build ./internal/...→ cleango test -race -count=1 ./internal/...→ok internal/api 1.800s,ok internal/k8s 8.241s(six auxiliary packages have no tests, reported as?)Test plan
Go test + vet + buildcheck appears and goes greenFollow-ups (not in this PR)
npm ci && npm run buildinweb/, thengo build ./...against the real embed. Separate job so a broken frontend doesn't mask a green Go suite.mainrequiring this check to pass before merge. GitHub settings, not workflow.gofmt -l,goimports, or agolangci-lintaction. Nice-to-have.🤖 Generated with Claude Code
Greptile Summary
This PR adds a CI workflow (
.github/workflows/ci.yaml) that runsgo vet,go build, andgo test -race -count=1against./internal/...on every PR and push tomain. The design choices are sound: Go version is read fromgo.mod, permissions are locked tocontents: read, and the stub approach for the missing frontend embed is a reasonable workaround documented clearly in both the workflow and the PR description.Confidence Score: 5/5
Safe to merge — all findings are P2 style suggestions that do not affect correctness or reliability.
The workflow is well-structured with correct trigger events, minimal permissions, and no hardcoded Go version. The two findings (mutable action tags, missing concurrency group/timeout) are best-practice hardening suggestions, not blocking defects.
No files require special attention.
Security Review
actions/checkout@v4andactions/setup-go@v5are pinned to mutable major-version tags. A supply-chain compromise that re-points a tag would execute arbitrary code on the runner, with access to runner filesystem and any secrets scoped to the workflow — even thoughpermissions: contents: readconstrains the GitHub API token. Pinning to full commit SHAs mitigates this risk.Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[PR opened / push to main] --> B[Checkout repo] B --> C[Setup Go via go.mod] C --> D[Stub internal/web/dist/index.html] D --> E[go vet ./internal/...] E --> F[go build ./internal/...] F --> G[go test -race -count=1 ./internal/...] G --> H{All steps pass?} H -- Yes --> I[✅ CI green] H -- No --> J[❌ CI red — blocks merge]Reviews (1): Last reviewed commit: "ci: add Go test+vet+build workflow for P..." | Re-trigger Greptile