Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

# DevBox - Multi-service local development CLI tool
# Built with Go 1.23, Cobra CLI, Docker Compose integration

language: "en-US"
tone_instructions: "Review as an experienced Go developer. Be direct and technical. Focus on correctness, security, and idiomatic Go patterns."
early_access: false

reviews:
profile: "assertive"
request_changes_workflow: true
high_level_summary: true
poem: false
review_status: true
collapse_walkthrough: false
auto_review:
enabled: true
drafts: false

path_filters:
- "!vendor/**"
- "!**/*_test.go"
- "!tests/e2e/fixtures/**"

path_instructions:
- path: "cmd/devbox/**/*.go"
instructions: |
This directory contains CLI commands using Cobra framework.
Each command is in a separate file with an init() function that registers via root.AddCommand().

Review for:
- Proper error handling with context wrapping (fmt.Errorf with %w)
- Consistent command structure following existing patterns
- Appropriate use of runWrapper for context handling
- Clear, concise command descriptions
- No over-engineering - only implement what's needed

- path: "internal/**/*.go"
instructions: |
Core packages providing project functionality:
- project/: Project configuration, Docker Compose extensions (x-devbox-*)
- manager/: Project/service autodetection from current directory
- git/: Git operations (clone, sparse checkout, sync)
- cert/: SSL certificate generation
- hosts/: /etc/hosts management with project-scoped markers
- table/: CLI table output formatting

Review for:
- Clean interfaces and proper encapsulation
- Error wrapping with context
- No interface{} - use 'any' instead (enforced by linter)
- YAGNI principle - no speculative features or premature abstractions
- Security considerations for host file and certificate operations

- path: "internal/project/**/*.go"
instructions: |
Project configuration extending Docker Compose with x-devbox extensions:
- x-devbox-sources: Repository definitions with branch and sparse checkout
- x-devbox-scenarios: Named command shortcuts
- x-devbox-hosts: /etc/hosts entries
- x-devbox-cert: SSL certificate domains

State is persisted in .devboxstate JSON file.
Mount system replaces volume binds with local paths.

Review for proper parsing and validation of compose extensions.

- path: "internal/manager/**/*.go"
instructions: |
Project autodetection with three-step process:
1. Check if directory is a local mount of any project
2. Match Git remote URL + path against project source definitions
3. Check if directory is the project's manifest repository

Review for edge cases in path matching and Git remote detection.

- path: "tests/e2e/**/*.go"
instructions: |
E2E tests written in Go using testify/suite.
Tests run against the built devbox binary.

Review for:
- Clear test naming and organization
- Proper cleanup of test resources (containers, temp dirs)
- Realistic test scenarios

tools:
golangci-lint:
enabled: true
gitleaks:
enabled: true
yamllint:
enabled: true
hadolint:
enabled: true
shellcheck:
enabled: true
markdownlint:
enabled: true
actionlint:
enabled: true

chat:
auto_reply: true

knowledge_base:
learnings:
scope: "local"
pull_requests:
scope: "local"
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ jobs:
cache: true

- name: Lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v9
with:
version: latest
version: v2.7.2

- name: Unit tests
run: go test -race ./internal/...
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ TODO
/vendor
.air.toml
build-errors.log
.golangci.yml
site/
tests/e2e/venv/
tests/e2e/__pycache__/
Expand Down
28 changes: 28 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
version: "2"

linters:
enable:
- errcheck
# - wrapcheck # TODO
- govet
- ineffassign
- staticcheck
- goconst
# - gocyclo # TODO
- misspell
- whitespace
- nolintlint
settings:
gocyclo:
min-complexity: 15
exclusions:
paths:
- tmp

formatters:
enable:
- gofmt
- goimports
exclusions:
paths:
- tmp
10 changes: 2 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.PHONY: all tidy vendor lint test test-e2e build clean
.PHONY: all tidy vendor lint test test-e2e

all: tidy vendor lint test build
all: tidy vendor lint test

tidy:
go mod tidy
Expand All @@ -16,9 +16,3 @@ test:

test-e2e:
go test -v ./tests/e2e/ -timeout 10m

build:
go build -o devbox ./cmd/devbox/

clean:
rm -f devbox
4 changes: 2 additions & 2 deletions cmd/devbox/restart.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func init() {

cmd.PersistentFlags().StringSliceVarP(&profiles, "profile", "p", []string{}, "Profile to use")

cmd.RegisterFlagCompletionFunc("profile", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = cmd.RegisterFlagCompletionFunc("profile", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
p, err := manager.AutodetectProject(projectName)
if err != nil {
return []string{}, cobra.ShellCompDirectiveNoFileComp
Expand Down Expand Up @@ -93,7 +93,7 @@ func runRestart(ctx context.Context, p *project.Project, services []string, noDe
return fmt.Errorf("failed to check if services are running: %w", err)
}

if noDeps == false && !isRunning {
if !noDeps && !isRunning {
return nil
}

Expand Down
7 changes: 4 additions & 3 deletions cmd/devbox/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,12 @@ func runRun(ctx context.Context, p *project.Project, command string, args []stri
}

var tty bool
if noTtyFlag {
switch {
case noTtyFlag:
tty = false
} else if scenario.Tty != nil {
case scenario.Tty != nil:
tty = *scenario.Tty
} else {
default:
tty = isTTYAvailable()
}

Expand Down
17 changes: 1 addition & 16 deletions cmd/devbox/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"fmt"
"os/exec"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -78,7 +77,7 @@ func init() {

cmd.PersistentFlags().StringSliceVarP(&profiles, "profile", "p", []string{}, "Profile to use")

cmd.RegisterFlagCompletionFunc("profile", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = cmd.RegisterFlagCompletionFunc("profile", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
p, err := manager.AutodetectProject(projectName)
if err != nil {
return []string{}, cobra.ShellCompDirectiveNoFileComp
Expand All @@ -102,20 +101,6 @@ func getProfileCompletions(p *project.Project, toComplete string) ([]string, cob
return result, cobra.ShellCompDirectiveNoFileComp
}

func getAvailableProfiles(p *project.Project, name string) []string {
allProfileNames := p.AllServices().GetProfiles()
sort.Strings(allProfileNames)

var values []string
for _, profileName := range allProfileNames {
if strings.HasPrefix(profileName, name) {
values = append(values, profileName)
}
}

return values
}

func runBuild(ctx context.Context, p *project.Project) error {
uniqueImages := map[string]bool{}
services := []string{}
Expand Down
11 changes: 7 additions & 4 deletions internal/cert/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,12 @@ func decodePEM[T any](data []byte) (*T, error) {
return nil, fmt.Errorf("failed to parse PEM block: %w", err)
}

return result.(*T), nil
typedResult, ok := result.(*T)
if !ok {
return nil, fmt.Errorf("unexpected type in PEM decoding")
}

return typedResult, nil
}

func generateCertificate(isCA bool, parentCert *x509.Certificate, parentKey *rsa.PrivateKey, commonName string, extra ...string) ([]byte, []byte, error) {
Expand All @@ -262,9 +267,7 @@ func generateCertificate(isCA bool, parentCert *x509.Certificate, parentKey *rsa
certTemplate.NotAfter = time.Now().Add(2 * 365 * 24 * time.Hour) // 2 year for CA
} else {
certTemplate.DNSNames = append(certTemplate.DNSNames, commonName)
for _, name := range extra {
certTemplate.DNSNames = append(certTemplate.DNSNames, name)
}
certTemplate.DNSNames = append(certTemplate.DNSNames, extra...)
}

if parentCert == nil || parentKey == nil {
Expand Down
4 changes: 2 additions & 2 deletions internal/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (s *svc) SetLocalExclude(patterns []string) error {
if err != nil {
return fmt.Errorf("failed to open exclude file: %w", err)
}
defer file.Close()
defer func() { _ = file.Close() }()

for _, pattern := range patterns {
_, err = file.WriteString(pattern + "\n")
Expand All @@ -54,7 +54,7 @@ func (s *svc) SetLocalExclude(patterns []string) error {
func (s *svc) Sync(ctx context.Context, url, branch string, sparseCheckout []string) error {
// if there is no `.git` directory we should not to try to reset because it will try to lock a repo above
if _, err := os.Stat(filepath.Join(s.targetPath, ".git")); os.IsNotExist(err) {
os.RemoveAll(s.targetPath)
_ = os.RemoveAll(s.targetPath)
}

isExist := false
Expand Down
2 changes: 1 addition & 1 deletion internal/hosts/hosts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TestSave(t *testing.T) {

tempFile, err := os.CreateTemp("", "hosts-test")
assert.NoError(t, err)
defer os.Remove(tempFile.Name())
defer func() { _ = os.Remove(tempFile.Name()) }()

err = os.WriteFile(tempFile.Name(), []byte(tc.initialContent), 0644)
assert.NoError(t, err)
Expand Down
25 changes: 11 additions & 14 deletions internal/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ type Project struct {
LocalMounts map[string]string // some service's full mount path -> local path
HasHosts bool

hostConfigs HostConfigs
envFiles []string
envFiles []string
}

func init() {
Expand All @@ -43,7 +42,7 @@ func init() {
consts.ComposeDisableDefaultEnvFile,
consts.ComposeProfiles,
} {
os.Unsetenv(envName)
_ = os.Unsetenv(envName)
}
}

Expand Down Expand Up @@ -196,23 +195,23 @@ func loadState(p *Project) error {

func applySources(p *Project) error {
if s, ok := p.Extensions["x-devbox-sources"]; ok {
p.Sources = s.(SourceConfigs) // nolint: forcetypeassert
p.Sources = s.(SourceConfigs) //nolint: forcetypeassert
}

return nil
}

func applyScenarios(p *Project) error {
if s, ok := p.Extensions["x-devbox-scenarios"]; ok {
p.Scenarios = s.(ScenarioConfigs) // nolint: forcetypeassert
p.Scenarios = s.(ScenarioConfigs) //nolint: forcetypeassert
}

return nil
}

func applyHosts(p *Project) error {
if s, ok := p.Extensions["x-devbox-hosts"]; ok {
hostConfigs := s.(HostConfigs) // nolint: forcetypeassert
hostConfigs := s.(HostConfigs) //nolint: forcetypeassert

ipToHosts := make(map[string][]string)
for _, item := range hostConfigs {
Expand Down Expand Up @@ -246,7 +245,7 @@ func applyHosts(p *Project) error {

func applyCert(p *Project) error {
if s, ok := p.Extensions["x-devbox-cert"]; ok {
p.CertConfig = s.(CertConfig) // nolint: forcetypeassert
p.CertConfig = s.(CertConfig) //nolint: forcetypeassert

if p.CertConfig.CertFile != "" {
p.CertConfig.CertFile = p.absPath(p.CertConfig.CertFile)
Expand All @@ -263,7 +262,7 @@ func setupGracePeriod(p *Project) error {
var defaultStopGracePeriod *Duration

if s, ok := p.Extensions["x-devbox-default-stop-grace-period"]; ok {
v := s.(Duration) // nolint: forcetypeassert
v := s.(Duration) //nolint: forcetypeassert
defaultStopGracePeriod = &v
}

Expand Down Expand Up @@ -305,7 +304,7 @@ func applyLabels(p *Project) error {
}

func mountSourceVolumes(p *Project) error {
fullPathToSources := filepath.Join(p.WorkingDir)
fullPathToSources := p.WorkingDir

for name, service := range p.Services {
envPrefix := fmt.Sprintf("DEVBOX_%s_", convertToEnvName(service.Name))
Expand Down Expand Up @@ -353,12 +352,10 @@ func convertToEnvName(name string) string {
if unicode.IsLetter(char) || unicode.IsDigit(char) {
result.WriteRune(unicode.ToUpper(char))
prevWasUnderscore = false
} else {
} else if !prevWasUnderscore {
// Replace invalid characters with underscores, avoiding consecutive underscores
if !prevWasUnderscore {
result.WriteRune('_')
prevWasUnderscore = true
}
result.WriteRune('_')
prevWasUnderscore = true
}
}

Expand Down
Loading