carlos / platform Public

Merge pull request #75 from carlosframework/ci-build-gate

ci: run the build gate on every PR, including a darwin compile
James Adam 38b499902cb015f194b64f741eb2681132eb8816 parent 4093755parent b5aa780
2 files changed, +112 −0
  • .github/workflows/ci.yml +107 −0
  • CLAUDE.md +5 −0
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..b461422e5
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,107 @@
+# The build gate from CLAUDE.md, enforced by a machine rather than by
+# whoever happens to be at a keyboard. The gate itself is not new; what is
+# new is that it now runs somewhere other than a maintainer's Linux laptop.
+#
+# The cross-compile job exists because of a specific bug: readDiskFreeBytes
+# in internal/host/probes.go multiplied an int64 by syscall.Statfs_t.Bsize,
+# which is int64 on Linux and uint32 on darwin. That is a compile error on
+# macOS for every GOARCH, and internal/host is imported by the carlos CLI —
+# the laptop tool the README's developer story is built around. It sat in
+# main undetected because every person who built it was on Linux. A
+# `GOOS=darwin go build ./...` costs seconds on a Linux runner and would
+# have caught it at the commit that introduced it.
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+permissions:
+ contents: read
+
+# A second push to a PR makes the first run's answer irrelevant.
+concurrency:
+ group: ci-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ gate:
+ name: build · vet · test (linux)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+ cache: true
+ - run: go build ./...
+ - run: go vet ./...
+ # -count=1 to match CLAUDE.md: never satisfy the gate from a cached
+ # pass. The explicit timeout is above the 10m default because the
+ # root package's e2e tests compile the carlos binary many times over.
+ - run: go test ./... -count=1 -timeout 30m
+
+ cross:
+ name: compile ${{ matrix.goos }}/${{ matrix.goarch }}
+ runs-on: ubuntu-latest
+ strategy:
+ # One platform breaking must not hide the state of the others.
+ fail-fast: false
+ matrix:
+ include:
+ - {goos: darwin, goarch: arm64}
+ - {goos: darwin, goarch: amd64}
+ - {goos: linux, goarch: arm64}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+ cache: true
+ # Compile only — these binaries are never run, so no runner for the
+ # target OS is needed. CGO_ENABLED=0 is already the default when
+ # cross-compiling; it is explicit here so that adding a cgo dependency
+ # fails loudly in this job rather than silently changing what is
+ # covered. The tree is pure Go today (modernc.org/sqlite, no import "C").
+ - run: go build ./...
+ env:
+ GOOS: ${{ matrix.goos }}
+ GOARCH: ${{ matrix.goarch }}
+ CGO_ENABLED: "0"
+
+ gofmt:
+ name: gofmt (changed files)
+ runs-on: ubuntu-latest
+ # Only meaningful against a base to diff from.
+ if: github.event_name == 'pull_request'
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+ # CLAUDE.md's rule is "gofmt -l clean for files you touch", not for the
+ # tree: three pre-existing unformatted files are deliberately left
+ # alone, so a repo-wide check would fail on arrival and teach everyone
+ # to ignore this job. Scoped to the files the PR actually changes.
+ - name: gofmt changed files
+ env:
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
+ run: |
+ set -euo pipefail
+ files=$(git diff --name-only --diff-filter=ACMR "$BASE_SHA"...HEAD -- '*.go')
+ if [ -z "$files" ]; then
+ echo "no Go files changed"
+ exit 0
+ fi
+ echo "checking:"
+ echo "$files" | sed 's/^/ /'
+ unformatted=$(gofmt -l $files)
+ if [ -n "$unformatted" ]; then
+ echo
+ echo "needs gofmt -w:"
+ echo "$unformatted" | sed 's/^/ /'
+ exit 1
+ fi
diff --git a/CLAUDE.md b/CLAUDE.md
index 3ef9f15d4..67bb9728e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -47,6 +47,11 @@ remote form was the bug.)
18 packages, before any commit lands.
- `gofmt -l` clean for files you touch (three pre-existing unformatted files
elsewhere are left alone).
+- `.github/workflows/ci.yml` runs that same gate on every PR, plus a
+ `GOOS=darwin` compile. **The gate is not Linux-only**: `internal/host` is
+ linked into the CLI, which people run on Macs, so a Linux-only type
+ assumption breaks `go build .` for them. Cross-compiling is enough to catch
+ it — no Mac required.
- Sandbox note: use `GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod
GOPROXY=off`; go commands may need the sandbox disabled ("read-only file
system" from the toolchain is the tell).