| 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 |