Thanks for your interest in contributing to gplay (Google Play Console CLI).
Requirements:
- Go 1.21+
Clone and build:
git clone https://github.com/tamtom/play-console-cli.git
cd play-console-cli
make buildRun tests:
go test ./...Optional tooling:
make tools # installs gofumpt + golangci-lint
make lint # uses golangci-lint if installed, else go vet
make format # go fmt + gofumpt
make dev # format + lint + test + buildIntegration tests hit the real Google Play API and are skipped by default. Set credentials in your environment and run:
export GPLAY_SERVICE_ACCOUNT="/path/to/service-account.json"
export GPLAY_PACKAGE="com.example.app"
make test-integrationIf you have Google Play Console API credentials, you can run real API calls locally:
export GPLAY_SERVICE_ACCOUNT="/path/to/service-account.json"
export GPLAY_PACKAGE="com.example.app"
gplay tracks list --package "$GPLAY_PACKAGE"
gplay reviews list --package "$GPLAY_PACKAGE"Credentials are stored in config with file path reference only. Do not commit service account JSON files.
play-console-cli/
├── cmd/ # Main entry point
├── internal/
│ ├── cli/ # Command implementations
│ │ ├── registry/ # Command registration
│ │ ├── shared/ # Shared utilities
│ │ └── */ # Individual command packages
│ ├── output/ # Output formatting
│ ├── playclient/ # Google Play API client
│ ├── update/ # Self-update functionality
│ └── version/ # Version information
├── .github/workflows/ # CI/CD workflows
├── Makefile # Build automation
└── install.sh # Installation script
-
Create a new package under
internal/cli/:mkdir internal/cli/yourcommand
-
Follow the existing pattern (see other command files for reference):
package yourcommand import ( "context" "flag" "github.com/peterbourgon/ff/v3/ffcli" "github.com/tamtom/play-console-cli/internal/cli/shared" "github.com/tamtom/play-console-cli/internal/playclient" ) func YourCommand() *ffcli.Command { fs := flag.NewFlagSet("yourcommand", flag.ExitOnError) return &ffcli.Command{ Name: "yourcommand", ShortUsage: "gplay yourcommand <subcommand> [flags]", ShortHelp: "Brief description.", FlagSet: fs, UsageFunc: shared.DefaultUsageFunc, Subcommands: []*ffcli.Command{ // Add subcommands here }, Exec: func(ctx context.Context, args []string) error { return flag.ErrHelp }, } }
-
Register the command in
internal/cli/registry/registry.go:import "github.com/tamtom/play-console-cli/internal/cli/yourcommand" // In Subcommands(): yourcommand.YourCommand(),
- Register new commands in
internal/cli/registry/registry.go - Always set
UsageFunc: shared.DefaultUsageFuncfor command groups and subcommands - For outbound HTTP, use
shared.ContextWithTimeoutsoGPLAY_TIMEOUTapplies - Validate required flags and return clear error messages
- Use
shared.PrintOutput()for consistent output formatting
Flag handling:
packageName := fs.String("package", "", "Package name (applicationId)")
outputFlag := fs.String("output", "json", "Output format: json, table, markdown")
pretty := fs.Bool("pretty", false, "Pretty-print JSON output")Package name resolution:
pkg := shared.ResolvePackageName(*packageName, service.Cfg)
if strings.TrimSpace(pkg) == "" {
return fmt.Errorf("--package is required")
}Context with timeout:
ctx, cancel := shared.ContextWithTimeout(ctx, service.Cfg)
defer cancel()Output formatting:
return shared.PrintOutput(result, *outputFlag, *pretty)- Keep PRs small and focused
- Add or update tests for new behavior
- Update
README.mdif behavior or scope changes - Avoid committing any credentials or service account files
- Run
make devbefore submitting
If you find a security issue, please report it responsibly by opening a private issue or contacting the maintainer directly.
By contributing, you agree that your contributions will be licensed under the MIT License.