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:
2026-01-01 23:17:29 +02:00
committed by GitHub
parent 85a439d804
commit 7f80105ff5
65 changed files with 2321 additions and 1710 deletions

View File

@@ -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"}
}
}

View File

@@ -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)
}
}

View File

@@ -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()

View File

@@ -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_") ||

View File

@@ -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