mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-03-14 14:00:53 +00:00
feat: add interactive wizard, contextual errors, and code improvements
- Add interactive configuration wizard with auto-detection and multi-format export - Implement contextual error system with 14 error codes and actionable suggestions - Add centralized progress indicators with consistent theming - Fix all cyclomatic complexity issues (8 functions refactored) - Eliminate code duplication with centralized utilities and error handling - Add comprehensive test coverage for all new components - Update TODO.md with completed tasks and accurate completion dates
This commit is contained in:
416
internal/errors/suggestions.go
Normal file
416
internal/errors/suggestions.go
Normal file
@@ -0,0 +1,416 @@
|
||||
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,
|
||||
fmt.Sprintf("Check if the file exists: %s", 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",
|
||||
fmt.Sprintf("Try: ls -la %s", 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,
|
||||
fmt.Sprintf("Check file permissions: ls -la %s", path),
|
||||
fmt.Sprintf("Try changing permissions: chmod 644 %s", path),
|
||||
)
|
||||
|
||||
// Check if it's a directory
|
||||
if info, err := os.Stat(path); err == nil && info.IsDir() {
|
||||
suggestions = append(suggestions,
|
||||
fmt.Sprintf("For directories, try: chmod 755 %s", 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{
|
||||
fmt.Sprintf("Missing required fields: %s", 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,
|
||||
fmt.Sprintf("Current directory: %s", 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,
|
||||
fmt.Sprintf("Config path: %s", configPath),
|
||||
fmt.Sprintf("Check if file exists: ls -la %s", 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{
|
||||
fmt.Sprintf("Invalid fields: %s", 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,
|
||||
fmt.Sprintf("Template path: %s", templatePath),
|
||||
"Ensure template file exists and is readable",
|
||||
)
|
||||
}
|
||||
|
||||
if theme, ok := context["theme"]; ok {
|
||||
suggestions = append(suggestions,
|
||||
fmt.Sprintf("Current theme: %s", 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,
|
||||
fmt.Sprintf("Output directory: %s", dir),
|
||||
fmt.Sprintf("Check permissions: ls -la %s", dir),
|
||||
fmt.Sprintf("Create directory if needed: mkdir -p %s", 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,
|
||||
fmt.Sprintf("Analyzing action: %s", 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,
|
||||
fmt.Sprintf("Cache path: %s", cachePath),
|
||||
fmt.Sprintf("Check permissions: ls -la %s", cachePath),
|
||||
"You can disable cache temporarily with environment variables",
|
||||
)
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
Reference in New Issue
Block a user