Thank you for your interest in contributing to newrelic-cli! This guide will help you get started.
- Go 1.21 or later
- Make
- golangci-lint (optional, for linting)
# Clone the repository
git clone https://github.com/open-cli-collective/newrelic-cli.git
cd newrelic-cli
# Build
make build
# Run tests
make test
# Install locally
make install./nrq --versionAll Go code must be formatted with gofmt. Run before committing:
make fmtWe use golangci-lint for static analysis:
make lint- Packages: lowercase, short, no underscores (
apps,alerts,configcmd) - Files: lowercase with underscores (
log_rules.go,client_test.go) - Types: PascalCase (
AlertPolicy,ClientConfig) - Functions: PascalCase for exported, camelCase for internal
- Variables: camelCase (
apiKey,accountID) - Constants: PascalCase for exported (
RegionUS), camelCase for internal
internal/cmd/<resource>/
├── <resource>.go # Register function, parent command
├── list.go # list subcommand
├── get.go # get subcommand
└── <resource>_test.go # Tests
We follow Conventional Commits:
type(scope): description
[optional body]
[optional footer]
| Type | Description |
|---|---|
feat |
New feature |
fix |
Bug fix |
docs |
Documentation only |
refactor |
Code change that neither fixes nor adds |
test |
Adding or correcting tests |
chore |
Maintenance tasks |
Use the affected area: apps, alerts, api, config, view, readme, etc.
feat(alerts): add conditions list command
fix(nrql): handle empty result sets gracefully
docs(readme): add scripting examples section
refactor(api): extract dashboard methods to separate file
test(apps): add unit tests for metrics command
chore(deps): update cobra to v1.8.0PR titles must use conventional commit format because the repository uses squash merges with PR_TITLE as the commit message. The Auto Release workflow checks the squash commit message for a feat: or fix: prefix to decide whether to create a release.
| PR Title | Triggers Release? |
|---|---|
feat(keys): add API key commands |
Yes |
fix(nrql): handle empty results |
Yes |
docs: update README |
No |
Add API key commands |
No (missing prefix!) |
If your PR changes Go code and should trigger a release, the title must start with feat:, feat(scope):, fix:, or fix(scope):.
git checkout main
git pull origin main
git checkout -b type/descriptionBranch naming:
feat/add-alerts-conditionsfix/nrql-timeoutdocs/update-readmerefactor/extract-api
- Write code following the style guidelines
- Add tests for new functionality
- Update documentation if needed
# Format, lint, and test
make verifygit add .
git commit -m "type(scope): description"
git push -u origin your-branch- Target the
mainbranch - Fill out the PR template
- Request review from maintainers
newrelic-cli/
├── cmd/nrq/ # Entry point
│ └── main.go
├── api/ # Public Go library
│ ├── client.go # HTTP client, New(), NewWithConfig()
│ ├── types.go # Data types
│ ├── errors.go # Error types
│ └── *.go # Domain-specific methods
├── internal/
│ ├── cmd/ # Cobra commands
│ │ ├── root/ # Root command, global options
│ │ ├── apps/ # apps commands
│ │ ├── alerts/ # alerts commands
│ │ └── ...
│ ├── config/ # Credential storage
│ ├── version/ # Version info
│ └── view/ # Output formatting
├── docs/ # Additional documentation
├── Makefile # Build targets
├── go.mod # Module definition
└── go.sum # Dependency checksums
Create a new directory in internal/cmd/:
mkdir internal/cmd/newcommandinternal/cmd/newcommand/newcommand.go:
package newcommand
import (
"github.com/spf13/cobra"
"github.com/open-cli-collective/newrelic-cli/internal/cmd/root"
)
func Register(rootCmd *cobra.Command, opts *root.Options) {
cmd := &cobra.Command{
Use: "newcommand",
Short: "Description of the command",
}
cmd.AddCommand(newListCmd(opts))
rootCmd.AddCommand(cmd)
}internal/cmd/newcommand/list.go:
package newcommand
import (
"github.com/spf13/cobra"
"github.com/open-cli-collective/newrelic-cli/api"
"github.com/open-cli-collective/newrelic-cli/internal/cmd/root"
)
func newListCmd(opts *root.Options) *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List items",
RunE: func(cmd *cobra.Command, args []string) error {
return runList(opts)
},
}
}
func runList(opts *root.Options) error {
client, err := api.New()
if err != nil {
return err
}
// Call API
items, err := client.ListItems()
if err != nil {
return err
}
// Format output
v := opts.View()
headers := []string{"ID", "NAME"}
rows := make([][]string, len(items))
for i, item := range items {
rows[i] = []string{item.ID, item.Name}
}
return v.Render(headers, rows, items)
}cmd/nrq/main.go:
import (
// ...
"github.com/open-cli-collective/newrelic-cli/internal/cmd/newcommand"
)
func main() {
root.RegisterCommands(
// ...existing...
newcommand.Register,
)
// ...
}api/newcommand.go:
package api
func (c *Client) ListItems() ([]Item, error) {
// Implementation
}api/types.go:
type Item struct {
ID string `json:"id"`
Name string `json:"name"`
}internal/cmd/newcommand/newcommand_test.go:
package newcommand
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestListCommand(t *testing.T) {
// Test implementation
}# All tests
make test
# With coverage
make test-cover
# Specific package
go test ./internal/cmd/apps/...Use table-driven tests with testify/assert:
func TestFunction(t *testing.T) {
tests := []struct {
name string
input string
expected string
wantErr bool
}{
{
name: "valid input",
input: "test",
expected: "result",
wantErr: false,
},
{
name: "empty input",
input: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Function(tt.input)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.expected, result)
})
}
}If you have questions about contributing, please open an issue or reach out to the maintainers.