# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. **gh-action-readme** - CLI tool for GitHub Actions documentation generation ## โš ๏ธ Code Quality Anti-Patterns - DO NOT REPEAT **CRITICAL:** The following patterns have caused quality issues in the past. These mistakes must not be repeated: ### ๐Ÿšซ High Cognitive Complexity #### Never write functions with cognitive complexity > 15 **Bad - Repeated Mistakes:** - Nested conditionals in test assertions - Complex error checking logic duplicated across tests - Deep nesting in validation functions **Always:** - Extract complex logic into helper functions - Create test helper functions for repeated assertion patterns - Keep functions focused on a single responsibility - Break down complex conditions into smaller, testable pieces **Example:** Instead of 19 lines of nested error checking, create a helper: ```go // โŒ BAD - High complexity func TestValidation(t *testing.T) { if result.HasErrors { found := false for _, err := range result.Errors { if strings.Contains(err.Message, expected) { found = true break } } if !found { t.Errorf("error not found") } } else { // more nesting... } } // โœ… GOOD - Use helper func TestValidation(t *testing.T) { assertValidationError(t, result, "field", true, "expected message") } ``` ### ๐Ÿšซ Duplicate String Literals #### Never repeat string literals across test files **Bad - Repeated Mistakes:** - File paths like `"/tmp/action.yml"` repeated 22 times - Action references like `"actions/checkout@v3"` duplicated - Error messages and test scenarios hardcoded everywhere **Always:** - Use constants from `appconstants/` for production strings - Use constants from `testutil/test_constants.go` for test-only strings - Add new constants when you see duplication (>2 uses) **Red Flag Patterns:** - Same string literal in multiple test files - Same file path repeated in different tests - Same error message in multiple assertions ### ๐Ÿšซ Inline YAML and Config Data in Tests #### Never embed YAML or config data directly in test code **Bad - Repeated Mistakes:** - Inline YAML strings with backticks in test functions - Config data hardcoded in test setup - Template content embedded in test files **Always:** - Create fixture files in `testdata/yaml-fixtures/` - Use `testutil.MustReadFixture()` to load fixtures - Add constants to `testutil/test_constants.go` for fixture paths - Reuse fixtures across multiple tests **Example:** ```go // โŒ BAD - Inline YAML testConfig := ` theme: default output_format: md ` // โœ… GOOD - Use fixture testConfig := string(testutil.MustReadFixture(testutil.TestConfigDefault)) ``` **Fixture Organization:** - `testdata/yaml-fixtures/configs/` - Config files - `testdata/yaml-fixtures/actions/` - Action files - `testdata/yaml-fixtures/template-fixtures/` - Template files ### ๐Ÿšซ Co-Authored-By Lines in Commits #### Never add Co-Authored-By or similar bylines to commit messages **Bad - Repeated Mistakes:** - Adding `Co-Authored-By: Claude Sonnet 4.5 ` to commits - Including attribution lines at end of commit messages - Adding signature or generated-by lines **Always:** - Write clean commit messages following conventional commits format - Omit any co-author, attribution, or signature lines - Focus commit message on what changed and why **Example:** ```text โŒ BAD: refactor: move inline YAML to fixtures Benefits: - Improved maintainability - Better separation Co-Authored-By: Claude Sonnet 4.5 โœ… GOOD: refactor: move inline YAML to fixtures for better test maintainability - Created 16 new config fixtures - Replaced 19 inline YAML instances - All tests passing with no regressions ``` **When user says "no bylines":** - This means: Remove ALL attribution/co-author lines - Do NOT argue or explain why they might be useful - Just comply immediately and recommit without bylines ### โœ… Prevention Mechanisms **Before writing ANY code:** 1. Check `testutil/test_constants.go` for existing constants 2. Check `testdata/yaml-fixtures/` for existing fixtures 3. Consider if your function will exceed complexity limits 4. Plan helper functions for complex logic upfront **Before committing:** 1. Run `make lint` - catches complexity and duplication 2. Pre-commit hooks will catch most issues 3. SonarCloud will flag remaining issues in PR **Remember:** It's easier to write clean code initially than to refactor after quality issues are raised. ## ๐Ÿ›ก๏ธ Quality Standards This project enforces strict quality gates aligned with [SonarCloud "Sonar way"](https://docs.sonarsource.com/sonarqube-cloud/standards/managing-quality-gates/): | Metric | Threshold | Check Command | | ------ | --------- | ------------- | | Code Coverage | โ‰ฅ 72% (overall); 80% target | `make test-coverage-check` | | Duplicated Lines | โ‰ค 3% (new code) | `make lint` (via dupl) | | Security Rating | A (no issues) | `make security` | | Reliability Rating | A (no bugs) | `make lint` | | Maintainability | A (tech debt โ‰ค 5%) | `make lint` | | Cyclomatic Complexity | โ‰ค 10 per function | `make lint` (via gocyclo) | | Line Length | โ‰ค 120 characters | `make lint` (via lll) | **Current Coverage:** 72.8% overall (target: 80%) **Coverage Threshold:** Set in `Makefile` as `COVERAGE_THRESHOLD := 72.0` **Pre-commit Quality Checks:** - All linters run automatically via pre-commit hooks - EditorConfig compliance enforced - Security scans (gitleaks) prevent secret commits ## ๐Ÿ“ Template Updates **Templates are embedded from:** `templates_embed/templates/` **To modify templates:** 1. Edit template files directly in `templates_embed/templates/` 2. Rebuild the binary: `go build .` 3. Templates are automatically embedded via `//go:embed` directive **Available template locations:** - Default: `templates_embed/templates/readme.tmpl` - GitHub theme: `templates_embed/templates/themes/github/readme.tmpl` - GitLab theme: `templates_embed/templates/themes/gitlab/readme.tmpl` - Minimal theme: `templates_embed/templates/themes/minimal/readme.tmpl` - Professional: `templates_embed/templates/themes/professional/readme.tmpl` - AsciiDoc theme: `templates_embed/templates/themes/asciidoc/readme.adoc` **Template embedding:** Handled by `templates_embed/embed.go` using Go's embed directive. The embedded filesystem is used by default, with fallback to filesystem for development. ## ๐Ÿšจ CRITICAL: README Protection **NEVER overwrite `/README.md`** - The root README.md is the main project documentation. **For testing generation commands:** ```bash # Safe testing approaches gh-action-readme gen testdata/example-action/ gh-action-readme gen testdata/composite-action/action.yml gh-action-readme gen testdata/ --output /tmp/test-output.md ``` ## ๐Ÿ—๏ธ Architecture Overview ### Command Handler Pattern **All Cobra command handlers return errors** instead of calling `os.Exit()` directly. This enables comprehensive unit testing. **Pattern:** ```go // Handler function signature - returns error func myHandler(cmd *cobra.Command, args []string) error { if err := someOperation(); err != nil { return fmt.Errorf("operation failed: %w", err) } return nil } // Wrapped in command definition for Cobra compatibility var myCmd = &cobra.Command{ Use: "my-command", Short: "Description", Run: wrapHandlerWithErrorHandling(myHandler), } ``` The `wrapHandlerWithErrorHandling()` wrapper (in `main.go`): - Initializes `globalConfig` if nil (important for testing) - Calls the handler and captures the error - Displays error via `ColoredOutput` and exits with code 1 if error occurs **Testing handlers:** ```go func TestMyHandler(t *testing.T) { cmd := &cobra.Command{} cmd.Flags().String("some-flag", "default", "") err := myHandler(cmd, []string{}) // Can now test error conditions without os.Exit() if err != nil { t.Errorf("unexpected error: %v", err) } } ``` ### Dependency Injection for Testing Functions that interact with I/O or global state use **nil-default parameter pattern** for testability: ```go // Production signature with optional injectable dependencies func myFunction(output *ColoredOutput, config *AppConfig, reader InputReader) error { // Default to real implementations if not provided if config == nil { config = globalConfig } if reader == nil { reader = &StdinReader{} // Real stdin } // ... function logic } // Production usage (pass nil for defaults) err := myFunction(output, nil, nil) // Test usage (inject mocks) mockConfig := internal.DefaultAppConfig() mockReader := &TestInputReader{responses: []string{"y"}} err := myFunction(output, mockConfig, mockReader) ``` **Examples in codebase:** - `applyUpdates()` - accepts `InputReader` for stdin mocking (main.go:1094) - `setupDepsUpgrade()` - accepts `*AppConfig` for config injection (main.go:1001) **Test interfaces:** ```go // InputReader for mocking user input type InputReader interface { ReadLine() (string, error) } type TestInputReader struct { responses []string index int } ``` ### Template Rendering Pipeline 1. **Parser** (`internal/parser.go`): - Parses `action.yml` files using `goccy/go-yaml` - Extracts permissions from header comments via `parsePermissionsFromComments()` - Merges comment and YAML permissions (YAML takes precedence) - Returns `*ActionYML` struct with all parsed data 1. **Template Data Builder** (`internal/template.go`): - `BuildTemplateData()` creates comprehensive `TemplateData` struct - Embeds `*ActionYML` (all action fields accessible via `.Name`, `.Inputs`, `.Permissions`, etc.) - Detects git repository info (org, repo, default branch) - Extracts action subdirectory for monorepo support - Builds `uses:` statement with proper path/version 1. **Template Functions** (`internal/template.go:templateFuncs()`): - `gitUsesString` - Generates complete `org/repo/path@version` string - `actionVersion` - Determines version (config override โ†’ default branch โ†’ "v1") - `gitOrg`, `gitRepo` - Extract git repository information - Standard Go template functions: `lower`, `upper`, `replace`, `join` 1. **Renderer** (`internal/template.go:RenderReadme()`): - Reads template from embedded filesystem via `templates_embed.ReadTemplate()` - Executes template with `TemplateData` - Supports multiple formats (md, html, json, asciidoc) ### Key Data Structures **ActionYML** - Parsed action.yml data: ```go type ActionYML struct { Name string Description string Inputs map[string]ActionInput Outputs map[string]ActionOutput Runs map[string]any Branding *Branding Permissions map[string]string // From comments or YAML field } ``` **TemplateData** - Complete data for rendering: ```go type TemplateData struct { *ActionYML // Embedded - all fields accessible directly Git git.RepoInfo Config *AppConfig UsesStatement string // Pre-built "org/repo/path@version" ActionPath string // For subdirectory extraction RepoRoot string Dependencies []dependencies.Dependency } ``` ### Monorepo Action Path Resolution The tool automatically detects and handles monorepo actions: ```text Input: /repo/actions/csharp-build/action.yml Root: /repo Output: org/repo/actions/csharp-build@main ``` Implementation in `internal/template.go:extractActionSubdirectory()`: - Calculates relative path from repo root to action directory - Returns empty string for root-level actions - Returns subdirectory path for monorepo actions - Used by `buildUsesString()` to construct proper `uses:` statements ### Permissions Parsing Supports three sources (merged with priority): 1. **Header comments** (lowest priority): ```yaml # permissions: # - contents: read # - issues: write ``` 1. **YAML field** (highest priority): ```yaml permissions: contents: write ``` 1. **Merged result**: YAML overrides comment values for duplicate keys, all unique keys included ## ๐Ÿ› ๏ธ Development Commands ### Building and Running ```bash # Build binary go build . # Run without installing go run . gen testdata/example-action/ go run . validate go run . config show # Build and run tests make build make test make test-coverage # With coverage report make test-coverage-html # HTML coverage + open in browser ``` ### Testing ```bash # Run all tests go test ./... # Run specific test package go test ./internal go test ./internal/wizard # Run specific test by name go test ./internal -run TestParsePermissions go test ./internal -v -run "TestParseActionYML_.*Permissions" # Run tests with race detection go test -race ./... # Test all themes for theme in default github gitlab minimal professional; do ./gh-action-readme gen testdata/example-action/ --theme $theme --output /tmp/test-$theme.md done ``` ### Advanced Testing #### Mutation Testing Mutation testing verifies test effectiveness by modifying source code and checking if tests catch the changes. **Status:** Mutation test files are implemented but currently disabled due to go-mutesting tool compatibility issues with Go 1.25+. The test code is ready for when compatibility is resolved. **Test files created:** - `internal/parser_mutation_test.go` - Permission parsing mutations - `internal/validation/validation_mutation_test.go` - Version validation mutations - `internal/validation/strings_mutation_test.go` - URL/string parsing mutations **What they test:** - Parser: permission extraction, indentation logic, comment handling - Validation: version format checks, URL parsing, string sanitization **Expected results:** <5% mutation survival rate (>95% of mutations caught by tests) #### Property-Based Testing Property-based testing uses random input generation to verify mathematical properties and invariants: ```bash # Run all property tests make test-property # Run property tests by component make test-property-validation # String manipulation properties make test-property-parser # Permission merging properties ``` **What it tests:** - Idempotency: `f(f(x)) == f(x)` - Invariants: No consecutive spaces, no boundary whitespace - Structural properties: Required symbols present, correct format - Identity properties: Empty inputs produce empty outputs **Test generation:** Each property is verified with 100+ random inputs #### Quick vs Comprehensive Testing ```bash # Quick test (unit tests only, ~4 seconds) make test-quick # Comprehensive test (unit + property tests, ~6 seconds) make test # Coverage analysis make test-coverage # CLI coverage report make test-coverage-html # HTML coverage report + browser make test-coverage-check # Verify coverage >= 72% ``` **Note:** Mutation tests require go-mutesting (Go 1.22/1.23 compatible). Run `make test-mutation` if supported. Not included in `make test` by default for broad compatibility. ### Linting and Quality ```bash # Run all linters via pre-commit make lint # Run golangci-lint directly golangci-lint run golangci-lint run --timeout=5m # Check editor config compliance make editorconfig # Auto-fix editorconfig issues make editorconfig-fix # Format code make format ``` ### Security Scanning ```bash # Run all security checks make security # Individual security tools make vulncheck # Go vulnerability check make audit # Nancy dependency audit make trivy # Container security scanner make gitleaks # Secret detection ``` ### Dependencies ```bash # Check for outdated dependencies make deps-check # Update dependencies interactively make deps-update # Update all dependencies to latest make deps-update-all # Install development tools make devtools ``` ### Pre-commit Hooks ```bash # Install hooks (run once per clone) make pre-commit-install # Update hooks to latest versions make pre-commit-update # Run hooks manually pre-commit run --all-files ``` ## โš™๏ธ Configuration System ### Configuration Hierarchy (highest to lowest priority) 1. **Command-line flags** - Override everything 2. **Action-specific config** - `.ghreadme.yaml` in action directory 3. **Repository config** - `.ghreadme.yaml` in repo root 4. **Global config** - `~/.config/gh-action-readme/config.yaml` 5. **Environment variables** - `GH_README_GITHUB_TOKEN`, `GITHUB_TOKEN` 6. **Defaults** - Built-in fallbacks ### Version Resolution for Usage Examples Priority order for `uses: org/repo@VERSION`: 1. `Config.Version` - Explicit override (e.g., `version: "v2.0.0"`) 2. `Config.UseDefaultBranch` + `Git.DefaultBranch` - Detected branch (e.g., `@main`) 3. Fallback - `"v1"` Implemented in `internal/template.go:getActionVersion()`. ### Permissions Parsing **Comment Format Support:** - List format: `# permissions:\n# - key: value` - Object format: `# permissions:\n# key: value` - Mixed format: Both styles in same block - Inline comments: `# key: value # explanation` **Merge Behavior:** ```go // internal/parser.go:ParseActionYML() if a.Permissions == nil && commentPermissions != nil { a.Permissions = commentPermissions // Use comments } else if a.Permissions != nil && commentPermissions != nil { // Merge: YAML overrides, add missing from comments for key, value := range commentPermissions { if _, exists := a.Permissions[key]; !exists { a.Permissions[key] = value } } } ``` ## ๐Ÿ”„ Adding New Features ### New Theme 1. Create template file: ```bash touch templates_embed/templates/themes/THEME_NAME/readme.tmpl ``` 1. Add theme constant to `appconstants/constants.go`: ```go ThemeTHEMENAME = "theme-name" TemplatePathTHEMENAME = "templates/themes/theme-name/readme.tmpl" ``` **Note:** The template path constant still uses `templates/` prefix (not `templates_embed/templates/`) as this is the logical path used by the code. The physical file lives in `templates_embed/templates/` but is referenced as `templates/` in the code. 1. Add to theme resolver in `internal/config.go:resolveThemeTemplate()`: ```go case appconstants.ThemeTHEMENAME: templatePath = appconstants.TemplatePathTHEMENAME ``` 1. Update `main.go:configThemesHandler()` to list the new theme 2. Rebuild: `go build .` ### New Output Format 1. Add format constant to `appconstants/constants.go` 2. Add case in `internal/generator.go:GenerateFromFile()`: ```go case appconstants.OutputFormatNEW: return g.generateNEW(action, outputDir, actionPath) ``` 1. Implement generator method: ```go func (g *Generator) generateNEW(action *ActionYML, outputDir, actionPath string) error { opts := TemplateOptions{ TemplatePath: g.resolveTemplatePathForFormat(), Format: "new", } // ... implementation } ``` 1. Update CLI help text in `main.go` ### New Template Function Add to `internal/template.go:templateFuncs()`: ```go func templateFuncs() template.FuncMap { return template.FuncMap{ "myFunc": myFuncImplementation, // ... existing functions } } ``` ### New Parser Field When adding fields to `ActionYML`: 1. Update struct in `internal/parser.go` 2. Update `ActionYMLForJSON` in `internal/json_writer.go` (for JSON output) 3. Add field to JSON struct initialization 4. Add tests in `internal/parser_test.go` 5. Update templates if field should be displayed ## ๐Ÿ“Š Package Structure - **`main.go`** - CLI entry point (Cobra commands) - **`internal/generator.go`** - Core generation orchestration - **`internal/parser.go`** - Action.yml parsing (including permissions from comments) - **`internal/template.go`** - Template data building and rendering - **`internal/config.go`** - Configuration management (Viper) - **`internal/json_writer.go`** - JSON output format - **`internal/output.go`** - Colored CLI output - **`internal/progress.go`** - Progress bars for batch operations - **`internal/git/`** - Git repository detection - **`internal/validation/`** - Action.yml validation - **`internal/wizard/`** - Interactive configuration wizard - **`internal/dependencies/`** - Dependency analysis for actions - **`internal/errors/`** - Contextual error handling - **`appconstants/`** - Application constants - **`testutil/`** - Testing utilities - **`templates_embed/`** - Embedded template filesystem ## ๐Ÿงช Testing Guidelines ### Test File Locations - Unit tests: `internal/*_test.go` alongside source files - Test fixtures: `testdata/yaml-fixtures/` (organized by type) - Integration tests: Manual CLI testing with testdata ### Test Fixture Organization **CRITICAL:** Always use fixtures, never inline YAML in tests. **Fixture Structure:** ```text testdata/yaml-fixtures/ โ”œโ”€โ”€ actions/ โ”‚ โ”œโ”€โ”€ composite/ # Composite actions โ”‚ โ”œโ”€โ”€ javascript/ # JavaScript actions โ”‚ โ”œโ”€โ”€ docker/ # Docker actions โ”‚ โ””โ”€โ”€ invalid/ # Invalid actions for error testing โ”œโ”€โ”€ dependencies/ # Actions with specific dependencies โ”œโ”€โ”€ configs/ # Configuration files โ””โ”€โ”€ error-scenarios/ # Edge cases and error conditions ``` **Using Fixtures in Tests:** ```go // Use fixture constants from testutil/test_constants.go testutil.WriteActionFixture(t, tmpDir, testutil.TestFixtureCompositeBasic) // Available fixture constants: // - TestFixtureJavaScriptSimple // - TestFixtureCompositeBasic // - TestFixtureCompositeWithDeps // - TestFixtureCompositeMultipleNamedSteps // - TestFixtureActionWithCheckoutV3/V4 // See testutil/test_constants.go for complete list ``` **Adding New Fixtures:** 1. Create YAML file in appropriate subdirectory: `testdata/yaml-fixtures/actions/composite/my-new-fixture.yml` 2. Add constant to `testutil/test_constants.go`: `TestFixtureMyNewFixture = "actions/composite/my-new-fixture.yml"` 3. Use in tests: `testutil.WriteActionFixture(t, tmpDir, testutil.TestFixtureMyNewFixture)` ### Running Specific Tests ```bash # Parser tests go test ./internal -v -run TestParse # Permissions tests go test ./internal -run ".*Permissions" # Template tests go test ./internal -run ".*Template|.*Uses" # Generator tests go test ./internal -run "TestGenerator" # Wizard tests go test ./internal/wizard -v ``` ### Template Testing After Updates ```bash # 1. Rebuild with updated templates go build . # 2. Test all themes for theme in default github gitlab minimal professional; do ./gh-action-readme gen testdata/example-action/ --theme $theme --output /tmp/test-$theme.md echo "=== $theme theme ===" grep -i "permissions" /tmp/test-$theme.md && echo "โœ… Found" || echo "โŒ Missing" done # 4. Test JSON output ./gh-action-readme gen testdata/example-action/ -f json -o /tmp/test.json cat /tmp/test.json | python3 -m json.tool | grep -A 3 permissions ``` ## ๐Ÿ“ฆ Dependency Management **Automated Updates:** - Renovate bot runs weekly (Mondays 4am UTC) - Auto-merges minor/patch updates - Major updates require manual review - Groups `golang.org/x` packages together **Manual Updates:** ```bash make deps-check # Show outdated make deps-update # Interactive with go-mod-upgrade make deps-update-all # Update all to latest ``` ## ๐Ÿ” Security **Pre-commit Hooks:** - `gitleaks` - Secret detection - `golangci-lint` - Static analysis including security checks - `editorconfig-checker` - File format validation **Security Scanning:** - CodeQL analysis on push/PR - Go vulnerability check (govulncheck) - Trivy container scanning - Nancy dependency audit **Path Validation:** All file reads use validated paths to prevent path traversal: ```go // templates_embed/embed.go:ReadTemplate() cleanPath := filepath.Clean(templatePath) if cleanPath != templatePath || strings.Contains(cleanPath, "..") { return nil, filepath.ErrBadPattern } ```