Contributing to gh-fleet¶
Thanks for helping. This page covers setting up, the checks every change must pass, and how changes get merged.
Set up¶
You need Go (the version in go.mod), git, and uv
for the docs site. golangci-lint, govulncheck and goreleaser are run with
go run at pinned versions, so there is nothing else to install.
git clone https://github.com/rossedman/gh-fleet
cd gh-fleet
make build # builds ./gh-fleet
gh extension install . # use your build as `gh fleet`
Run every check¶
That runs, in order: build, golangci-lint, go test -race with the coverage
gate, tools/check-cmds.sh, doc comment checks, generated docs drift, a
benchmark smoke run, a fuzz smoke run, govulncheck, the strict docs build and
goreleaser check. CI runs the same targets. Smaller targets:
| Target | What it does |
|---|---|
make test |
go test -race ./... |
make cover |
tests with coverage, failing below 90% of hand-written code |
make lint |
golangci-lint, check-cmds, doccheck |
make bench |
every benchmark, results saved to bench/ for benchstat |
make fuzz |
every fuzz target for FUZZTIME (default 30s) |
make docs |
regenerate docs/reference/ from the CLI and the campaign schema |
make schema |
refresh GitHub's GraphQL schema, which every query the client sends is validated against |
make docs-serve |
preview the docs site at http://127.0.0.1:8000 |
make integration |
the opt-in suite against a real throwaway org (see below) |
Two more gates run in CI and, through .claude/settings.json, in Claude Code:
- guardrails (
.guardrails.yaml, strict preset) blocks a Claude turn, and a pull request, until touched packages build, passgo test -race, meet 90% coverage (80% per package) and are lint and govulncheck clean. Run it yourself withguardrails gate --all. - sarifix (
sarifix.yaml) runs golangci-lint and gitleaks, reports what a change introduced withsarifix check, and onmainuploads findings to code scanning and keeps one issue per batch of lint findings.
Both run golangci-lint built with this module's Go version, as make lint
does: the released binaries are built with an older Go and refuse to load
the module, so put a matching build on PATH
(GOTOOLCHAIN=$(go env GOVERSION) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.15.0).
Quality bars¶
- Tests: at least 90% coverage of hand-written code, enforced in CI. All
GitHub access goes through the
internal/ghapi.Clientinterface: test with the in-memory fake ininternal/ghapi/fake, and test git against local bare repos frominternal/testutil. No test talks to the real GitHub API; every GraphQL query the client builds is validated against GitHub's published schema ininternal/ghapi/testdatainstead. Tests run on Linux, macOS and Windows. - Doc comments on every package, function and method, starting with the
identifier's name (
go run ./tools/doccheck ./...). - One cobra command per file in
cmd/, named after the command (tools/check-cmds.sh). Commands parse flags and callinternal/app; no logic lives incmd/. - Benchmarks for every critical path and fuzz targets for every one that handles outside input (see below).
- Terminal output goes through
internal/uiandinternal/render: no colors defined anywhere else, nothing styled when output is piped or--jsonis set, andNO_COLORis honored. Follow gh's conventions: results on stdout and everything else on stderr, tab-separated tables with no header when piped, and gh's exit codes (docs/guides/scripting.md).
Critical paths¶
These functions parse input, sit in hot loops, or decide what gets pushed. Changes to them need a benchmark and, where they take outside input, a fuzz target.
| Function | Why | Benchmark | Fuzz |
|---|---|---|---|
campaign.Parse |
parses user YAML, schema validation, line-numbered errors | FuzzParse |
|
target.Evaluate |
the include/exclude rules, run for every repo in an org | BenchmarkEvaluate |
FuzzEvaluate |
target.Resolver.Resolve |
target resolution over large orgs | BenchmarkResolve5000 |
|
engine.Run |
the worker pool every command runs on | BenchmarkEngineConcurrency{1,8,32} |
|
ghapi.Transport.RoundTrip |
rate limiting, pauses and retries for every API call | ||
engine.Pipeline.Process |
decides when to commit, push, open, update or close a PR | ||
star.localPath |
the sandbox boundary: every path a Starlark script passes to repo |
FuzzLocalPath |
|
star.Script.Run |
runs untrusted-by-default campaign code against a clone | ||
json.encode_like |
rewrites JSON files keeping their layout | FuzzEncodeLike |
|
yamlpath.set |
line-based YAML edits under most library helpers | FuzzYAMLSet |
|
toml.set |
line-based TOML edits | FuzzTOMLSet |
Adding to the Starlark library¶
The built-in library lives in internal/star/lib/*.star. Its reference
pages (docs/reference/starlark-library.md and docs/reference/starlark/)
and their entries in mkdocs.yml are generated from the docstrings by
make docs, so a docstring is the only documentation you write:
def ensure_lines(repo, path, lines, header = None):
"""Appends each of `lines` that a file does not already contain.
Optional prose: how it matches, what it leaves alone.
Args:
path: the file to edit, relative to the repo root.
lines: a list of lines, or one string with a line per line.
header: a line written above the new lines when any are added.
Returns:
The list of lines that were added.
Example:
before .gitignore:
node_modules
call:
ensure_lines(repo, ".gitignore", [".env"])
after .gitignore:
node_modules
.env
returns:
[".env"]
"""
- Every public function needs a summary line, an
Args:entry for each parameter exceptrepo(in order),Returns:, and anExample:.TestLibDocumentedenforces this. - An example lists files with
before <path>:(andcampaign <path>:for files next to the campaign), thecall:(statements, then an expression), files withafter <path>:ordeleted <path>, and thereturns:value as Starlark prints it.TestLibExamplesruns every example, checks the files and the value, and runs it again to check that nothing is written the second time. Files with abeforeand noaftermust be left unchanged. - Use
r"""for a docstring that contains a backslash. - Add table tests for the edge cases to
internal/star/lib*_test.go, and an example campaign underexamples/when the helper serves a common change; the reference links each function to the examples that use it.
Integration tests¶
make integration runs a full campaign (apply twice, status, close) against a
real organization you own. It is skipped unless GH_FLEET_TEST_ORG names an
org whose repos may be changed, and GH_FLEET_TEST_REPO names one of them.
Use a throwaway org.
Commits and pull requests¶
Commit messages and PR titles follow Conventional Commits:
feat(apply): add --reopen to reopen closed PRs
fix(target): treat topic filters as case-insensitive
docs: explain the commit-message override
The release changelog is grouped by these types, so pick the one that fits.
PRs are squash-merged once CI is green and a code owner has approved. Keep a PR to one change; add or update tests and docs with it.
Questions¶
Open a discussion or issue. For security problems, follow the security policy instead.