Guidance for AI agents working in this repository.
CI/CD tooling for the Ray project on Buildkite:
- rayci: Generates Buildkite pipeline definitions from
.buildkite/*.rayci.yaml - wanda: Builds container images using a container registry as a content-addressed build cache
- rayapp: Builds Ray application templates (zip artifacts)
- raycirun: Library for triggering Buildkite builds programmatically
- reefd: EC2/database service with reaper functionality
- raycilint: Quality gates for CI (test coverage, file length checks)
If you are unsure where logic lives:
- pipeline generation + rules + selection:
raycicmd/ - Buildkite "step conversion": likely in
raycicmd/(strategy converters) - container build + registry/cache:
wanda/ - template zips:
rayapp/ - build triggering:
raycirun/ - Quality gates:
raycilint/
- Understand the command/package boundary you’re touching (CLI vs library).
- Make the smallest change that preserves existing behavior.
- Run targeted tests locally, then
go test ./..., thengo fmt ./.... - Keep exported APIs narrow and stable.
# Build rayci
go build .
# Build wanda
go build ./wanda/wanda
# Build rayapp
go build ./rayapp/rayapp
# Build rayci-lint
go build -o /tmp/rayci-lint ./raycilint/raycilint
# Run all tests
go test ./...
# Run tests for a specific package
go test ./raycicmd
go test ./wanda
# Run a specific test
go test ./raycicmd -run TestConverterBasic
# Format code
go fmt ./...
# Build release binaries (creates _release/ directory)
bash release.sh# rayci: generate pipeline YAML
./rayci -repo . -output pipeline.yaml
./rayci -repo . -output - # output to stdout
./rayci -upload # upload directly to Buildkite
# wanda: build container image (local mode)
./wanda spec.wanda.yaml
# wanda: build in RayCI mode (uses RAYCI_* env vars)
./wanda -rayci
# rayci-lint: run coverage checks (default minimum 80%)
./rayci-lint go-coverage
./rayci-lint go-coverage -min-coverage-pct=90
# rayci-lint: check file lengths (default max 500 lines)
./rayci-lint go-filelength
./rayci-lint go-filelength -max-lines=400- Scans
.buildkite/*.rayci.yamlfor step definitions - Detects changed files via git diff (for PRs)
- Applies tag rules from
.buildkite/*.rules.txtto determine which tests run - Filters steps based on tags and selections
- Outputs Buildkite pipeline YAML
- Parses
.wanda.yamlspecification (name, dockerfile, srcs, froms, build_args) - Computes content-addressed digest from build inputs
- Checks cache in registry - skips build on cache hit
- Builds with Docker and pushes to work repository
Tag rules (.buildkite/*.rules.txt) map file paths to test tags:
- Lines declare tags with
! tag_name - Patterns match directories (
dir/), files, or globs (*.py) @ tag1 tag2emits tags for matched files; rules without@are skipping rules;separates rules- Test files (
*.rules.test.txt) validate rules with formatpath: expected_tag1 expected_tag2 - Used for conditional testing based on changed files
Pipeline steps use a strategy pattern with multiple converters (waitConverter, blockConverter, triggerConverter, wandaConverter, commandConverter). Converters implement match() and convert() methods; first match wins with commandConverter as fallback.
Buildkite steps are represented as map[string]any to handle dynamic YAML structures. Helper functions extract typed values: stringInMap(), boolInMap(), intInMap(), toStringList().
- Maximum line length: 100 characters
- Enforced by
golinesin pre-commit hooks - Long lines will be automatically wrapped on commit
- Avoid obvious comments that describe what the code literally does
- Keep comments for: exported functions (godoc), non-obvious behavior, algorithm explanations
- Prefer self-documenting code over comments
- Remove comments like
// Parse the spec filebeforeParseSpecFile()calls
Keep package interfaces narrow. Prefer unexported functions and types unless they need to be accessed from outside the package. For subcommands or internal logic, use unexported functions with the same signature as the main entry point, and separate testable inner functions if needed.
// Good: narrow interface
func Main(args []string, envs Envs) error {
if args[1] == "test-rules" {
return subcmdTestRules(args[2:], envs) // unexported
}
// ...
}
func subcmdTestRules(args []string, envs Envs) error {
return execTestRules(args, envs, os.Stdout) // unexported, testable
}
// Bad: unnecessarily wide interface
func TestRulesMain(args []string, envs Envs, stdout io.Writer) error { // exported
// ...
}Use pointers to structs by default (e.g., []*MyStruct instead of []MyStruct) when performance or memory footprint is not a concern. This avoids needing to revisit the decision when adding more fields to the struct later.
Wrap errors with context using %w at each level. Errors bubble up to Main without inline logging:
if err != nil {
return fmt.Errorf("parse config file: %w", err)
}Use unexported newTypeName constructors (not NewTypeName):
func newBuildInput(spec *Spec, dir string) (*buildInput, error) { ... }Emulate sets using map[string]struct{}:
seen := make(map[string]struct{})
seen[key] = struct{}{}
if _, ok := seen[key]; ok { ... }Environment variables are accessed through the Envs interface, allowing test stubs:
type Envs interface {
Getenv(string) string
}
// Production: osEnvs wraps os.Getenv
// Tests: newEnvsMap(map[string]string{...})-
Format test errors using "got, want" ordering:
if got != want { t.Errorf("FunctionName() = %v, want %v", got, want) }
-
For multiline test strings, prefer
strings.Join()over backtick literals:// Preferred input := strings.Join([]string{"! mytag", "python/", "@ mytag", ";"}, "\n") // Avoid input := `! mytag python/ @ mytag ;`