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.gofor 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.gofor 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 filestestdata/yaml-fixtures/actions/- Action filestestdata/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:
- Check
testutil/test_constants.gofor existing constants - Check
testdata/yaml-fixtures/for existing fixtures - Consider if your function will exceed complexity limits
- Plan helper functions for complex logic upfront
Before committing:
- Run
make lint- catches complexity and duplication - Pre-commit hooks will catch most issues
- 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:
- Edit template files directly in
templates_embed/templates/ - Rebuild the binary:
go build . - Templates are automatically embedded via
//go:embeddirective
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
globalConfigif nil (important for testing) - Calls the handler and captures the error
- Displays error via
ColoredOutputand 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()- acceptsInputReaderfor stdin mocking (main.go:1094)setupDepsUpgrade()- accepts*AppConfigfor 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
- Parser (
internal/parser.go):
- Parses
action.ymlfiles usinggoccy/go-yaml - Extracts permissions from header comments via
parsePermissionsFromComments() - Merges comment and YAML permissions (YAML takes precedence)
- Returns
*ActionYMLstruct with all parsed data
- Template Data Builder (
internal/template.go):
BuildTemplateData()creates comprehensiveTemplateDatastruct- 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
- Template Functions (
internal/template.go:templateFuncs()):
gitUsesString- Generates completeorg/repo/path@versionstringactionVersion- Determines version (config override → default branch → "v1")gitOrg,gitRepo- Extract git repository information- Standard Go template functions:
lower,upper,replace,join
- 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 properuses:statements
Permissions Parsing
Supports three sources (merged with priority):
- Header comments (lowest priority):
# permissions:
# - contents: read
# - issues: write
- YAML field (highest priority):
permissions:
contents: write
- 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 mutationsinternal/validation/validation_mutation_test.go- Version validation mutationsinternal/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)
- Command-line flags - Override everything
- Action-specific config -
.ghreadme.yamlin action directory - Repository config -
.ghreadme.yamlin repo root - Global config -
~/.config/gh-action-readme/config.yaml - Environment variables -
GH_README_GITHUB_TOKEN,GITHUB_TOKEN - Defaults - Built-in fallbacks
Version Resolution for Usage Examples
Priority order for uses: org/repo@VERSION:
Config.Version- Explicit override (e.g.,version: "v2.0.0")Config.UseDefaultBranch+Git.DefaultBranch- Detected branch (e.g.,@main)- 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
- Create template file:
touch templates_embed/templates/themes/THEME_NAME/readme.tmpl
- 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.
- Add to theme resolver in
internal/config.go:resolveThemeTemplate():
case appconstants.ThemeTHEMENAME:
templatePath = appconstants.TemplatePathTHEMENAME
-
Update
main.go:configThemesHandler()to list the new theme -
Rebuild:
go build .
New Output Format
-
Add format constant to
appconstants/constants.go -
Add case in
internal/generator.go:GenerateFromFile():
case appconstants.OutputFormatNEW:
return g.generateNEW(action, outputDir, actionPath)
- Implement generator method:
func (g *Generator) generateNEW(action *ActionYML, outputDir, actionPath string) error {
opts := TemplateOptions{
TemplatePath: g.resolveTemplatePathForFormat(),
Format: "new",
}
// ... implementation
}
- 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:
- Update struct in
internal/parser.go - Update
ActionYMLForJSONininternal/json_writer.go(for JSON output) - Add field to JSON struct initialization
- Add tests in
internal/parser_test.go - Update templates if field should be displayed
📊 Package Structure
main.go- CLI entry point (Cobra commands)internal/generator.go- Core generation orchestrationinternal/parser.go- Action.yml parsing (including permissions from comments)internal/template.go- Template data building and renderinginternal/config.go- Configuration management (Viper)internal/json_writer.go- JSON output formatinternal/output.go- Colored CLI outputinternal/progress.go- Progress bars for batch operationsinternal/git/- Git repository detectioninternal/validation/- Action.yml validationinternal/wizard/- Interactive configuration wizardinternal/dependencies/- Dependency analysis for actionsinternal/errors/- Contextual error handlingappconstants/- Application constantstestutil/- Testing utilitiestemplates_embed/- Embedded template filesystem
🧪 Testing Guidelines
Test File Locations
- Unit tests:
internal/*_test.goalongside 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:
- Create YAML file in appropriate subdirectory:
testdata/yaml-fixtures/actions/composite/my-new-fixture.yml - Add constant to
testutil/test_constants.go:TestFixtureMyNewFixture = "actions/composite/my-new-fixture.yml" - 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/xpackages 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 detectiongolangci-lint- Static analysis including security checkseditorconfig-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
}