Files
gh-action-readme/internal/apperrors/suggestions.go
Ismo Vuorinen 7f80105ff5 feat: go 1.25.5, dependency updates, renamed internal/errors (#129)
* feat: rename internal/errors to internal/apperrors

* fix(tests): clear env values before using in tests

* feat: rename internal/errors to internal/apperrors

* chore(deps): update go and all dependencies

* chore: remove renovate from pre-commit, formatting

* chore: sonarcloud fixes

* feat: consolidate constants to appconstants/constants.go

* chore: sonarcloud fixes

* feat: simplification, deduplication, test utils

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: clean up

* fix: config discovery, const deduplication

* chore: fixes
2026-01-01 23:17:29 +02:00

416 lines
12 KiB
Go

package apperrors
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/ivuorinen/gh-action-readme/appconstants"
)
// GetSuggestions returns context-aware suggestions for the given error code.
func GetSuggestions(code appconstants.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 appconstants.ErrorCode) func(map[string]string) []string {
handlers := map[appconstants.ErrorCode]func(map[string]string) []string{
appconstants.ErrCodeFileNotFound: getFileNotFoundSuggestions,
appconstants.ErrCodePermission: getPermissionSuggestions,
appconstants.ErrCodeInvalidYAML: getInvalidYAMLSuggestions,
appconstants.ErrCodeInvalidAction: getInvalidActionSuggestions,
appconstants.ErrCodeNoActionFiles: getNoActionFilesSuggestions,
appconstants.ErrCodeGitHubAPI: getGitHubAPISuggestions,
appconstants.ErrCodeConfiguration: getConfigurationSuggestions,
appconstants.ErrCodeValidation: getValidationSuggestions,
appconstants.ErrCodeTemplateRender: getTemplateSuggestions,
appconstants.ErrCodeFileWrite: getFileWriteSuggestions,
appconstants.ErrCodeDependencyAnalysis: getDependencyAnalysisSuggestions,
appconstants.ErrCodeCacheAccess: getCacheAccessSuggestions,
}
// Special cases for handlers without context
if code == appconstants.ErrCodeGitHubRateLimit {
return func(_ map[string]string) []string { return getGitHubRateLimitSuggestions() }
}
if code == appconstants.ErrCodeGitHubAuth {
return func(_ map[string]string) []string { return getGitHubAuthSuggestions() }
}
// All other cases are handled by the handlers map
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
}