# Contributing to notion-cli Thank you for your interest in contributing to notion-cli! This document provides guidelines and instructions for contributing to the project. ## Table of Contents - [Code of Conduct](#code-of-conduct) - [Getting Started](#getting-started) - [Development Setup](#development-setup) - [Code Style Guidelines](#code-style-guidelines) - [Testing Requirements](#testing-requirements) - [Pull Request Process](#pull-request-process) - [Commit Message Format](#commit-message-format) - [Project Structure](#project-structure) - [Reporting Issues](#reporting-issues) ## Code of Conduct This project follows a simple code of conduct: be respectful, constructive, and collaborative. We welcome contributions from everyone. ## Getting Started 1. **Fork the repository** on GitHub 2. **Clone your fork** locally: ```bash git clone https://github.com/YOUR_USERNAME/notion-cli.git cd notion-cli ``` 3. **Add upstream remote**: ```bash git remote add upstream https://github.com/Coastal-Programs/notion-cli.git ``` 4. **Create a branch** for your changes: ```bash git checkout -b feature/your-feature-name ``` ## Development Setup ### Prerequisites - Go 1.26 or later - Make - Git - golangci-lint (optional, for extended linting) ### Installation ```bash # Download Go module dependencies go mod download # Build the binary make build # Run the test suite make test ``` The built binary is placed at `build/notion-cli`. You can also install it directly into your `$GOPATH/bin`: ```bash make install ``` ### Environment Setup Set the Notion API token as an environment variable: ```bash export NOTION_TOKEN="secret_your_token_here" ``` Get your token from: https://www.notion.so/my-integrations ### OAuth Credentials (maintainers only) The `auth login` command uses Notion's OAuth flow, which requires a public Client ID and Client Secret. CI release builds embed these into the binary via `-ldflags -X` from GitHub Actions secrets (`NOTION_OAUTH_CLIENT_ID` / `NOTION_OAUTH_SECRET`). Local dev builds can either embed them at build time or read them from runtime environment variables; builds without usable credentials return `OAuthNotConfigured` if you try `auth login` (everything else still works with `NOTION_TOKEN`). If you are a maintainer and want OAuth in a local `make build`, store the credentials at `~/.config/notion-cli-dev/.env` — the Makefile auto-loads it. This path lives **outside the repo tree** so a stray `git add .` cannot stage it and a typo in `.gitignore` cannot expose it: ```bash mkdir -p ~/.config/notion-cli-dev cat > ~/.config/notion-cli-dev/.env <<'EOF' NOTION_OAUTH_CLIENT_ID= NOTION_OAUTH_SECRET= EOF chmod 600 ~/.config/notion-cli-dev/.env ``` Then `make build` and verify with `./build/notion-cli doctor --json` — the `OAuth Credentials` check should pass. > **Migrating from `.env.local`?** Earlier versions read these vars from > `.env.local` in the repo root. Move the contents to the path above and > shred the old file: `shred -u .env.local` (or `rm -P .env.local` on macOS). Security rules: - **Never** commit the dotfile, paste it in issues, or share it in screenshots / screen recordings. - Treat the "secret" as a soft secret — anyone with a release binary can extract it via `strings` (this is accepted in OAuth native-app distribution; PKCE mitigates the risk). - If a credential leaks, follow the rotation procedure in `SECURITY.md` and cut a patch release. - CI logs auto-redact GitHub Actions secrets; locally, `make build` is silenced so the ldflags don't echo to your terminal. ### Pre-commit hooks (Lefthook) We use [Lefthook](https://lefthook.dev) to run `gofmt` and `go vet` against staged Go files before each commit, so formatting drift never reaches CI. Hooks are opt-in per contributor. Install the Lefthook binary once: ```bash # macOS (recommended) brew install lefthook # Or via Go (any platform) go install github.com/evilmartians/lefthook@latest ``` Then, from the repo root, wire up the git hooks: ```bash lefthook install ``` That writes the hook scripts into `.git/hooks/`. Config lives in `lefthook.yml` (committed); per-contributor overrides go in `lefthook-local.yml` (gitignored). ## Code Style Guidelines ### Go Conventions - Follow standard Go conventions as described in [Effective Go](https://go.dev/doc/effective_go) - All code must be formatted with `gofmt` (run `make fmt`) - All code must pass `go vet` and `golangci-lint` (run `make lint`) - Keep functions focused and short - Return errors rather than panicking - Use `context.Context` for all API calls ### Code Patterns - All commands use Cobra; register via `Register*Commands(root *cobra.Command)` - Use `pkg/output.Printer` for all output, never `fmt.Println` directly - Use `internal/errors.NotionCLIError` for errors, never raw errors - Use envelope format for JSON output: `{success, data, metadata}` - Use `internal/resolver.ExtractID()` for all ID/URL inputs **Example:** ```go // 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) } ``` ### Naming Conventions - **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`) ### Documentation All exported functions, types, and packages must have GoDoc comments. Comments should start with the name of the thing being documented: ```go // 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 } ``` ## Testing Requirements ### Running Tests ```bash # 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 ``` ### Test Coverage - All new features must include tests - Aim for 80%+ code coverage - Test both success and error cases - Use `net/http/httptest` for mocking HTTP API calls ### Test Structure Tests use Go's built-in `testing` package. Test files live alongside the code they test with a `_test.go` suffix: ```go 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) } } ``` ### Test Guidelines 1. **Mock external dependencies** - Use `net/http/httptest` for HTTP calls, never make real API calls 2. **Use descriptive test names** - `TestSetAndGet`, `TestNewCacheInvalidSize`, `TestRetryOnRateLimit` 3. **Use table-driven tests** where appropriate for testing multiple inputs 4. **Test edge cases** - Empty inputs, nil values, zero values, boundary conditions 5. **Keep tests isolated** - No shared mutable state between tests 6. **Use `t.Helper()`** in test helper functions for better error reporting 7. **Use `t.Parallel()`** where safe to speed up the test suite ## Pull Request Process ### Before Submitting 1. **Update from upstream**: ```bash git fetch upstream git rebase upstream/main ``` 2. **Run all checks**: ```bash make build make test make lint ``` 3. **Update documentation** if needed: - Update README.md for new features - Add CHANGELOG.md entry - Update GoDoc comments ### Submitting 1. **Push to your fork**: ```bash git push origin feature/your-feature-name ``` 2. **Create Pull Request** on GitHub with: - Clear title describing the change - Detailed description of what changed and why - Reference any related issues (`Fixes #123`) 3. **Fill out PR template** completely ### PR Review Process - 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 ### PR Checklist - [ ] 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`) ## Commit Message Format We follow **Conventional Commits** specification: ``` ():