The per-push quality bar Latere's Go repositories share, as one binary,
lateregate, that a repository pins in go.mod and runs with no
arguments. It decides which gates apply by reading the tree, runs all of
them, and reports every failure in one pass. The same binary runs in the
pre-commit and pre-push hooks, on a laptop, and in CI, so a gate cannot
pass in one place and fail in another.
This repository owns what each gate asserts.
latere-ai/ci owns the GitHub Actions
workflows that run the binary on a runner and cut releases. The two
version independently: a repository takes new gates by bumping its
go.mod pin, and a new pipeline when the @v1 tag it calls moves.
In a Go module:
go get -tool latere.ai/x/ci-gate/cmd/lateregate
go tool lateregate initinit writes the wiring the bar expects, where it is missing: the CI
caller at .github/workflows/ci.yml, the two git hooks under
.githooks/ (and git config core.hooksPath .githooks), two lines in
.gitignore, and a CHANGELOG.md with an ## Unreleased heading. It
never overwrites a changelog or a pre-push hook that exists, and never
touches Makefile or .lateregate.yaml.
Then write the two decisions the tool cannot make for you in
.lateregate.yaml:
identity:
role: none # a library or a tool; a service declares its own role
license:
spdx: MIT
holder: Your NameStamp the notice on every file, check the wiring, and run the bar:
go tool lateregate license -w # writes the SPDX notice where it is missing
go tool lateregate contract # "in shape", or every drift named
go tool lateregate # the whole barCommit the result. A release workflow is a separate caller, chosen by what the repository ships; the ci README lists them.
lateregate with no arguments runs every gate that applies, does not
stop at the first failure, and puts the summary last:
PASS fmt-check
PASS modernize
SKIP spec-lint tracks no specs/ files
FAIL suite the suite's coverage is under the floor: below 90%: internal/store 71.2%, ...
FOLD test into suite
FOLD race into suite
FOLD hermetic into suite
FOLD tempdir into suite
FOLD containers into suite
FOLD cover into suite
lateregate: 1 of 13 gates failed: suite
| Gate | What it asserts |
|---|---|
fmt-check |
no tracked Go source is unformatted |
modernize |
no code that a standard library call already covers |
cgo-free |
no Go file imports "C" |
otel-client |
no outbound HTTP client is built without a tracing transport |
container-labels |
every package that imports testcontainers-go imports reap, which labels its containers |
license |
every source file carries the declared SPDX notice |
spec-lint |
the spec tree agrees with itself and with its index |
depcheck |
no build reaches a dependency nobody admitted |
registers |
no developer sentence in a string handed to a user-facing function |
platform-via-sdk |
no platform route outside the Go SDK, latere.ai/x/sdk-go |
words |
no word on the organization's do-not-use list, which the binary carries, in what a model or a person reads |
identity |
the repository holds the rules of the identity role it declares |
postgres |
the repository holds the Postgres connection role it declares |
enum-go, enum-typescript |
declared enum domains use named members and exhaustive switches |
lean |
every Lake project the repository tracks builds with no warning, so no proof uses sorry |
lint |
golangci-lint at a pinned version, against a shared config rendered on every run |
vuln |
govulncheck at a pinned version finds no reachable vulnerability |
suite |
go vet, then one run of the test suite that is race-clean, over a per-package coverage floor, leaves nothing under TMPDIR and no container behind, and needs only the Go toolchain on PATH |
A gate that does not apply is skipped with the reason: spec-lint needs a
specs/ directory, container-labels a package that imports
testcontainers-go, depcheck, registers and the enum gates a key
naming what to check, platform-via-sdk its
platform_via_sdk.enabled key, which a client sets, words a place a
model or a person reads, and lean a tracked
lakefile.toml or lakefile.lean. test, race, hermetic, tempdir, containers
and cover are the six properties suite checks in one run, and each
still runs by name for isolating one of them. docs/gates.md is the
reference: what each gate checks, how it fails, and how to configure it.
go tool lateregate list prints the plan without running it, and go tool lateregate <gate> runs one gate.
A test helper that starts a container labels it and sweeps before it
starts one, with the reap package this module ships:
import "latere.ai/x/ci-gate/reap"
_, _ = reap.Sweep(ctx) // what an exited test binary left, on this machine
ctr, err := postgres.Run(ctx, image, testcontainers.WithLabels(reap.Labels()))
if err != nil && ctr != nil {
_ = ctr.Terminate(ctx) // a failed start still made a container
}A network the helper makes takes the same labels, through
network.WithLabels(reap.Labels()). The labels name the process that owns
the container and the lateregate run it belongs to. suite sweeps before it runs and, after go test
exits, removes and fails on every container the run left. Between them a
container is removed when its test binary panics, is killed, or has no
TestMain, on engines where testcontainers' own reaper cannot run, such
as Podman. A container without the labels is invisible to both, so
container-labels fails a package that imports testcontainers-go and not
reap. docs/gates.md
has the details.
A gate that applies runs unless the repository has written down that it is behind, and until when:
waive:
cover:
reason: the tree is at 82.2% and the gap is in handler and runner
until: 2026-11-15Both fields are mandatory. After the date the gate runs and fails on its own terms.
Everything else in .lateregate.yaml is a decision with its reason:
a coverage exemption, a spec vocabulary, a dependency allowlist. The tool
decides every other value by default, rejects unknown keys, and reports a
key that restates its default. docs/configuration.md
is the full reference.
| Gates | every gate: what it asserts, when it applies, how it fails, its keys |
| Commands | every subcommand and flag, the wiring contract checks, and the two hooks |
| Configuration | .lateregate.yaml: defaults, waivers, and every key |
| Releasing | the changelog rule, lateregate release, stamps, and the CI guard before a cut |
init writes the caller. It is the whole CI configuration for the bar:
jobs:
gate:
uses: latere-ai/ci/.github/workflows/lateregate.yml@v1The reusable workflow asks the binary for its plan (lateregate list -json) and runs one job per gate that runs, so a repository never lists
its gates in YAML. Its inputs and runner options are in the
ci README.
CONTRIBUTING.md is how to build, test and release
this tool. The design and the reasoning behind each gate are in
specs/.
MIT. See LICENSE.