Thank you for your interest in contributing to Conductor! This document provides guidelines and setup instructions for contributors.
- Go 1.22 or later
- Git
- Clone the repository:
git clone https://github.com/tombee/conductor.git
cd conductor- Install dependencies:
go mod download- Build and test:
make build # Build the conductor binary
make test # Run all unit tests
make lint # Run golangci-lint- (Optional) Set up pre-commit hooks:
pip install pre-commit
pre-commit install| Target | Description |
|---|---|
make build |
Build the conductor binary |
make test |
Run all unit tests |
make test-integration |
Run integration tests (requires API keys) |
make lint |
Run golangci-lint |
make coverage |
Generate test coverage report |
make install |
Install conductor binary to /usr/local/bin |
make clean |
Clean build artifacts |
- Follow standard Go conventions (see Effective Go)
- Use
gofmtto format all code - Write clear, concise GoDoc comments for all exported symbols
- Keep functions focused and testable
- Prefer interfaces over concrete types in public APIs
Conductor uses a typed error system for consistent, user-friendly error handling:
Return typed errors from pkg/errors for expected failure modes:
import conductorerrors "github.com/tombee/conductor/pkg/errors"
// GOOD: Typed error for expected failure
func GetWorkflow(id string) (*Workflow, error) {
wf := store.Find(id)
if wf == nil {
return nil, &conductorerrors.NotFoundError{
Resource: "workflow",
ID: id,
}
}
return wf, nil
}
// BAD: Generic error string
func GetWorkflow(id string) (*Workflow, error) {
wf := store.Find(id)
if wf == nil {
return nil, errors.New("not found")
}
return wf, nil
}Available error types:
ValidationError: User input validation failuresNotFoundError: Resource not foundProviderError: LLM provider failuresConfigError: Configuration problemsTimeoutError: Operation timeouts
Wrap errors from external packages with context (enforced by wrapcheck linter):
// GOOD: Wrapped with context
data, err := os.ReadFile(path)
if err != nil {
return conductorerrors.Wrapf(err, "reading config file %s", path)
}
// BAD: No context (wrapcheck violation)
data, err := os.ReadFile(path)
if err != nil {
return err
}Include suggestions that users can act on:
// GOOD: Actionable suggestion via typed error
return &conductorerrors.ValidationError{
Field: "workflow_name",
Message: "name cannot contain special characters",
Suggestion: "Use only alphanumeric characters and hyphens",
}
// GOOD: Actionable suggestion via UserVisibleError
return &operation.Error{
Type: operation.ErrorTypeAuth,
Message: "Authentication failed",
SuggestText: "Check API key in config.yaml or GITHUB_TOKEN environment variable",
}Use errors that support Unwrap() to preserve error chains:
// GOOD: Preserves cause for errors.Is/As
return &conductorerrors.ProviderError{
Provider: "anthropic",
Message: "request failed",
Cause: originalErr, // Supports errors.Is/As
}
// BAD: Loses original error type
return &conductorerrors.ProviderError{
Provider: "anthropic",
Message: fmt.Sprintf("request failed: %v", originalErr),
}Use errors.As() for typed errors, errors.Is() for sentinel errors:
import (
"errors"
conductorerrors "github.com/tombee/conductor/pkg/errors"
)
// GOOD: Type-safe error checking
var notFoundErr *conductorerrors.NotFoundError
if errors.As(err, ¬FoundErr) {
log.Printf("Resource not found: %s/%s", notFoundErr.Resource, notFoundErr.ID)
return
}
// BAD: String matching
if strings.Contains(err.Error(), "not found") {
// Fragile and breaks with wrapping
}Existing domain errors should implement UserVisibleError for CLI integration:
type MyError struct {
Message string
SuggestText string
}
func (e *MyError) Error() string { return e.Message }
func (e *MyError) IsUserVisible() bool { return true }
func (e *MyError) UserMessage() string { return e.Message }
func (e *MyError) Suggestion() string { return e.SuggestText }For more details, see:
- Error Handling Guide - Comprehensive developer guide
- Error Codes Reference - Catalog of error types
- Packages: Short, lowercase, single-word names (e.g.,
workflow,llm,agent) - Interfaces: Describe behavior (e.g.,
Provider,Storage,Executor) - Structs: Nouns describing the entity (e.g.,
WorkflowDefinition,ModelInfo) - Functions: Verbs describing the action (e.g.,
Execute,Register,Stream)
- Write evergreen comments that explain what and why, not when or how it changed
- Avoid temporal references like "added for feature X" or "updated in v2.0"
- Git history tracks changes - comments should be timeless
- Example:
// Good: Provider interface abstracts LLM API clients for swappable implementations type Provider interface { ... } // Bad: Provider interface added in v0.1 to support multiple LLM providers type Provider interface { ... }
All contributions must meet these testing standards:
- Coverage: 80%+ for
pkg/*(embeddable packages), 70%+ forinternal/* - All exported functions must have tests
- Test both happy paths and error conditions
- Use table-driven tests for multiple scenarios
- Mock external dependencies (LLM APIs, filesystem, network)
Example:
func TestProviderRegistry(t *testing.T) {
tests := []struct {
name string
setup func(*Registry)
want error
}{
{"register valid provider", setupValid, nil},
{"register duplicate provider", setupDuplicate, ErrDuplicate},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// test implementation
})
}
}- Demonstrate features work end-to-end
- Use real components where possible (e.g., SQLite)
- Mock only external services (LLM APIs)
- Place in
*_integration_test.gofiles
- Unit tests: Same package,
*_test.gosuffix - Integration tests: Same package,
*_integration_test.gosuffix - Test helpers:
testutil/package - Mock implementations:
mocks/subdirectory per package
Every pull request automatically runs:
- Test: Unit tests via
make test - Lint: Code quality checks via golangci-lint
- Build: Binary compilation verification
For security reasons, pull requests from forks have limited CI capabilities:
| Check | Fork PRs | Same-repo PRs |
|---|---|---|
| Unit tests | ✅ Run | ✅ Run |
| Lint | ✅ Run | ✅ Run |
| Build | ✅ Run | ✅ Run |
| Integration tests | ⏭️ Skipped | ✅ Run |
Why? Integration tests require API keys (Anthropic, OpenAI) which cannot be safely exposed to fork PRs. After a maintainer reviews and merges your PR, integration tests run automatically on the main branch.
Releases are automated via GoReleaser when a version tag is pushed:
git tag v0.1.0
git push origin v0.1.0This triggers the release workflow which builds binaries for all platforms and creates a GitHub Release.
-
Create a feature branch:
git checkout -b feature/your-feature-name
-
Make your changes:
- Write code following the style guidelines
- Add tests for new functionality
- Update documentation as needed
- Run tests and linter locally
-
Commit your changes:
- Use clear, descriptive commit messages
- Follow conventional commits format:
feat:,fix:,docs:,test:,refactor: - Example:
feat(llm): add OpenAI provider implementation
-
Push to your fork:
git push origin feature/your-feature-name
-
Open a Pull Request:
- Use the PR template
- Link to any related issues
- Provide a clear description of changes
- Ensure CI passes
Before submitting, verify:
- Tests cover new/changed code (coverage does not decrease)
- GoDoc comments on all new exported types/functions
- README updated if user-facing behavior changes
- CHANGELOG.md entry added for notable changes
- All tests pass locally (
go test ./...) - Linter passes (
golangci-lint run) - No foreman-specific imports in
pkg/*packages
Every PR must include appropriate documentation:
- GoDoc comments: All exported symbols (types, functions, constants)
- README updates: Changes to user-facing behavior or APIs
- Architecture docs: Significant design decisions
- Runbooks: Operational procedures for new features
- CHANGELOG: User-facing changes following Keep a Changelog
All contributions require code review before merging:
- Maintainers will review for code quality, test coverage, and documentation
- Address review feedback promptly
- Be open to suggestions and constructive criticism
- Once approved, a maintainer will merge your PR
Packages in pkg/ are designed for embedding in external projects:
- No foreman dependencies: Never import foreman-specific code
- Interface-driven: Expose interfaces, not concrete types
- Stable APIs: Breaking changes require major version bump
- Well-documented: Every exported symbol has GoDoc comments
- Fully tested: 80%+ coverage required
Packages in internal/ are foreman-specific implementation details:
- May import foreman-specific code
- Not available to external consumers
- Can have breaking changes without version bump
- 70%+ coverage required
- Questions: Open a GitHub Discussion
- Bug Reports: Open a GitHub Issue with reproduction steps
- Feature Requests: Open a GitHub Issue describing the use case
- Security Issues: Email security@tombee.com (do not open public issues)
By contributing to Conductor, you agree that your contributions will be licensed under the Apache 2.0 License.