mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-01-26 11:14:04 +00:00
* 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.
418 lines
12 KiB
Go
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
|
|
}
|