mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-03-09 03:58:49 +00:00
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
This commit is contained in:
@@ -11,17 +11,12 @@ import (
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
|
||||
"github.com/ivuorinen/gh-action-readme/appconstants"
|
||||
"github.com/ivuorinen/gh-action-readme/internal"
|
||||
"github.com/ivuorinen/gh-action-readme/internal/git"
|
||||
"github.com/ivuorinen/gh-action-readme/internal/helpers"
|
||||
)
|
||||
|
||||
const (
|
||||
// Language constants to avoid repetition.
|
||||
langJavaScriptTypeScript = "JavaScript/TypeScript"
|
||||
langGo = "Go"
|
||||
)
|
||||
|
||||
// ProjectDetector handles auto-detection of project settings.
|
||||
type ProjectDetector struct {
|
||||
output *internal.ColoredOutput
|
||||
@@ -33,7 +28,7 @@ type ProjectDetector struct {
|
||||
func NewProjectDetector(output *internal.ColoredOutput) (*ProjectDetector, error) {
|
||||
currentDir, err := helpers.GetCurrentDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current directory: %w", err)
|
||||
return nil, fmt.Errorf(appconstants.ErrFailedToGetCurrentDir, err)
|
||||
}
|
||||
|
||||
return &ProjectDetector{
|
||||
@@ -172,7 +167,7 @@ func (d *ProjectDetector) detectVersion() string {
|
||||
|
||||
// detectVersionFromPackageJSON detects version from package.json.
|
||||
func (d *ProjectDetector) detectVersionFromPackageJSON() string {
|
||||
packageJSONPath := filepath.Join(d.currentDir, "package.json")
|
||||
packageJSONPath := filepath.Join(d.currentDir, appconstants.PackageJSON)
|
||||
data, err := os.ReadFile(packageJSONPath) // #nosec G304 -- path is constructed from current directory
|
||||
if err != nil {
|
||||
return ""
|
||||
@@ -264,7 +259,7 @@ func (d *ProjectDetector) handleDirectory(info os.FileInfo) error {
|
||||
func (d *ProjectDetector) findActionFilesInDirectory(dir string) ([]string, error) {
|
||||
var actionFiles []string
|
||||
|
||||
for _, filename := range []string{"action.yml", "action.yaml"} {
|
||||
for _, filename := range []string{appconstants.ActionFileNameYML, appconstants.ActionFileNameYAML} {
|
||||
actionPath := filepath.Join(dir, filename)
|
||||
if _, err := os.Stat(actionPath); err == nil {
|
||||
actionFiles = append(actionFiles, actionPath)
|
||||
@@ -276,7 +271,7 @@ func (d *ProjectDetector) findActionFilesInDirectory(dir string) ([]string, erro
|
||||
|
||||
// isActionFile checks if a filename is an action file.
|
||||
func (d *ProjectDetector) isActionFile(filename string) bool {
|
||||
return filename == "action.yml" || filename == "action.yaml"
|
||||
return filename == appconstants.ActionFileNameYML || filename == appconstants.ActionFileNameYAML
|
||||
}
|
||||
|
||||
// analyzeActionFile analyzes an action file to extract characteristics.
|
||||
@@ -315,7 +310,7 @@ func (d *ProjectDetector) analyzeRunsSection(action map[string]any, settings *De
|
||||
}
|
||||
|
||||
// Check if it's a composite action
|
||||
if using, ok := runs["using"].(string); ok && using == "composite" {
|
||||
if using, ok := runs["using"].(string); ok && using == appconstants.ActionTypeComposite {
|
||||
settings.HasCompositeAction = true
|
||||
}
|
||||
|
||||
@@ -377,17 +372,17 @@ func (d *ProjectDetector) analyzeProjectFiles() map[string]string {
|
||||
// detectLanguageFromFile detects programming language from filename.
|
||||
func (d *ProjectDetector) detectLanguageFromFile(filename string, characteristics map[string]string) {
|
||||
switch filename {
|
||||
case "package.json":
|
||||
characteristics["language"] = langJavaScriptTypeScript
|
||||
case appconstants.PackageJSON:
|
||||
characteristics["language"] = appconstants.LangJavaScriptTypeScript
|
||||
characteristics["type"] = "Node.js Project"
|
||||
case "go.mod":
|
||||
characteristics["language"] = langGo
|
||||
characteristics["language"] = appconstants.LangGo
|
||||
characteristics["type"] = "Go Module"
|
||||
case "Cargo.toml":
|
||||
characteristics["language"] = "Rust"
|
||||
characteristics["type"] = "Rust Project"
|
||||
case "pyproject.toml", "requirements.txt":
|
||||
characteristics["language"] = "Python"
|
||||
characteristics["language"] = appconstants.LangPython
|
||||
characteristics["type"] = "Python Project"
|
||||
case "Gemfile":
|
||||
characteristics["language"] = "Ruby"
|
||||
@@ -447,11 +442,11 @@ func (d *ProjectDetector) suggestTheme(settings *DetectedSettings) {
|
||||
case settings.HasCompositeAction:
|
||||
settings.SuggestedTheme = "professional"
|
||||
case settings.HasDockerfile:
|
||||
settings.SuggestedTheme = "github"
|
||||
case settings.Language == langGo:
|
||||
settings.SuggestedTheme = "minimal"
|
||||
settings.SuggestedTheme = appconstants.ThemeGitHub
|
||||
case settings.Language == appconstants.LangGo:
|
||||
settings.SuggestedTheme = appconstants.ThemeMinimal
|
||||
case settings.Framework != "":
|
||||
settings.SuggestedTheme = "github"
|
||||
settings.SuggestedTheme = appconstants.ThemeGitHub
|
||||
default:
|
||||
settings.SuggestedTheme = "default"
|
||||
}
|
||||
@@ -464,9 +459,9 @@ func (d *ProjectDetector) suggestRunsOn(settings *DetectedSettings) {
|
||||
}
|
||||
|
||||
switch settings.Language {
|
||||
case langJavaScriptTypeScript:
|
||||
case appconstants.LangJavaScriptTypeScript:
|
||||
settings.SuggestedRunsOn = []string{"ubuntu-latest", "windows-latest", "macos-latest"}
|
||||
case langGo, "Python":
|
||||
case appconstants.LangGo, appconstants.LangPython:
|
||||
settings.SuggestedRunsOn = []string{"ubuntu-latest"}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
|
||||
"github.com/ivuorinen/gh-action-readme/appconstants"
|
||||
"github.com/ivuorinen/gh-action-readme/internal"
|
||||
)
|
||||
|
||||
@@ -17,11 +18,11 @@ type ExportFormat string
|
||||
|
||||
const (
|
||||
// FormatYAML exports configuration as YAML.
|
||||
FormatYAML ExportFormat = "yaml"
|
||||
FormatYAML ExportFormat = appconstants.OutputFormatYAML
|
||||
// FormatJSON exports configuration as JSON.
|
||||
FormatJSON ExportFormat = "json"
|
||||
FormatJSON ExportFormat = appconstants.OutputFormatJSON
|
||||
// FormatTOML exports configuration as TOML.
|
||||
FormatTOML ExportFormat = "toml"
|
||||
FormatTOML ExportFormat = appconstants.OutputFormatTOML
|
||||
)
|
||||
|
||||
// ConfigExporter handles exporting configuration to various formats.
|
||||
@@ -39,7 +40,9 @@ func NewConfigExporter(output *internal.ColoredOutput) *ConfigExporter {
|
||||
// ExportConfig exports the configuration to the specified format and path.
|
||||
func (e *ConfigExporter) ExportConfig(config *internal.AppConfig, format ExportFormat, outputPath string) error {
|
||||
// Create output directory if it doesn't exist
|
||||
if err := os.MkdirAll(filepath.Dir(outputPath), 0750); err != nil { // #nosec G301 -- output directory permissions
|
||||
outputDir := filepath.Dir(outputPath)
|
||||
// #nosec G301 -- output directory permissions
|
||||
if err := os.MkdirAll(outputDir, appconstants.FilePermDir); err != nil {
|
||||
return fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
@@ -71,7 +74,7 @@ func (e *ConfigExporter) GetDefaultOutputPath(format ExportFormat) (string, erro
|
||||
|
||||
switch format {
|
||||
case FormatYAML:
|
||||
return filepath.Join(dir, "config.yaml"), nil
|
||||
return filepath.Join(dir, appconstants.ConfigYAML), nil
|
||||
case FormatJSON:
|
||||
return filepath.Join(dir, "config.json"), nil
|
||||
case FormatTOML:
|
||||
@@ -97,14 +100,14 @@ func (e *ConfigExporter) exportYAML(config *internal.AppConfig, outputPath strin
|
||||
encoder := yaml.NewEncoder(file, yaml.Indent(2))
|
||||
|
||||
// Add header comment
|
||||
_, _ = file.WriteString("# gh-action-readme configuration file\n")
|
||||
_, _ = file.WriteString("# Generated by the interactive configuration wizard\n\n")
|
||||
_, _ = file.WriteString(appconstants.MsgConfigHeader)
|
||||
_, _ = file.WriteString(appconstants.MsgConfigWizardHeader)
|
||||
|
||||
if err := encoder.Encode(exportConfig); err != nil {
|
||||
return fmt.Errorf("failed to encode YAML: %w", err)
|
||||
}
|
||||
|
||||
e.output.Success("Configuration exported to: %s", outputPath)
|
||||
e.output.Success(appconstants.MsgConfigurationExportedTo, outputPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -129,7 +132,7 @@ func (e *ConfigExporter) exportJSON(config *internal.AppConfig, outputPath strin
|
||||
return fmt.Errorf("failed to encode JSON: %w", err)
|
||||
}
|
||||
|
||||
e.output.Success("Configuration exported to: %s", outputPath)
|
||||
e.output.Success(appconstants.MsgConfigurationExportedTo, outputPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -149,13 +152,13 @@ func (e *ConfigExporter) exportTOML(config *internal.AppConfig, outputPath strin
|
||||
}()
|
||||
|
||||
// Write TOML header
|
||||
_, _ = file.WriteString("# gh-action-readme configuration file\n")
|
||||
_, _ = file.WriteString("# Generated by the interactive configuration wizard\n\n")
|
||||
_, _ = file.WriteString(appconstants.MsgConfigHeader)
|
||||
_, _ = file.WriteString(appconstants.MsgConfigWizardHeader)
|
||||
|
||||
// Basic TOML export (simplified version)
|
||||
e.writeTOMLConfig(file, exportConfig)
|
||||
|
||||
e.output.Success("Configuration exported to: %s", outputPath)
|
||||
e.output.Success(appconstants.MsgConfigurationExportedTo, outputPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -270,7 +273,7 @@ func (e *ConfigExporter) writePermissionsSection(file *os.File, config *internal
|
||||
|
||||
_, _ = fmt.Fprintf(file, "\n[permissions]\n")
|
||||
for key, value := range config.Permissions {
|
||||
_, _ = fmt.Fprintf(file, "%s = %q\n", key, value)
|
||||
_, _ = fmt.Fprintf(file, appconstants.FormatEnvVar, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,6 +285,6 @@ func (e *ConfigExporter) writeVariablesSection(file *os.File, config *internal.A
|
||||
|
||||
_, _ = fmt.Fprintf(file, "\n[variables]\n")
|
||||
for key, value := range config.Variables {
|
||||
_, _ = fmt.Fprintf(file, "%s = %q\n", key, value)
|
||||
_, _ = fmt.Fprintf(file, appconstants.FormatEnvVar, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/goccy/go-yaml"
|
||||
|
||||
"github.com/ivuorinen/gh-action-readme/internal"
|
||||
"github.com/ivuorinen/gh-action-readme/testutil"
|
||||
)
|
||||
|
||||
func TestConfigExporter_ExportConfig(t *testing.T) {
|
||||
@@ -68,7 +69,7 @@ func testYAMLExport(exporter *ConfigExporter, config *internal.AppConfig) func(*
|
||||
t.Fatalf("ExportConfig() error = %v", err)
|
||||
}
|
||||
|
||||
verifyFileExists(t, outputPath)
|
||||
testutil.AssertFileExists(t, outputPath)
|
||||
verifyYAMLContent(t, outputPath, config)
|
||||
}
|
||||
}
|
||||
@@ -85,7 +86,7 @@ func testJSONExport(exporter *ConfigExporter, config *internal.AppConfig) func(*
|
||||
t.Fatalf("ExportConfig() error = %v", err)
|
||||
}
|
||||
|
||||
verifyFileExists(t, outputPath)
|
||||
testutil.AssertFileExists(t, outputPath)
|
||||
verifyJSONContent(t, outputPath, config)
|
||||
}
|
||||
}
|
||||
@@ -102,19 +103,11 @@ func testTOMLExport(exporter *ConfigExporter, config *internal.AppConfig) func(*
|
||||
t.Fatalf("ExportConfig() error = %v", err)
|
||||
}
|
||||
|
||||
verifyFileExists(t, outputPath)
|
||||
testutil.AssertFileExists(t, outputPath)
|
||||
verifyTOMLContent(t, outputPath)
|
||||
}
|
||||
}
|
||||
|
||||
// verifyFileExists checks that a file exists at the given path.
|
||||
func verifyFileExists(t *testing.T, outputPath string) {
|
||||
t.Helper()
|
||||
if _, err := os.Stat(outputPath); os.IsNotExist(err) {
|
||||
t.Fatal("Expected output file to exist")
|
||||
}
|
||||
}
|
||||
|
||||
// verifyYAMLContent verifies YAML content is valid and contains expected data.
|
||||
func verifyYAMLContent(t *testing.T, outputPath string, expected *internal.AppConfig) {
|
||||
t.Helper()
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/ivuorinen/gh-action-readme/appconstants"
|
||||
"github.com/ivuorinen/gh-action-readme/internal"
|
||||
)
|
||||
|
||||
@@ -88,11 +89,11 @@ func (v *ConfigValidator) ValidateField(fieldName, value string) *ValidationResu
|
||||
v.validateRepository(value, result)
|
||||
case "version":
|
||||
v.validateVersion(value, result)
|
||||
case "theme":
|
||||
case appconstants.ConfigKeyTheme:
|
||||
v.validateTheme(value, result)
|
||||
case "output_format":
|
||||
case appconstants.ConfigKeyOutputFormat:
|
||||
v.validateOutputFormat(value, result)
|
||||
case "output_dir":
|
||||
case appconstants.ConfigKeyOutputDir:
|
||||
v.validateOutputDir(value, result)
|
||||
case "github_token":
|
||||
v.validateGitHubToken(value, result)
|
||||
@@ -129,7 +130,7 @@ func (v *ConfigValidator) DisplayValidationResult(result *ValidationResult) {
|
||||
|
||||
// Display suggestions
|
||||
if len(result.Suggestions) > 0 {
|
||||
v.output.Info("\nSuggestions:")
|
||||
v.output.Info(appconstants.SectionSuggestions)
|
||||
for _, suggestion := range result.Suggestions {
|
||||
v.output.Printf(" 💡 %s", suggestion)
|
||||
}
|
||||
@@ -485,8 +486,8 @@ func (v *ConfigValidator) isValidGitHubToken(token string) bool {
|
||||
// GitHub personal access tokens start with ghp_ or github_pat_
|
||||
// Classic tokens are 40 characters after the prefix
|
||||
// Fine-grained tokens have different formats
|
||||
return strings.HasPrefix(token, "ghp_") ||
|
||||
strings.HasPrefix(token, "github_pat_") ||
|
||||
return strings.HasPrefix(token, appconstants.TokenPrefixGitHubPersonal) ||
|
||||
strings.HasPrefix(token, appconstants.TokenPrefixGitHubPAT) ||
|
||||
strings.HasPrefix(token, "gho_") ||
|
||||
strings.HasPrefix(token, "ghu_") ||
|
||||
strings.HasPrefix(token, "ghs_") ||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ivuorinen/gh-action-readme/appconstants"
|
||||
"github.com/ivuorinen/gh-action-readme/internal"
|
||||
"github.com/ivuorinen/gh-action-readme/internal/git"
|
||||
"github.com/ivuorinen/gh-action-readme/internal/helpers"
|
||||
@@ -72,7 +73,7 @@ func (w *ConfigWizard) detectProjectSettings() error {
|
||||
// Detect current directory
|
||||
currentDir, err := helpers.GetCurrentDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current directory: %w", err)
|
||||
return fmt.Errorf(appconstants.ErrFailedToGetCurrentDir, err)
|
||||
}
|
||||
|
||||
w.actionDir = currentDir
|
||||
@@ -180,7 +181,7 @@ func (w *ConfigWizard) displayThemeOptions(themes []struct {
|
||||
for i, theme := range themes {
|
||||
marker := " "
|
||||
if theme.name == w.config.Theme {
|
||||
marker = "►"
|
||||
marker = appconstants.SymbolArrow
|
||||
}
|
||||
w.output.Printf(" %s %d. %s - %s", marker, i+1, theme.name, theme.desc)
|
||||
}
|
||||
@@ -191,7 +192,7 @@ func (w *ConfigWizard) displayFormatOptions(formats []string) {
|
||||
for i, format := range formats {
|
||||
marker := " "
|
||||
if format == w.config.OutputFormat {
|
||||
marker = "►"
|
||||
marker = appconstants.SymbolArrow
|
||||
}
|
||||
w.output.Printf(" %s %d. %s", marker, i+1, format)
|
||||
}
|
||||
@@ -247,7 +248,9 @@ func (w *ConfigWizard) configureGitHubIntegration() {
|
||||
token := w.promptSensitive("Enter your GitHub token (or press Enter to skip)")
|
||||
if token != "" {
|
||||
// Validate token format (basic check)
|
||||
if strings.HasPrefix(token, "ghp_") || strings.HasPrefix(token, "github_pat_") {
|
||||
hasPersonalPrefix := strings.HasPrefix(token, appconstants.TokenPrefixGitHubPersonal)
|
||||
hasPATPrefix := strings.HasPrefix(token, appconstants.TokenPrefixGitHubPAT)
|
||||
if hasPersonalPrefix || hasPATPrefix {
|
||||
w.config.GitHubToken = token
|
||||
w.output.Success("GitHub token configured ✓")
|
||||
} else {
|
||||
@@ -297,9 +300,9 @@ func (w *ConfigWizard) confirmConfiguration() error {
|
||||
// promptWithDefault prompts for input with a default value.
|
||||
func (w *ConfigWizard) promptWithDefault(prompt, defaultValue string) string {
|
||||
if defaultValue != "" {
|
||||
w.output.Printf("%s [%s]: ", prompt, defaultValue)
|
||||
w.output.Printf(appconstants.FormatPromptDefault, prompt, defaultValue)
|
||||
} else {
|
||||
w.output.Printf("%s: ", prompt)
|
||||
w.output.Printf(appconstants.FormatPrompt, prompt)
|
||||
}
|
||||
|
||||
if w.scanner.Scan() {
|
||||
@@ -316,7 +319,7 @@ func (w *ConfigWizard) promptWithDefault(prompt, defaultValue string) string {
|
||||
|
||||
// promptSensitive prompts for sensitive input (like tokens) without echoing.
|
||||
func (w *ConfigWizard) promptSensitive(prompt string) string {
|
||||
w.output.Printf("%s: ", prompt)
|
||||
w.output.Printf(appconstants.FormatPrompt, prompt)
|
||||
if w.scanner.Scan() {
|
||||
return strings.TrimSpace(w.scanner.Text())
|
||||
}
|
||||
@@ -331,12 +334,12 @@ func (w *ConfigWizard) promptYesNo(prompt string, defaultValue bool) bool {
|
||||
defaultStr = "Y/n"
|
||||
}
|
||||
|
||||
w.output.Printf("%s [%s]: ", prompt, defaultStr)
|
||||
w.output.Printf(appconstants.FormatPromptDefault, prompt, defaultStr)
|
||||
|
||||
if w.scanner.Scan() {
|
||||
input := strings.ToLower(strings.TrimSpace(w.scanner.Text()))
|
||||
switch input {
|
||||
case "y", "yes":
|
||||
case "y", appconstants.InputYes:
|
||||
return true
|
||||
case "n", "no":
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user