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