Thanks for your interest in contributing! This document covers everything you need to get started.
- Go 1.23+ (we use Go 1.25)
- Make (optional, but recommended)
- golangci-lint for linting
Install golangci-lint:
# macOS
brew install golangci-lint
# Linux
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
# Windows
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest# Clone your fork
git clone https://github.com/YOUR_USERNAME/cli-with.git
cd cli-with
# Install dependencies
go mod download
# Build the binary
make build
# Or: go build -o with ./cmd/with
# Verify it works
./with version# Run all tests
make test
# Run with race detector
make test-race
# Run specific package tests
go test -v ./internal/crypto/...
# Run integration tests (these build and execute the binary)
go test -v ./tests/...cli-with/
├── cmd/with/ # Main application entry point
├── internal/
│ ├── crypto/ # Encryption and key derivation
│ └── storage/ # Vault storage and keychain integration
├── tests/ # Integration and security tests
├── Makefile # Build and test automation
├── go.mod # Go module definition
└── .golangci.yml # Linter configuration
We follow standard Go conventions with a few specifics:
Run the formatter before committing:
make fmt
# Or: gofmt -w .We use golangci-lint with these linters enabled:
errcheck- Check unchecked errorsgosimple- Simplify codegovet- Vet examines sourceineffassign- Detect ineffective assignmentsstaticcheck- Static analysisunused- Check unused codegofmt- Format checkgoimports- Import formattingmisspell- Spell checkrevive- Fast linterunconvert- Detect unnecessary type conversionsgocyclo- Cyclomatic complexity (max 15)
Run linters:
make lint
# Or: golangci-lint run ./...- Handle all errors - Never ignore errors. Use
_only when explicitly ignoring. - Write tests - New features should include unit tests.
- Keep functions focused - Functions should do one thing well.
- Document exported items - Add doc comments to exported types and functions.
- No external dependencies without discussion - Keep the dependency tree minimal.
We follow Conventional Commits:
<type>(<scope>): <description>
[optional body]
[optional footer(s)]
| Type | Description |
|---|---|
feat |
New feature |
fix |
Bug fix |
docs |
Documentation changes |
style |
Formatting, missing semicolons, etc. (no code change) |
refactor |
Code refactoring |
test |
Adding or updating tests |
chore |
Build process, dependencies, tooling |
feat(exec): add support for custom environment variables
fix(vault): handle corrupted vault file gracefully
docs(readme): update installation instructions
test(crypto): add benchmarks for argon2id key derivation
- Use the imperative mood ("add feature" not "added feature")
- First line should be 50 characters or less
- Body should explain the "why", not the "what"
- Reference issues and pull requests in the footer
- Run tests:
make test - Run linters:
make lint - Format code:
make fmt - Update documentation if needed
- Add tests for new features or bug fixes
- Fork the repository
- Create a feature branch from
main:git checkout -b feat/my-new-feature
- Make your changes
- Commit with conventional commit messages
- Push to your fork
- Open a Pull Request
Use the same format as commit messages:
feat(vault): add key rotation support
Include:
- What: Brief description of changes
- Why: Motivation for the change
- How: Implementation details if relevant
- Testing: How you tested the changes
- Breaking Changes: If any
PRs are reviewed against these criteria:
- Tests pass (
make test) - Linters pass (
make lint) - Code follows project style
- New code has tests
- Documentation updated if needed
- No unnecessary dependencies added
- Commit messages follow conventional commits
- Address review feedback with new commits (not force pushes)
- Keep the PR up to date with
main:git fetch origin git rebase origin/main
Place unit tests in the same package as the code they test:
internal/crypto/aesgcm.go
internal/crypto/aesgcm_test.go
Integration tests go in tests/. These test the full application by building and running the binary.
func TestFunctionName_Scenario_ExpectedResult(t *testing.T) {}Example:
func TestEncrypt_ValidKey_Succeeds(t *testing.T) {}
func TestEncrypt_InvalidKey_ReturnsError(t *testing.T) {}Prefer table-driven tests for multiple scenarios:
func TestValidateKeyName(t *testing.T) {
tests := []struct {
name string
key string
wantErr bool
}{
{"valid", "API_KEY", false},
{"valid underscore prefix", "_SECRET", false},
{"invalid number prefix", "2FAST", true},
{"invalid hyphen", "my-key", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateKeyName(tt.key)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateKeyName(%q) error = %v, wantErr %v", tt.key, err, tt.wantErr)
}
})
}
}- Open a Discussion for questions
- Open an Issue for bugs or feature requests
By contributing, you agree that your contributions will be licensed under the MIT License.