Thank you for your interest in contributing to Plus! This document provides guidelines and information for contributors.
Before creating an issue, please:
- Search existing issues to avoid duplicates
- Use the issue templates when available
- Provide detailed information including:
- Plus version
- Operating system
- Go version (if building from source)
- Steps to reproduce
- Expected vs actual behavior
- Relevant logs or error messages
We welcome feature suggestions! Please:
- Check the roadmap in README.md first
- Open a discussion before implementing large features
- Describe the use case and why it's valuable
- Consider backward compatibility
-
Fork the repository
git clone https://github.com/elastic-io/plus.git cd plus -
Set up development environment
# Install Go 1.21 or later go version # Install dependencies go mod download # Run tests make test
-
Create a feature branch
git checkout -b feature/your-feature-name
-
Make your changes
- Follow the coding standards below
- Add tests for new functionality
- Update documentation as needed
-
Test your changes
# Run all tests make test # Run with coverage make test-coverage # Run linting make lint # Test locally go run ./cmd/plus --config config.dev.yaml
-
Commit your changes
git add . git commit -m "feat: add new feature description"
-
Push and create PR
git push origin feature/your-feature-name
We follow standard Go conventions:
- gofmt for formatting
- golint for linting
- go vet for static analysis
- Effective Go guidelines
// Good
type PackageManager struct {
repoService *service.RepoService
}
func (pm *PackageManager) UploadPackage(ctx context.Context, name string) error {
// implementation
}
// Bad
type pkgMgr struct {
repo_svc *service.RepoService
}
func (p *pkgMgr) upload_pkg(ctx context.Context, n string) error {
// implementation
}// Good - wrap errors with context
func (s *Service) ProcessPackage(filename string) error {
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("failed to read package file %s: %w", filename, err)
}
// process data
return nil
}
// Bad - ignore or lose error context
func (s *Service) ProcessPackage(filename string) error {
data, _ := os.ReadFile(filename) // Don't ignore errors
// process data
return nil
}// Good - structured logging
log.Printf("🔍 Processing package: repo=%s, file=%s", repoName, filename)
log.Printf("✅ Package uploaded successfully: %s", filename)
log.Printf("❌ Upload failed: repo=%s, error=%v", repoName, err)
// Use consistent emoji prefixes:
// 🔍 for debug/info
// ✅ for success
// ❌ for errors
// 🔄 for processing
// 📁 for file operations// Good - RESTful design
GET /repos // List repositories
POST /repos // Create repository
GET /repo/{name} // Get repository info
DELETE /repo/{name} // Delete repository
POST /repo/{name}/upload // Upload package
POST /repo/{name}/refresh // Refresh metadata// Consistent response structure
type Response struct {
Status Status `json:"status"`
Data interface{} `json:"data,omitempty"`
}
type Status struct {
Status string `json:"status"` // "success" or "error"
Message string `json:"message"`
Code int `json:"code"`
}func TestPackageUpload(t *testing.T) {
// Arrange
service := NewTestService()
testFile := createTestRPM(t)
defer os.Remove(testFile)
// Act
err := service.UploadPackage(context.Background(), "test-repo", testFile)
// Assert
assert.NoError(t, err)
assert.FileExists(t, filepath.Join("storage", "test-repo", "Packages", testFile))
}func TestAPIEndToEnd(t *testing.T) {
// Start test server
server := startTestServer(t)
defer server.Close()
// Test repository creation
resp := createRepository(t, server.URL, "test-repo")
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Test package upload
uploadResp := uploadPackage(t, server.URL, "test-repo", "test.rpm")
assert.Equal(t, http.StatusOK, uploadResp.StatusCode)
}plus/
├── cmd/
│ └── plus/ # Main application entry point
├── internal/
│ ├── api/ # HTTP handlers and routing
│ ├── config/ # Configuration management
│ ├── metrics/ # Metrics collection
│ ├── middleware/ # HTTP middleware
│ ├── service/ # Business logic
│ └── types/ # Data structures
├── pkg/
│ ├── client/ # Go client library
│ ├── repo/ # Repository implementations
│ └── storage/ # Storage backends
├── assets/ # Embedded static files
├── docs/ # Documentation
├── scripts/ # Build and deployment scripts
└── tests/ # Integration tests
- Unit Tests - Test individual functions/methods
- Integration Tests - Test component interactions
- API Tests - Test HTTP endpoints
- Performance Tests - Test under load
# All tests
make test
# Unit tests only
go test ./internal/...
# Integration tests
go test ./tests/...
# With coverage
make test-coverage
# Specific package
go test ./internal/service -v
# With race detection
go test -race ./...- Use
testdata/directories for test files - Clean up temporary files in tests
- Use table-driven tests for multiple scenarios
func TestValidatePackage(t *testing.T) {
tests := []struct {
name string
filename string
want bool
wantErr bool
}{
{"valid RPM", "test.rpm", true, false},
{"valid DEB", "test.deb", true, false},
{"invalid extension", "test.txt", false, true},
{"empty filename", "", false, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ValidatePackage(tt.filename)
if (err != nil) != tt.wantErr {
t.Errorf("ValidatePackage() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ValidatePackage() = %v, want %v", got, tt.want)
}
})
}
}- Public functions must have godoc comments
- Complex logic should be commented
- Examples in godoc when helpful
// UploadPackage uploads a package file to the specified repository.
// It validates the package format, stores the file, and updates repository metadata.
//
// Example:
// err := service.UploadPackage(ctx, "my-repo", "package.rpm", reader)
// if err != nil {
// log.Printf("Upload failed: %v", err)
// }
func (s *Service) UploadPackage(ctx context.Context, repoName, filename string, reader io.Reader) error {
// implementation
}- Update
docs/api.mdfor API changes - Include request/response examples
- Document error codes and messages
We use Semantic Versioning:
- MAJOR version for incompatible API changes
- MINOR version for backward-compatible functionality
- PATCH version for backward-compatible bug fixes
- Update version in relevant files
- Update CHANGELOG.md with changes
- Run full test suite
- Update documentation
- Create release PR
- Tag release after merge
- Build and publish artifacts
# Go toolchain
go install golang.org/x/tools/cmd/goimports@latest
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
# Development tools
make install-toolsmake build # Build binary
make test # Run tests
make test-coverage # Run tests with coverage
make lint # Run linters
make fmt # Format code
make clean # Clean build artifacts
make docker-build # Build Docker image
make docker-run # Run in DockerRecommended extensions:
- Go (official)
- REST Client
- Docker
Settings:
{
"go.formatTool": "goimports",
"go.lintTool": "golangci-lint",
"go.testFlags": ["-v", "-race"]
}# Run with debug logging
go run ./cmd/plus --config config.dev.yaml --debug
# Run with race detection
go run -race ./cmd/plus --config config.dev.yaml
# Profile memory usage
go run ./cmd/plus --config config.dev.yaml --profile-
Port already in use
lsof -i :8080 kill -9 <PID>
-
Permission denied on storage
chmod -R 755 storage/
-
Module issues
go mod tidy go mod download
- GitHub Discussions for questions and ideas
- GitHub Issues for bugs and feature requests
- Code Review for implementation feedback
Contributors will be:
- Listed in CONTRIBUTORS.md
- Mentioned in release notes
- Invited to maintainer team (for significant contributions)
Thank you for contributing to Plus! 🚀