Thank you for your interest in contributing to OpenFrame CLI! This document covers everything you need to know to submit high-quality contributions.
- Join the OpenMSP Slack community to discuss your ideas before starting large features
- Read the Architecture Overview to understand the codebase
- Set up your development environment and verify you can build and run locally
Note: 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.
Slack invite: https://join.slack.com/t/openmsp/shared_invite/zt-36bl7mx0h-3~U2nFH6nqHqoTPXMaHEHA
| Tool | Version | Purpose |
|---|---|---|
| Go | 1.21+ | Primary language runtime |
| Git | 2.30+ | Version control |
| Docker | 24.x+ | Container runtime (integration tests) |
| k3d | 5.x+ | Local Kubernetes clusters (integration tests) |
| Helm | 3.x+ | Kubernetes package manager (integration tests) |
# Clone the repository
git clone https://github.com/flamingo-stack/openframe-cli.git
cd openframe-cli
# Download dependencies
go mod download
# Build the binary
go build -o openframe .
# Verify
./openframe --version# Install goimports (formatting + import management)
go install golang.org/x/tools/cmd/goimports@latest
# Install golangci-lint
curl -sSfL https://raw.githubusercontent.com/golangci-lint/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
# Format code
goimports -w .
# Run vet
go vet ./...
# Run linter
golangci-lint runOpenFrame CLI follows standard Go conventions:
gofmt/goimportsformatting is required — no unformatted code will be mergedgo vetmust pass with no warnings- Follow Effective Go and the Go Code Review Comments
| 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 |
Wrap errors with context and use structured types from shared/errors:
// 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
}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
},
}
cmd.Flags().StringVar(&flagVar, "flag-name", "default", "Flag description")
return cmd
}- Services must accept interfaces (not concrete types) for all dependencies
- Always accept
context.Contextas the first argument for cancellable operations - Return descriptive errors, not boolean success flags
- Use the
CommandExecutorinterface for all external binary invocations — neveros/execdirectly
Any credential read from environment variables, config files, or user prompts must be registered with the redact package before use:
import "github.com/flamingo-stack/openframe-cli/internal/shared/redact"
redact.RegisterSecret(githubToken)
redact.RegisterSecret(registryPassword)Always use argv arrays via CommandExecutor, never shell string concatenation:
// SAFE: argv array
result, err := exec.Execute(ctx, "k3d", "cluster", "list", "--output", "json")
// NEVER: shell injection risk
// exec.Execute(ctx, "sh", "-c", "k3d cluster list --output " + userInput)- User-supplied cluster names are validated via
ValidateClusterName - Any new credential/token is registered with
redact.RegisterSecret() - External commands use argv arrays, not shell strings
- New YAML/JSON input is validated via structured types before use
- Sensitive flags are not printed in error messages
| 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: Lowercase and hyphens only. Branch from main unless working on a specific release branch.
OpenFrame CLI uses Conventional Commits:
<type>(<scope>): <short description>
[optional body]
[optional footer(s)]
| 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 |
feat(cluster): add --wait flag to cluster create command
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
# Run all unit tests with race detector (recommended)
go test -race ./...
# Run tests with coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# Run integration tests (requires Docker, k3d, Helm, and 24GB+ RAM)
go test ./tests/integration/... -v -timeout 30mUse the MockCommandExecutor for isolated unit tests — never invoke real subprocesses:
func TestCreateCluster(t *testing.T) {
testutil.InitializeTestMode()
mock := testutil.NewTestMockExecutor()
mock.SetResponse("k3d cluster create", &executor.CommandResult{
ExitCode: 0,
Stdout: `{"name": "test-cluster"}`,
})
svc := cluster.NewClusterService(mock)
err := svc.CreateCluster(context.Background(), "test-cluster")
assert.NoError(t, err)
}| Package Type | Target |
|---|---|
Core services (internal/) |
≥ 80% |
Command layer (cmd/) |
≥ 70% |
| Provider implementations | ≥ 75% |
| Shared utilities | ≥ 85% |
# 1. Run all tests
go test -race ./...
# 2. Format code
goimports -w .
# 3. Run vet
go vet ./...
# 4. Build successfully
go build -o openframe .## 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
| 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 |
Follow these steps when adding a new CLI 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
--helpoutput is accurate and descriptive
To add a new cluster provider (e.g., Kind):
- Implement the
Providerinterface ininternal/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
When reviewing a PR, check:
Correctness:
- Logic is correct and handles edge cases
- Error paths are handled and tested
- Context cancellation is propagated correctly
Security:
- No secrets hardcoded or logged
- New credentials registered with
redact.RegisterSecret() - User input validated before reaching shell-outs
- External commands use argv arrays, not shell strings
Architecture:
- Business logic is in the service layer, not command layer
- Dependencies are injected via interfaces (testable)
- New command follows the established Cobra pattern
Tests:
- Unit tests cover new code paths
- Error cases are tested
- Mock executor used instead of real subprocess calls in unit tests
Documentation:
- Public API has Go doc comments
- Complex logic has inline comments
-
--helptext is accurate and helpful
Release binaries are code-signed automatically during the release workflow:
| Platform | Mechanism |
|---|---|
| macOS | codesign (Developer ID Application, hardened runtime) + notarytool notarization |
| Windows | Authenticode via Azure Trusted Signing |
| Linux | Integrity via checksums.txt + cosign bundle |
All release binaries can be verified using cosign:
cosign verify-blob --bundle checksums.txt.bundle \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/flamingo-stack/openframe-cli/\.github/workflows/release\.yml@.*$' \
checksums.txt- OpenMSP Slack: https://www.openmsp.ai/
- Slack invite: https://join.slack.com/t/openmsp/shared_invite/zt-36bl7mx0h-3~U2nFH6nqHqoTPXMaHEHA
- OpenFrame platform repo: https://github.com/flamingo-stack/openframe-oss-tenant
- Releases: https://github.com/flamingo-stack/openframe-cli/releases