Files
gh-action-readme/internal/errors/suggestions.go
Ismo Vuorinen 4f12c4d3dd feat(lint): add many linters, make all the tests run fast! (#23)
* chore(lint): added nlreturn, run linting

* chore(lint): replace some fmt.Sprintf calls

* chore(lint): replace fmt.Sprintf with strconv

* chore(lint): add goconst, use http lib for status codes, and methods

* chore(lint): use errors lib, errCodes from internal/errors

* chore(lint): dupl, thelper and usetesting

* chore(lint): fmt.Errorf %v to %w, more linters

* chore(lint): paralleltest, where possible

* perf(test): optimize test performance by 78%

- Implement shared binary building with package-level cache to eliminate redundant builds
- Add strategic parallelization to 15+ tests while preserving environment variable isolation
- Implement thread-safe fixture caching with RWMutex to reduce I/O operations
- Remove unnecessary working directory changes by leveraging embedded templates
- Add embedded template system with go:embed directive for reliable template resolution
- Fix linting issues: rename sharedBinaryError to errSharedBinary, add nolint directive

Performance improvements:
- Total test execution time: 12+ seconds → 2.7 seconds (78% faster)
- Binary build overhead: 14+ separate builds → 1 shared build (93% reduction)
- Parallel execution: Limited → 15+ concurrent tests (60-70% better CPU usage)
- I/O operations: 66+ fixture reads → cached with sync.RWMutex (50% reduction)

All tests maintain 100% success rate and coverage while running nearly 4x faster.
2025-08-06 15:28:09 +03:00

418 lines
12 KiB
Go

package errors
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
)
// GetSuggestions returns context-aware suggestions for the given error code.
func GetSuggestions(code ErrorCode, context map[string]string) []string {
if handler := getSuggestionHandler(code); handler != nil {
return handler(context)
}
return getDefaultSuggestions()
}
// getSuggestionHandler returns the appropriate suggestion function for the error code.
func getSuggestionHandler(code ErrorCode) func(map[string]string) []string {
handlers := map[ErrorCode]func(map[string]string) []string{
ErrCodeFileNotFound: getFileNotFoundSuggestions,
ErrCodePermission: getPermissionSuggestions,
ErrCodeInvalidYAML: getInvalidYAMLSuggestions,
ErrCodeInvalidAction: getInvalidActionSuggestions,
ErrCodeNoActionFiles: getNoActionFilesSuggestions,
ErrCodeGitHubAPI: getGitHubAPISuggestions,
ErrCodeConfiguration: getConfigurationSuggestions,
ErrCodeValidation: getValidationSuggestions,
ErrCodeTemplateRender: getTemplateSuggestions,
ErrCodeFileWrite: getFileWriteSuggestions,
ErrCodeDependencyAnalysis: getDependencyAnalysisSuggestions,
ErrCodeCacheAccess: getCacheAccessSuggestions,
}
// Special cases for handlers without context
switch code {
case ErrCodeGitHubRateLimit:
return func(_ map[string]string) []string { return getGitHubRateLimitSuggestions() }
case ErrCodeGitHubAuth:
return func(_ map[string]string) []string { return getGitHubAuthSuggestions() }
case ErrCodeFileNotFound, ErrCodePermission, ErrCodeInvalidYAML, ErrCodeInvalidAction,
ErrCodeNoActionFiles, ErrCodeGitHubAPI, ErrCodeConfiguration, ErrCodeValidation,
ErrCodeTemplateRender, ErrCodeFileWrite, ErrCodeDependencyAnalysis, ErrCodeCacheAccess,
ErrCodeUnknown:
// These cases are handled by the map above
}
return handlers[code]
}
// getDefaultSuggestions returns generic suggestions for unknown errors.
func getDefaultSuggestions() []string {
return []string{
"Check the error message for more details",
"Run with --verbose flag for additional debugging information",
"Visit the project documentation for help",
}
}
func getFileNotFoundSuggestions(context map[string]string) []string {
suggestions := []string{}
if path, ok := context["path"]; ok {
suggestions = append(suggestions,
"Check if the file exists: "+path,
"Verify the file path is correct",
)
// Check if it might be a case sensitivity issue
dir := filepath.Dir(path)
if _, err := os.Stat(dir); err == nil {
suggestions = append(suggestions,
"Check for case sensitivity in the filename",
"Try: ls -la "+dir,
)
}
// Suggest common file names if looking for action files
if strings.Contains(path, "action") {
suggestions = append(suggestions,
"Common action file names: action.yml, action.yaml",
"Check if the file is in a subdirectory",
)
}
}
suggestions = append(suggestions,
"Use --recursive flag to search in subdirectories",
"Ensure you have read permissions for the directory",
)
return suggestions
}
func getPermissionSuggestions(context map[string]string) []string {
suggestions := []string{}
if path, ok := context["path"]; ok {
suggestions = append(suggestions,
"Check file permissions: ls -la "+path,
"Try changing permissions: chmod 644 "+path,
)
// Check if it's a directory
if info, err := os.Stat(path); err == nil && info.IsDir() {
suggestions = append(suggestions,
"For directories, try: chmod 755 "+path,
)
}
}
// Add OS-specific suggestions
switch runtime.GOOS {
case "windows":
suggestions = append(suggestions,
"Run the command prompt as Administrator",
"Check Windows file permissions in Properties > Security",
)
default:
suggestions = append(suggestions,
"Check file ownership with: ls -la",
"You may need to use sudo for system directories",
"Ensure the parent directory has write permissions",
)
}
return suggestions
}
func getInvalidYAMLSuggestions(context map[string]string) []string {
suggestions := []string{
"Check YAML indentation (use spaces, not tabs)",
"Ensure all strings with special characters are quoted",
"Verify brackets and braces are properly closed",
"Use a YAML validator: https://yaml-online-parser.appspot.com/",
}
if line, ok := context["line"]; ok {
suggestions = append([]string{
fmt.Sprintf("Error near line %s - check indentation and syntax", line),
}, suggestions...)
}
if err, ok := context["error"]; ok {
if strings.Contains(err, "tab") {
suggestions = append([]string{
"YAML files must use spaces for indentation, not tabs",
"Replace all tabs with spaces (usually 2 or 4 spaces)",
}, suggestions...)
}
}
suggestions = append(suggestions,
"Common YAML issues: missing colons, incorrect nesting, invalid characters",
"Example valid action.yml structure available in documentation",
)
return suggestions
}
func getInvalidActionSuggestions(context map[string]string) []string {
suggestions := []string{
"Ensure the action file has required fields: name, description",
"Check that 'runs' section is properly configured",
"Verify the action type is valid (composite, docker, or javascript)",
}
if missingFields, ok := context["missing_fields"]; ok {
suggestions = append([]string{
"Missing required fields: " + missingFields,
}, suggestions...)
}
if invalidField, ok := context["invalid_field"]; ok {
suggestions = append(suggestions,
fmt.Sprintf("Invalid field '%s' - check spelling and placement", invalidField),
)
}
suggestions = append(suggestions,
"Refer to GitHub Actions documentation for action.yml schema",
"Use 'gh-action-readme schema' to see the expected format",
"Validate against the schema with 'gh-action-readme validate'",
)
return suggestions
}
func getNoActionFilesSuggestions(context map[string]string) []string {
suggestions := []string{
"Ensure you're in the correct directory",
"Look for files named 'action.yml' or 'action.yaml'",
"Use --recursive flag to search subdirectories",
}
if dir, ok := context["directory"]; ok {
suggestions = append(suggestions,
"Current directory: "+dir,
fmt.Sprintf("Try: find %s -name 'action.y*ml' -type f", dir),
)
}
suggestions = append(suggestions,
"GitHub Actions must have an action.yml or action.yaml file",
"Check if the file has a different extension (.yaml vs .yml)",
"Example: gh-action-readme gen --recursive",
)
return suggestions
}
func getGitHubAPISuggestions(context map[string]string) []string {
suggestions := []string{
"Check your internet connection",
"Verify GitHub's API status: https://www.githubstatus.com/",
"Ensure your GitHub token has the necessary permissions",
}
if statusCode, ok := context["status_code"]; ok {
switch statusCode {
case "401":
suggestions = append([]string{
"Authentication failed - check your GitHub token",
"Token may be expired or revoked",
}, suggestions...)
case "403":
suggestions = append([]string{
"Access forbidden - check token permissions",
"You may have hit the rate limit",
}, suggestions...)
case "404":
suggestions = append([]string{
"Repository or resource not found",
"Check if the repository is private and token has access",
}, suggestions...)
}
}
return suggestions
}
func getGitHubRateLimitSuggestions() []string {
return []string{
"GitHub API rate limit exceeded",
"Authenticate with a GitHub token to increase limits",
"Set GITHUB_TOKEN environment variable: export GITHUB_TOKEN=your_token",
"For GitHub CLI: gh auth login",
"Rate limits reset every hour",
"Consider using caching to reduce API calls",
"Use --quiet mode to reduce API usage for non-critical features",
}
}
func getGitHubAuthSuggestions() []string {
return []string{
"Set GitHub token: export GITHUB_TOKEN=your_personal_access_token",
"Or use GitHub CLI: gh auth login",
"Create a token at: https://github.com/settings/tokens",
"Token needs 'repo' scope for private repositories",
"For public repos only, 'public_repo' scope is sufficient",
"Check if token is set: echo $GITHUB_TOKEN",
"Ensure token hasn't expired",
}
}
func getConfigurationSuggestions(context map[string]string) []string {
suggestions := []string{
"Check configuration file syntax",
"Ensure configuration file exists",
"Use 'gh-action-readme config init' to create default config",
"Valid config locations: .gh-action-readme.yml, ~/.config/gh-action-readme/config.yaml",
}
if configPath, ok := context["config_path"]; ok {
suggestions = append(suggestions,
"Config path: "+configPath,
"Check if file exists: ls -la "+configPath,
)
}
if err, ok := context["error"]; ok {
if strings.Contains(err, "permission") {
suggestions = append(suggestions,
"Check file permissions for config file",
"Ensure parent directory is writable",
)
}
}
return suggestions
}
func getValidationSuggestions(context map[string]string) []string {
suggestions := []string{
"Review validation errors for specific issues",
"Check required fields are present",
"Ensure field values match expected types",
"Use 'gh-action-readme schema' to see valid structure",
}
if fields, ok := context["invalid_fields"]; ok {
suggestions = append([]string{
"Invalid fields: " + fields,
"Check spelling and nesting of these fields",
}, suggestions...)
}
if validationType, ok := context["validation_type"]; ok {
switch validationType {
case "required":
suggestions = append(suggestions,
"Add missing required fields to your action.yml",
"Required fields typically include: name, description, runs",
)
case "type":
suggestions = append(suggestions,
"Ensure field values match expected types",
"Strings should be quoted, booleans should be true/false",
)
}
}
return suggestions
}
func getTemplateSuggestions(context map[string]string) []string {
suggestions := []string{
"Check template syntax",
"Ensure all template variables are defined",
"Verify custom template path is correct",
}
if templatePath, ok := context["template_path"]; ok {
suggestions = append(suggestions,
"Template path: "+templatePath,
"Ensure template file exists and is readable",
)
}
if theme, ok := context["theme"]; ok {
suggestions = append(suggestions,
"Current theme: "+theme,
"Try using a different theme: --theme github",
"Available themes: default, github, gitlab, minimal, professional",
)
}
return suggestions
}
func getFileWriteSuggestions(context map[string]string) []string {
suggestions := []string{
"Check if the output directory exists",
"Ensure you have write permissions",
"Verify disk space is available",
}
if outputPath, ok := context["output_path"]; ok {
dir := filepath.Dir(outputPath)
suggestions = append(suggestions,
"Output directory: "+dir,
"Check permissions: ls -la "+dir,
"Create directory if needed: mkdir -p "+dir,
)
// Check if file already exists
if _, err := os.Stat(outputPath); err == nil {
suggestions = append(suggestions,
"File already exists - it will be overwritten",
"Back up existing file if needed",
)
}
}
return suggestions
}
func getDependencyAnalysisSuggestions(context map[string]string) []string {
suggestions := []string{
"Ensure GitHub token is set for dependency analysis",
"Check that the action file contains valid dependencies",
"Verify network connectivity to GitHub",
}
if action, ok := context["action"]; ok {
suggestions = append(suggestions,
"Analyzing action: "+action,
"Only composite actions have analyzable dependencies",
)
}
return append(suggestions,
"Dependency analysis requires 'uses' statements in composite actions",
"Example: uses: actions/checkout@v4",
)
}
func getCacheAccessSuggestions(context map[string]string) []string {
suggestions := []string{
"Check cache directory permissions",
"Ensure cache directory exists",
"Try clearing cache: gh-action-readme cache clear",
"Default cache location: ~/.cache/gh-action-readme",
}
if cachePath, ok := context["cache_path"]; ok {
suggestions = append(suggestions,
"Cache path: "+cachePath,
"Check permissions: ls -la "+cachePath,
"You can disable cache temporarily with environment variables",
)
}
return suggestions
}