Files
gh-action-readme/CLAUDE.md

24 KiB

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:

// ❌ 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:

// ❌ 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 <noreply@anthropic.com> 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:

❌ BAD:
refactor: move inline YAML to fixtures

Benefits:
- Improved maintainability
- Better separation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

✅ 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

🚫 Commit Messages Over 100 Characters

Commitlint enforces header-max-length: 100. Keep commit message first lines under 100 characters. The commit-msg hook catches this locally if installed via make pre-commit-install.

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":

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:

# 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:

// 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:

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:

// 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:

// 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:

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:

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:

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):
# permissions:
#   - contents: read
#   - issues: write
  1. YAML field (highest priority):
permissions:
  contents: write
  1. Merged result: YAML overrides comment values for duplicate keys, all unique keys included

🛠️ Development Commands

Building and Running

# 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

# 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:

# 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

# 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

# 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

# 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

# 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

# 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:

// 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:
touch templates_embed/templates/themes/THEME_NAME/readme.tmpl
  1. Add theme constant to appconstants/constants.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():
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():

case appconstants.OutputFormatNEW:
    return g.generateNEW(action, outputDir, actionPath)
  1. Implement generator method:
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():

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:

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:

// 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

# 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

# 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:

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:

// templates_embed/embed.go:ReadTemplate()
cleanPath := filepath.Clean(templatePath)
if cleanPath != templatePath || strings.Contains(cleanPath, "..") {
    return nil, filepath.ErrBadPattern
}