Contributing to OpenFrame CLI
Thank you for contributing to OpenFrame CLI! This guide covers everything you need to know to submit high-quality contributions.
Before You Start
- Join the OpenMSP Slack community to discuss your ideas before starting large features.
- Read the Architecture documentation to understand the codebase.
- Set up your development environment and verify you can build and run locally.
Code Style and Conventions
Go Style
OpenFrame CLI follows standard Go conventions:
# Format and organize imports
goimports -w .
# Run vet
go vet ./...
# Run linter (if golangci-lint is configured)
golangci-lint run
Naming Conventions
| Element |
Convention |
Example |
| Package names |
Lowercase, single word |
cluster, executor, redact |
| Exported types |
PascalCase |
ClusterService, CommandExecutor |
| Unexported types |
camelCase |
clusterManager, mockExecutor |
| Constants |
PascalCase (exported), camelCase (unexported) |
DefaultClusterName, maxRetries |
| Test files |
_test.go suffix |
service_test.go |
| Test functions |
Test prefix + PascalCase |
TestCreateClusterSuccess |
Error Handling
- Wrap errors with context using
fmt.Errorf("context: %w", err).
- Use
shared/errors types for structured errors (CommandError, AlreadyHandledError).
- Use
friendlyHint patterns for user-facing errors.
- Never swallow errors silently — return them or log them.
// GOOD: Wrap with context
if err := mgr.CreateCluster(ctx, cfg); err != nil {
return fmt.Errorf("creating cluster %q: %w", cfg.Name, err)
}
// BAD: Lost context
if err := mgr.CreateCluster(ctx, cfg); err != nil {
return err
}
Command Structure
When adding a new Cobra command, follow the established pattern:
func getMyCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "mycommand [name]",
Short: "One-line description",
Long: `Multi-line detailed description.
The long description should explain what the command does,
when to use it, and any important caveats.`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
// Validate input
// Delegate to service layer
// Handle errors via sharedErrors.HandleGlobalError
return nil
},
}
// Add flags
cmd.Flags().StringVar(&flagVar, "flag-name", "default", "Flag description")
return cmd
}
Service Layer Conventions
- Services must accept interfaces (not concrete types) for all dependencies.
- Always accept
context.Context as the first argument for cancellable operations.
- Return descriptive errors, not boolean success flags.
- Use the
CommandExecutor interface for all external binary invocations — never os/exec directly.
Branch Naming
| Type |
Pattern |
Example |
| Feature |
feature/<short-description> |
feature/add-kind-provider |
| Bug fix |
fix/<short-description> |
fix/cluster-delete-timeout |
| Documentation |
docs/<short-description> |
docs/update-contributing-guide |
| Refactor |
refactor/<short-description> |
refactor/extract-helm-manager |
| Test |
test/<short-description> |
test/add-bootstrap-integration |
| Chore |
chore/<short-description> |
chore/update-go-dependencies |
Rules:
- Use lowercase and hyphens only (no underscores, no uppercase).
- Keep descriptions short and meaningful.
- Branch from
main unless working on a specific release branch.
Commit Message Format
OpenFrame CLI uses Conventional Commits:
<type>(<scope>): <short description>
[optional body]
[optional footer(s)]
Types
| Type |
When to Use |
feat |
A new feature |
fix |
A bug fix |
docs |
Documentation changes only |
refactor |
Code change that neither fixes a bug nor adds a feature |
test |
Adding or modifying tests |
chore |
Build process, dependency updates, tooling |
perf |
Performance improvements |
ci |
CI/CD configuration changes |
This repository squash-merges PRs, and the PR title is the analyzed commit subject for release versioning — a free-form title contributes nothing to any release. See Releasing for details.
Scopes (Optional but Recommended)
| Scope |
Area |
cluster |
Cluster commands and services |
app |
App commands and chart services |
bootstrap |
Bootstrap command and service |
prereq |
Prerequisites system |
update |
Self-update mechanism |
executor |
Command executor |
k8s |
Kubernetes client package |
argocd |
ArgoCD provider |
helm |
Helm provider |
ui |
Terminal UI and wizards |
errors |
Error handling |
redact |
Secret redaction |
Examples
feat(cluster): add --wait flag to cluster create command
Adds a --wait flag that blocks until all cluster nodes are Ready.
Useful for CI pipelines that need the cluster immediately after creation.
fix(argocd): handle stalled sync after ref change
docs(contributing): add commit message guidelines
test(bootstrap): add integration test for non-interactive mode
chore: upgrade go-git to v5.12.0
Pull Request Process
Before Opening a PR
# 1. Ensure all tests pass
go test -race ./...
# 2. Format code
goimports -w .
# 3. Run vet
go vet ./...
# 4. Build successfully
go build -o openframe .
# 5. Test your changes manually
./openframe --help
PR Description Template
## Summary
<!-- What does this PR do? -->
## Changes
<!-- List the key changes made -->
-
-
## Testing
<!-- How was this tested? -->
- [ ] Unit tests added/updated
- [ ] Integration tests added/updated (if applicable)
- [ ] Manual testing performed
## Checklist
- [ ] Code follows the style guidelines
- [ ] Self-review completed
- [ ] Tests pass (`go test -race ./...`)
- [ ] `go vet ./...` passes
- [ ] `goimports` formatting applied
- [ ] No secrets or credentials in code
- [ ] Security guidelines followed (secrets registered with redact, no shell injection)
PR Size Guidelines
| Size |
Lines Changed |
Guidance |
| Small |
< 100 lines |
Preferred — fast review |
| Medium |
100–500 lines |
Include detailed description |
| Large |
500+ lines |
Split into smaller PRs if possible |
Review Checklist
When reviewing a PR, check:
Correctness:
Security:
Architecture:
Tests:
Documentation:
Adding a New Command
- Create the command file in
cmd/<group>/<command>.go.
- Define a
get<Name>Cmd() function returning *cobra.Command.
- Register it in the parent command group (e.g.,
cmd/cluster/cluster.go).
- Create a service in
internal/<group>/ with injected dependencies.
- Write unit tests using
testutil.TestClusterCommand.
- Add integration tests if the command interacts with external systems.
- Verify
--help output is accurate and descriptive.
Adding a New Provider
To add a new cluster provider (e.g., Kind):
- Implement the
Provider interface in internal/cluster/providers/<name>/manager.go.
- Add prerequisite definitions in
internal/cluster/prerequisites/.
- Register the provider in the cluster service provider resolution.
- Add the cluster type to
internal/cluster/models/cluster.go.
- Write unit and integration tests.
Security-Sensitive Changes
If your change touches credentials, downloads, or self-update code:
- New secrets/credentials must be registered with
redact.RegisterSecret() before any logging path can reach them.
- New external tool installers must use the verified-download path, not a raw shell pipe.
- New shelled-out commands must go through
CommandExecutor with argv slices, not interpolated shell strings.
- New user-supplied identifiers (names, refs, paths) must be validated before being passed to any external command.
See the Security documentation for the full set of practices.
Testing
Run all unit tests:
go test ./...
Run integration tests (these build and execute the real binary, and may skip themselves if required tools like Docker/k3d aren't present):
go test ./tests/integration/...
See the Testing documentation for the full testing strategy, including mocked unit tests and standardized per-command test coverage via TestClusterCommand.
Community
All contribution discussions happen in the OpenMSP Slack. There are no GitHub Issues or Discussions for this project — bring your questions, feature ideas, and bug reports to Slack.