Thank you for your interest in contributing to notion-cli! This document provides guidelines and instructions for contributing to the project.
- Code of Conduct
- Getting Started
- Development Setup
- Code Style Guidelines
- Testing Requirements
- Pull Request Process
- Commit Message Format
- Project Structure
- Reporting Issues
This project follows a simple code of conduct: be respectful, constructive, and collaborative. We welcome contributions from everyone.
- Fork the repository on GitHub
- Clone your fork locally:
git clone https://github.com/YOUR_USERNAME/notion-cli.git cd notion-cli - Add upstream remote:
git remote add upstream https://github.com/Coastal-Programs/notion-cli.git
- Create a branch for your changes:
git checkout -b feature/your-feature-name
- Go 1.21 or later
- Make
- Git
- golangci-lint (optional, for extended linting)
# Download Go module dependencies
go mod download
# Build the binary
make build
# Run the test suite
make testThe built binary is placed at build/notion-cli. You can also install it directly into your $GOPATH/bin:
make installSet the Notion API token as an environment variable:
export NOTION_TOKEN="secret_your_token_here"Get your token from: https://www.notion.so/my-integrations
- Follow standard Go conventions as described in Effective Go
- All code must be formatted with
gofmt(runmake fmt) - All code must pass
go vetandgolangci-lint(runmake lint) - Keep functions focused and short
- Return errors rather than panicking
- Use
context.Contextfor all API calls
- All commands use Cobra; register via
Register*Commands(root *cobra.Command) - Use
pkg/output.Printerfor all output, neverfmt.Printlndirectly - Use
internal/errors.NotionCLIErrorfor errors, never raw errors - Use envelope format for JSON output:
{success, data, metadata} - Use
internal/resolver.ExtractID()for all ID/URL inputs
Example:
// RegisterPageCommands adds all page subcommands to the root command.
func RegisterPageCommands(root *cobra.Command) {
pageCmd := &cobra.Command{
Use: "page",
Short: "Page operations",
}
pageCmd.AddCommand(newPageCreateCmd())
pageCmd.AddCommand(newPageRetrieveCmd())
root.AddCommand(pageCmd)
}- Files: snake_case (
cache_cmd.go,workspace.go) - Exported functions/types: PascalCase (
NewCache,NotionCLIError) - Unexported functions/types: camelCase (
doRequest,parseResponse) - Constants: PascalCase for exported, camelCase for unexported (
DefaultTimeout,maxRetries) - Acronyms: ALL_CAPS within identifiers (
ExtractID,ParseJSON,HTTPClient) - Packages: lowercase, single word when possible (
cache,retry,errors)
All exported functions, types, and packages must have GoDoc comments. Comments should start with the name of the thing being documented:
// NotionCLIError represents a structured error with an error code,
// user-facing message, and optional suggestions for resolution.
type NotionCLIError struct {
Code string
Message string
Suggestions []string
}
// NewCache creates a new in-memory TTL cache with the given maximum
// number of entries. If maxSize is zero or negative, a default of
// 1000 is used.
func NewCache(maxSize int) *Cache {
// Implementation
}
// ExtractID parses a Notion URL or raw ID string and returns
// the normalized UUID. It returns an error if the input cannot
// be resolved to a valid Notion ID.
func ExtractID(input string) (string, error) {
// Implementation
}# Run all tests
make test
# Run tests for a specific package
go test ./internal/cache/... -v
# Run a specific test function
go test ./internal/cache/... -run TestSetAndGet -v
# Run tests with race detection
go test -race ./...
# Run tests with coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out- All new features must include tests
- Aim for 80%+ code coverage
- Test both success and error cases
- Use
net/http/httptestfor mocking HTTP API calls
Tests use Go's built-in testing package. Test files live alongside the code they test with a _test.go suffix:
package cache
import (
"testing"
"time"
)
func TestNewCache(t *testing.T) {
c := NewCache(100)
defer c.Stop()
if c.Size() != 0 {
t.Errorf("expected empty cache, got size %d", c.Size())
}
}
func TestSetAndGet(t *testing.T) {
c := NewCache(100)
defer c.Stop()
c.Set("key1", "value1", 1*time.Minute)
val, ok := c.Get("key1")
if !ok {
t.Fatal("expected key1 to exist")
}
if val != "value1" {
t.Errorf("expected value1, got %v", val)
}
}- Mock external dependencies - Use
net/http/httptestfor HTTP calls, never make real API calls - Use descriptive test names -
TestSetAndGet,TestNewCacheInvalidSize,TestRetryOnRateLimit - Use table-driven tests where appropriate for testing multiple inputs
- Test edge cases - Empty inputs, nil values, zero values, boundary conditions
- Keep tests isolated - No shared mutable state between tests
- Use
t.Helper()in test helper functions for better error reporting - Use
t.Parallel()where safe to speed up the test suite
-
Update from upstream:
git fetch upstream git rebase upstream/main
-
Run all checks:
make build make test make lint -
Update documentation if needed:
- Update README.md for new features
- Add CHANGELOG.md entry
- Update GoDoc comments
-
Push to your fork:
git push origin feature/your-feature-name
-
Create Pull Request on GitHub with:
- Clear title describing the change
- Detailed description of what changed and why
- Reference any related issues (
Fixes #123)
-
Fill out PR template completely
- Maintainers will review within 1-2 weeks
- Address review feedback promptly
- Keep PRs focused on a single feature/fix
- Be open to suggestions and changes
- Code follows Go style guidelines
- All tests pass (
make test) - New tests added for new features
- Lint passes (
make lint) - Code is formatted (
make fmt) - Documentation updated
- CHANGELOG.md updated
- Commit messages follow conventional format
- No merge conflicts
- Build succeeds (
make build)
We follow Conventional Commits specification:
<type>(<scope>): <subject>
<body>
<footer>
feat: New featurefix: Bug fixdocs: Documentation changesstyle: Code style changes (formatting, etc.)refactor: Code refactoringtest: Adding or updating testschore: Maintenance tasks
feat(db): add schema discovery command
Implement new 'db schema' command to extract database schemas
in AI-parseable format. Supports JSON and table output.
Closes #42
fix(cache): resolve race condition in cache invalidation
Fixed issue where concurrent writes could corrupt cache state.
Added mutex lock for write operations.
Fixes #56
refactor(notion): simplify HTTP client retry logic
Consolidate retry and backoff into a single configurable function
to reduce code duplication across request methods.
- Use present tense ("add feature" not "added feature")
- Use imperative mood ("move cursor to..." not "moves cursor to...")
- Keep subject line under 72 characters
- Reference issues and PRs in footer
- Explain "what" and "why", not "how"
notion-cli/
├── cmd/
│ └── notion-cli/
│ └── main.go # Entry point
├── internal/
│ ├── cli/
│ │ ├── root.go # Cobra root command + global flags
│ │ └── commands/
│ │ ├── db.go # db query, retrieve, create, update, schema
│ │ ├── page.go # page create, retrieve, update, property_item
│ │ ├── block.go # block append, retrieve, delete, update, children
│ │ ├── user.go # user list, retrieve, bot
│ │ ├── search.go # search command
│ │ ├── sync.go # workspace sync
│ │ ├── list.go # list cached databases
│ │ ├── batch.go # batch retrieve
│ │ ├── whoami.go # connectivity check
│ │ ├── doctor.go # health checks
│ │ ├── config.go # config get/set/path/list
│ │ └── cache_cmd.go # cache info/stats
│ ├── notion/
│ │ └── client.go # HTTP client, auth, request/response
│ ├── cache/
│ │ ├── cache.go # In-memory TTL cache
│ │ └── workspace.go # Workspace database cache
│ ├── retry/
│ │ └── retry.go # Exponential backoff with jitter
│ ├── errors/
│ │ └── errors.go # NotionCLIError with codes, suggestions
│ ├── config/
│ │ └── config.go # Config loading (env vars + JSON file)
│ └── resolver/
│ └── resolver.go # URL/ID/name resolution
├── pkg/
│ └── output/
│ ├── output.go # JSON/text/table/CSV formatting
│ ├── envelope.go # Envelope wrapper
│ └── table.go # Table formatter
├── docs/ # Documentation
├── go.mod # Go module definition
├── go.sum # Dependency checksums
└── Makefile # Build, test, lint, release targets
- cmd/ - Application entry points. Each subdirectory is a separate binary.
- internal/ - Private application code. Cannot be imported by other modules.
- pkg/ - Public library code. Can be imported by external projects.
- docs/ - User-facing documentation and guides.
Include:
- Clear description of the issue
- Steps to reproduce
- Expected vs actual behavior
- Environment (Go version, OS, architecture)
- Error messages and stack traces
- Minimal reproduction example
Include:
- Clear description of the feature
- Use case and motivation
- Example usage
- Potential implementation approach
Do not open public issues for security vulnerabilities.
See SECURITY.md for reporting instructions.
Use the --verbose flag to enable debug output:
./build/notion-cli db query <id> --verbose# Build and test the binary
make build
./build/notion-cli --version
./build/notion-cli whoami
# Or install to $GOPATH/bin
make install
notion-cli db query <id>This project uses the Cobra CLI framework:
- Commands are created with
&cobra.Command{} - Flags are registered with
cmd.Flags()(local) orcmd.PersistentFlags()(inherited) - Use
RunE(notRun) so commands can return errors - Register subcommands via
Register*Commands(root *cobra.Command)functions
# Build the binary
make build
# Run the full test suite
make test
# Run linters (go vet + golangci-lint)
make lint
# Format all Go code
make fmt
# Tidy module dependencies
make tidy
# Cross-compile for all platforms
make release
# Clean build artifacts
make clean
# Run a specific test with verbose output
go test ./internal/retry/... -run TestExponentialBackoff -v
# Check test coverage for a package
go test -coverprofile=coverage.out ./internal/cache/...
go tool cover -func=coverage.out
# Clear local config/cache during development
rm -rf ~/.config/notion-cli/- Create a new file in
internal/cli/commands/(e.g.,newcmd.go) - Define the command using
&cobra.Command{} - Create a
RegisterNewCmdCommands(root *cobra.Command)function - Call the register function from
internal/cli/root.go - Add tests in a corresponding
_test.gofile - Use
pkg/output.Printerfor all output - Use
internal/errors.NotionCLIErrorfor error handling
- Check existing issues and PRs first
- Open a discussion on GitHub Discussions
- Review documentation in
/docsfolder
By contributing, you agree that your contributions will be licensed under the MIT License.
Thank you for contributing to notion-cli!