Thank you for your interest in contributing to Plumber! This guide will help you get started.
- AI Usage Policy
- Code of Conduct
- Getting Started
- How to Contribute
- Development Setup
- Coding Conventions
- Commit Conventions
- Review Process
If you use AI tools (e.g. Cursor, Claude Code, Copilot) to contribute to Plumber, please read our AI Usage Policy first. All AI usage must be disclosed, and AI-assisted pull requests must reference an accepted issue and be fully verified by a human.
Please be respectful and constructive in all interactions. We're building this together.
- Fork the repository on GitHub
- Clone your fork locally:
git clone https://github.com/YOUR_USERNAME/plumber.git cd plumber - Add the upstream remote:
git remote add upstream https://github.com/getplumber/plumber.git
Before opening an issue, please:
- Search existing issues to avoid duplicates
- Use a clear, descriptive title
- Provide as much context as possible:
- Plumber version (
plumber --version) - GitLab version (if relevant)
- Operating system
- Steps to reproduce
- Expected vs actual behavior
- Relevant logs (use
--verboseflag)
- Plumber version (
- Bug Report: Something isn't working as expected
- Feature Request: Suggest a new feature or enhancement
- Question: Ask for help or clarification
-
Create a branch from
main:git checkout -b feature/your-feature-name # or git checkout -b fix/your-bug-fix -
Make your changes following our coding conventions
-
Build and test your changes:
make build make test make lint -
Commit your changes following our commit conventions
-
Push:
git push origin feature/your-feature-name
-
Open a Pull Request against
mainwith:- A clear title and description
- Reference to related issues (e.g., "Fixes #123")
- Screenshots/output examples if applicable
- "Allow edits from maintainers" enabled (checked by default on GitHub). This lets maintainers push fixes or rebases directly to your branch, which speeds up the review process.
- Go 1.25 or later
- Make
- Git
- A GitLab token with
read_api+read_repositoryscopes (for testing against a real project)
Always use make build instead of go build directly. The Makefile embeds the default .plumber.yaml configuration into the binary (required for plumber config generate to work):
make buildThis runs two steps:
- Copies
.plumber.yamlintointernal/defaultconfig/default.yaml(with a build header) - Compiles the Go binary
| Target | Description |
|---|---|
make build |
Embed config + build binary |
make build-all |
Cross-compile for Linux, macOS, and Windows |
make test |
Embed config + run all tests |
make lint |
Embed config + lint code |
make run |
Embed config + go run . (quick dev iteration) |
make install |
Build + install to /usr/local/bin/ |
make clean |
Remove binary and generated default.yaml |
View configuration (no GitLab token needed — useful for testing config changes):
# View the default config
./plumber config view
# View a custom config file
./plumber config view --config my-test.yaml
# Generate a default config file
./plumber config generate --output test-config.yamlRun analysis (requires a GitLab token):
export GITLAB_TOKEN=glpat-xxxx
# Auto-detect from git remote
./plumber analyze
# Specify project explicitly
./plumber analyze --gitlab-url https://gitlab.com --project mygroup/myproject
# With debug output
./plumber analyze --verbose
# Lower threshold for testing
./plumber analyze --threshold 50
# Save JSON output
./plumber analyze --output results.json# Run all tests
make test
# Run tests for a specific package
go test ./configuration/ -v
# Run a specific test
go test ./configuration/ -run TestParseRequiredExpression -vThe expression parser (configuration/expression_test.go) has comprehensive test coverage for the required expression syntax. If you're working on expression parsing, run those tests frequently:
go test ./configuration/ -v -count=1The project includes Go fuzz tests for the expression parser and the git remote URL parser. These exercise the parsers with random inputs to catch panics, crashes, and unexpected behavior.
# Run fuzz tests (default 10s each)
go test -fuzz=FuzzParseRequiredExpression ./configuration/ -fuzztime=30s
go test -fuzz=FuzzParseGitRemoteURL ./utils/ -fuzztime=30sIf a fuzz test finds a crash, Go saves the failing input in testdata/fuzz/ inside the package directory. These corpus entries are committed to the repo so the regression is covered by go test going forward.
plumber/
├── main.go # Entry point
├── Makefile # Build, test, install targets
├── .plumber.yaml # Source-of-truth default configuration
│
├── cmd/ # CLI commands (Cobra)
│ ├── root.go # Root command + global flags
│ ├── analyze.go # plumber analyze
│ ├── config.go # plumber config view / generate
│ └── version.go # plumber version
│
├── configuration/ # Config loading, types, and validation
│ ├── configuration.go # Runtime Configuration struct
│ ├── plumberconfig.go # PlumberConfig YAML schema + loading
│ ├── expression.go # Boolean expression parser (required field)
│ └── expression_test.go # Expression parser tests
│
├── control/ # Compliance controls (evaluation logic)
│ ├── types.go # AnalysisResult + all result/metric types
│ ├── task.go # RunAnalysis() orchestrator
│ └── control*.go # Individual control implementations
│
├── collector/ # Data collection from GitLab APIs
│ └── dataCollection*.go # Pipeline origin, image, protection data
│
├── gitlab/ # GitLab API client (REST + GraphQL)
│ ├── client.go # HTTP client with retry + token masking
│ ├── project.go # Project details fetching
│ ├── models.go # Data models
│ ├── utils.go # Pattern matching, version comparison
│ └── utilsCI.go # CI config parsing, variable resolution
│
├── utils/ # Shared utilities
│ ├── gitremote.go # Auto-detect GitLab URL/project from git remote
│ └── hash.go # FNV-1a hashing
│
├── internal/
│ └── defaultconfig/ # Embedded default config (generated by make build)
│ ├── embed.go # go:embed directive
│ └── default.yaml # Auto-generated — do not edit directly
│
└── templates/
└── plumber.yml # GitLab CI component template
- Adding/modifying a control: Look at an existing
control/control*.gofile as a template. Each control has a conf struct, result struct, andRun()method. - Data collection: If a control needs more data, the
collector/package is responsible for gathering it from GitLab and passing it to controls. All actual GitLab API calls (REST and GraphQL) live in thegitlab/package — collectors use those, controls never call the API directly. - Configuration changes: Update
configuration/plumberconfig.gofor the Go types,.plumber.yamlfor the default config, andinternal/defaultconfig/default.yamlwill be regenerated bymake build. - Expression parser:
configuration/expression.gohandles therequiredfield syntax (e.g.,component/a AND component/b OR component/c). Seeconfiguration/expression_test.gofor examples. - CLI output:
cmd/analyze.gocontains the text output formatting (tables, colors).
- Follow standard Go conventions
- Use
gofmtto format code - Use meaningful variable and function names
- Add comments for exported functions and complex logic
- Handle errors explicitly - don't ignore them
- Use
logrusfor structured logging - Include relevant context fields:
l := logrus.WithFields(logrus.Fields{ "action": "FunctionName", "projectPath": projectPath, }) l.Info("Descriptive message")
- Use appropriate log levels:
Debug: Detailed info for troubleshootingInfo: General operational messagesWarn: Recoverable issuesError: Failures that need attention
- Return errors with context:
if err != nil { return fmt.Errorf("failed to fetch project: %w", err) }
- Log errors at the point where they're handled, not where they're created
When adding new fields to .plumber.yaml:
- Add the Go struct field in
configuration/plumberconfig.go - Add the field with YAML comments in
.plumber.yaml - Run
make buildto regenerateinternal/defaultconfig/default.yaml - Update
cmd/config.goif the field needs special display handling inconfig view - Update the README control documentation
We use Conventional Commits with scopes. This enables automated releases via semantic-release.
<type>(<scope>): <description>
| Type | Description | Triggers Release? |
|---|---|---|
feat |
New feature | ✅ Patch |
fix |
Bug fix | ✅ Patch |
perf |
Performance improvement | ✅ Patch |
refactor |
Code refactoring | ✅ Patch |
docs |
Documentation only | ❌ No |
style |
Formatting, whitespace | ❌ No |
test |
Adding/updating tests | ❌ No |
chore |
Maintenance, deps | ❌ No |
ci |
CI/CD changes | ❌ No |
Breaking changes (add ! after type, e.g., feat(api)!: remove endpoint) trigger a minor release.
Use a scope that describes the area of change:
analysis- Core analysis logiccontrols- Compliance controlscomponent- GitLab CI componentconf- Configuration handlingexpr- Expression parseroutput- CLI output formattinglog- Loggingdocs/readme- Documentation
feat(controls): add support for MR approval rules
fix(analysis): resolve variable expansion in nested includes
feat(expr): add NOT operator to required expression syntax
docs(readme): update token requirements
refactor(collector): extract image parsing into separate function
feat(component)!: change default threshold to 100
chore(deps): update go-gitlab to v0.100.0
- Use imperative mood ("add" not "added")
- Keep the commit message under 72 characters
- Scope is encouraged but optional
- Reference issues in the PR description (not in commit messages)
-
Before submitting, ensure your code:
- Builds successfully (
make build) - Passes tests (
make test) - Lints correctly (
make lint) - Is formatted (
gofmt -w .)
- Builds successfully (
-
Code review by maintainers:
- We aim to review PRs within a few days
- Be open to feedback and iterate
- Keep discussions focused and constructive
-
Merge requirements:
- At least one maintainer approval
- No unresolved conversations
- Up-to-date with
main
-
After merge:
- Delete your feature branch
- Semantic-release will automatically create a new version if your commit type triggers a release (see Commit Conventions)
If you have questions about contributing, feel free to:
- Open a GitHub Discussion
- Ask in an issue
- Join our Discord
Thank you for contributing to Plumber!