Skip to content

Commit 8babf43

Browse files
committed
Add AGENTS.md
Signed-off-by: David Gageot <david.gageot@docker.com>
1 parent c8caf9d commit 8babf43

1 file changed

Lines changed: 142 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# AGENTS.md
2+
3+
## Development commands
4+
5+
The project dogfoods itself: a `gogo.yaml` at the repo root drives
6+
day-to-day development. Use the `gogo` binary for all dev workflows — see
7+
`gogo.yaml` for the exact commands each task runs.
8+
9+
| Goal | Command |
10+
| ----------------- | ------------------ |
11+
| Build, lint, test | `gogo` (default) |
12+
| Build the binary | `gogo build` |
13+
| Run all tests | `gogo test` |
14+
| Run linters | `gogo lint` |
15+
| Format Go sources | `gogo format` |
16+
| Watch + rebuild | `gogo -w dev` |
17+
| Cross-compile all | `gogo build-cross` |
18+
| Clean artifacts | `gogo clean` |
19+
20+
CI (`.github/workflows/ci.yml`) runs tests, `golangci-lint`, and the
21+
multi-platform Docker build. Releases (`release.yml`) are tag-driven and
22+
publish cross-built binaries via `gh release create`.
23+
24+
The Go toolchain version comes from `go.mod` (`go 1.26.2`). Tests run with
25+
`-tests=true` under golangci-lint v2.
26+
27+
## Code style and conventions
28+
29+
- **Linters**: golangci-lint v2 with a long enable list (see
30+
`.golangci.yml`). Notable settings:
31+
- `gofumpt` with `extra-rules: true` and `gofmt` rewrites `interface{}`
32+
`any`.
33+
- `gci` enforces three import groups: standard, default, then
34+
`prefix(github.com/dgageot/gogo)` (custom-order). Match this exactly
35+
when adding imports.
36+
- `depguard` denies `github.com/stretchr/testify` from non-`_test.go`
37+
files.
38+
- `forbidigo` (tests only) bans `context.Background/TODO()`,
39+
`os.MkdirTemp/Setenv/Chdir`, and `fmt.Print*` — use the testing
40+
equivalents (`t.Context()`, `t.TempDir()`, `t.Setenv()`, `t.Chdir()`)
41+
and write to a buffer instead of stdout.
42+
- `revive` requires exported-symbol comments (incl. private receivers)
43+
and package comments; `staticcheck` runs all checks.
44+
- Disabled gocritic checks: `dupImport`, `hugeParam`, `rangeValCopy`,
45+
`unnamedResult`, `appendAssign`.
46+
- **Errors**: wrap with `fmt.Errorf("...: %w", err)`. Aggregate parallel
47+
errors with `errors.Join` (see `runDeps`). `os.ErrNotExist` is matched
48+
with `errors.Is`.
49+
- **Concurrency**: prefer `sync.Map` + `sync.Once` for memoization (see
50+
`taskRun`); use `wg.Go` (Go 1.25+) for fan-out work; always clone slices
51+
you stash on `ShellCommand.Env` (see `cloneShellCommand` in tests).
52+
- **Defaults**: use `cmp.Or(task.Dir, r.tf.Dir)` rather than ad-hoc empty
53+
checks (see `Runner.taskDir`).
54+
- **Sorted iteration**: when iterating maps for any user-visible or
55+
determinism-sensitive output, sort with
56+
`slices.Sorted(maps.Keys(m))` — the codebase does this consistently
57+
(`runner.go`, `vars.go`, `env.go`, `includes.go`).
58+
- **YAML unmarshalling**: when adding a field that should accept both a
59+
string and a struct, follow the `Cmd`/`Dep`/`Var`/`Precondition` pattern
60+
(try string first, then re-unmarshal into a `type plain X` to avoid
61+
recursion).
62+
- **Logging**: never `fmt.Print` in library code. Use `Runner.logTask` or
63+
write to the injected `RunnerIO` / `App.Stdout|Stderr`.
64+
- **Comments on exported symbols**: required by `revive`; keep them short
65+
and descriptive (Martin-Fowler style — say *why*, not *what*).
66+
67+
## Testing guidelines
68+
69+
- Run a single package: `go test ./taskfile -run TestRunWithExtraVars`.
70+
- Force re-run (no cache): `go test --count=1 ./...`.
71+
- Always `testify/require` for fatal preconditions and `testify/assert`
72+
for non-fatal checks. Never use bare `t.Fatal` / `t.Error` for value
73+
comparisons.
74+
- Use `t.Context()`, `t.TempDir()`, `t.Setenv()`, `t.Chdir()` — the linter
75+
enforces this.
76+
- **Test helpers**:
77+
- `taskfile/testhelper_test.go::writeFiles(t, dir, map[string]string)`
78+
writes a tree of files (creates parent dirs).
79+
- `taskfile/run_test.go::newTestRunner(t, tf, dir)` — returns a `Runner`
80+
with `BaseEnv = nil` and a `fakeShellRunner` already wired up.
81+
- `taskfile/run_test.go::fakeShellRunner` — implements `ShellRunner`,
82+
records every `Run`/`Output` call, and supports custom `runFunc` /
83+
`outputFunc` injection. `captureExecs(r)` returns a `*[]Execution`
84+
populated for `ShellCommandTask` calls only.
85+
- `taskfile/run_test.go::envValue(env, key)` — last-match env lookup
86+
(mirrors how `/bin/sh` resolves duplicates).
87+
- `app_test.go::newTestApp(t, dir, args...)` — builds an `App` wired to
88+
byte buffers; pass `dir = ""` to use the real `os.Getwd`.
89+
- Tests construct `Taskfile` literals directly when a YAML round-trip
90+
isn't part of the contract under test — this is the preferred style
91+
for runner-level tests. Use `Parse` / `LoadWithIncludes` only when the
92+
YAML/AST behavior matters.
93+
- Two-row table tests are split into two named tests; reserve table tests
94+
for genuinely repetitive cases (see `TestShellJoinPreservesBoundaries`
95+
for an accepted multi-case test).
96+
97+
## Configuration
98+
99+
- **`gogo.yaml`** — repo's own taskfile (the project eats its own dog food).
100+
Edit when changing dev workflows.
101+
- **`.golangci.yml`** — single source of truth for lint config; keep
102+
`gci.sections` in sync if the module path ever changes.
103+
- **`Dockerfile`** — multi-stage cross build using `tonistiigi/xx`
104+
(`xx-go build`) and `crazymax/osxcross` for darwin targets. CGO is on for
105+
darwin, off otherwise. Update `GO_VERSION` here when bumping `go.mod`.
106+
- **`.github/workflows/`**`ci.yml` (test/lint/build) and `release.yml`
107+
(tag-triggered cross build + GitHub release). Action SHAs are pinned;
108+
use the `ghapin` skill when bumping them.
109+
- **Generated/ignored** (`.gitignore`): `.gogo/` (checksum cache),
110+
`bin/`, `dist/`, `.zig-cache/`.
111+
- **No env vars** are required at runtime; gogo only consumes whatever the
112+
user puts in their own `gogo.yaml` / dotenv files.
113+
114+
## Common development patterns
115+
116+
- **Adding a new top-level CLI flag**: add a tagged field to `args` in
117+
`main.go`, handle it in `App.Run` before `runner.Run` is reached, and
118+
add a test in `app_test.go` using `newTestApp`.
119+
- **Adding a new task field**: add it to `Task` in `taskfile/types.go`,
120+
decide whether `UnmarshalYAML` needs string-shorthand support, thread
121+
it through `Runner.run` in the right phase (deps → vars → requires →
122+
env → preconditions → up-to-date → cmds), and cover it with a literal
123+
`Taskfile` test in `run_test.go` plus a `Parse`-based test in
124+
`parse_test.go` if YAML shape matters.
125+
- **Touching env/var resolution**: respect the existing precedence
126+
(`BaseEnv` < task dotenv < task vars < task env) and the rule that
127+
*task dotenv never overrides global dotenv or OS env* (see
128+
`TestTaskDotenvDoesNotOverrideGlobalDotenv`).
129+
- **Touching include logic**: cycles must be detected by absolute dir
130+
(`includeStack`), nested namespaces are colon-joined
131+
(`parent:child:grandchild`), and dotenv files dedupe globally via
132+
`seenDotenv`. `Namespaces` map keys are absolute dirs and are used by
133+
namespace-aware task name resolution.
134+
- **Adding a shell call**: route it through `Runner.ShellRunner` so tests
135+
can intercept it. Tag it with the right `ShellCommandKind` (`Task`,
136+
`Precondition`, or `Var`).
137+
- **Touching the `op://` path**: the trigger is `hasOpSecrets(env)` over
138+
the *fully-built* env (so dotenv-sourced secrets count). Don't move the
139+
check to before env composition.
140+
- **Editing watch behavior**: source collection is recursive over deps via
141+
`collectSources`; remember to `r.ResetRan()` between iterations or the
142+
memoized first run will be returned forever.

0 commit comments

Comments
 (0)