mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-02-15 12:49:57 +00:00
This commit represents a comprehensive refactoring of the codebase focused on improving code quality, testability, and maintainability. Key improvements: - Implement dependency injection and interface-based architecture - Add comprehensive test framework with fixtures and test suites - Fix all linting issues (errcheck, gosec, staticcheck, goconst, etc.) - Achieve full EditorConfig compliance across all files - Replace hardcoded test data with proper fixture files - Add configuration loader with hierarchical config support - Improve error handling with contextual information - Add progress indicators for better user feedback - Enhance Makefile with help system and improved editorconfig commands - Consolidate constants and remove deprecated code - Strengthen validation logic for GitHub Actions - Add focused consumer interfaces for better separation of concerns Testing improvements: - Add comprehensive integration tests - Implement test executor pattern for better test organization - Create extensive YAML fixture library for testing - Fix all failing tests and improve test coverage - Add validation test fixtures to avoid embedded YAML in Go files Build and tooling: - Update Makefile to show help by default - Fix editorconfig commands to use eclint properly - Add comprehensive help documentation to all make targets - Improve file selection patterns to avoid glob errors This refactoring maintains backward compatibility while significantly improving the internal architecture and developer experience.
286 lines
8.9 KiB
Go
286 lines
8.9 KiB
Go
// Package wizard provides configuration export functionality.
|
|
package wizard
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"github.com/ivuorinen/gh-action-readme/internal"
|
|
)
|
|
|
|
// ExportFormat represents the supported export formats.
|
|
type ExportFormat string
|
|
|
|
const (
|
|
// FormatYAML exports configuration as YAML.
|
|
FormatYAML ExportFormat = "yaml"
|
|
// FormatJSON exports configuration as JSON.
|
|
FormatJSON ExportFormat = "json"
|
|
// FormatTOML exports configuration as TOML.
|
|
FormatTOML ExportFormat = "toml"
|
|
)
|
|
|
|
// ConfigExporter handles exporting configuration to various formats.
|
|
type ConfigExporter struct {
|
|
output *internal.ColoredOutput
|
|
}
|
|
|
|
// NewConfigExporter creates a new configuration exporter.
|
|
func NewConfigExporter(output *internal.ColoredOutput) *ConfigExporter {
|
|
return &ConfigExporter{
|
|
output: output,
|
|
}
|
|
}
|
|
|
|
// 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
|
|
return fmt.Errorf("failed to create output directory: %w", err)
|
|
}
|
|
|
|
switch format {
|
|
case FormatYAML:
|
|
return e.exportYAML(config, outputPath)
|
|
case FormatJSON:
|
|
return e.exportJSON(config, outputPath)
|
|
case FormatTOML:
|
|
return e.exportTOML(config, outputPath)
|
|
default:
|
|
return fmt.Errorf("unsupported export format: %s", format)
|
|
}
|
|
}
|
|
|
|
// exportYAML exports configuration as YAML.
|
|
func (e *ConfigExporter) exportYAML(config *internal.AppConfig, outputPath string) error {
|
|
// Create a clean config without sensitive data for export
|
|
exportConfig := e.sanitizeConfig(config)
|
|
|
|
file, err := os.Create(outputPath) // #nosec G304 -- output path from function parameter
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create YAML file: %w", err)
|
|
}
|
|
defer func() {
|
|
_ = file.Close() // File will be closed, error not actionable in defer
|
|
}()
|
|
|
|
encoder := yaml.NewEncoder(file)
|
|
encoder.SetIndent(2)
|
|
|
|
// Add header comment
|
|
_, _ = file.WriteString("# gh-action-readme configuration file\n")
|
|
_, _ = file.WriteString("# Generated by the interactive configuration wizard\n\n")
|
|
|
|
if err := encoder.Encode(exportConfig); err != nil {
|
|
return fmt.Errorf("failed to encode YAML: %w", err)
|
|
}
|
|
|
|
e.output.Success("Configuration exported to: %s", outputPath)
|
|
return nil
|
|
}
|
|
|
|
// exportJSON exports configuration as JSON.
|
|
func (e *ConfigExporter) exportJSON(config *internal.AppConfig, outputPath string) error {
|
|
// Create a clean config without sensitive data for export
|
|
exportConfig := e.sanitizeConfig(config)
|
|
|
|
file, err := os.Create(outputPath) // #nosec G304 -- output path from function parameter
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create JSON file: %w", err)
|
|
}
|
|
defer func() {
|
|
_ = file.Close() // File will be closed, error not actionable in defer
|
|
}()
|
|
|
|
encoder := json.NewEncoder(file)
|
|
encoder.SetIndent("", " ")
|
|
|
|
if err := encoder.Encode(exportConfig); err != nil {
|
|
return fmt.Errorf("failed to encode JSON: %w", err)
|
|
}
|
|
|
|
e.output.Success("Configuration exported to: %s", outputPath)
|
|
return nil
|
|
}
|
|
|
|
// exportTOML exports configuration as TOML.
|
|
func (e *ConfigExporter) exportTOML(config *internal.AppConfig, outputPath string) error {
|
|
// For now, we'll use a basic TOML export since the TOML library adds dependencies
|
|
// In a full implementation, you would use "github.com/BurntSushi/toml"
|
|
exportConfig := e.sanitizeConfig(config)
|
|
|
|
file, err := os.Create(outputPath) // #nosec G304 -- output path from function parameter
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create TOML file: %w", err)
|
|
}
|
|
defer func() {
|
|
_ = file.Close() // File will be closed, error not actionable in defer
|
|
}()
|
|
|
|
// Write TOML header
|
|
_, _ = file.WriteString("# gh-action-readme configuration file\n")
|
|
_, _ = file.WriteString("# Generated by the interactive configuration wizard\n\n")
|
|
|
|
// Basic TOML export (simplified version)
|
|
e.writeTOMLConfig(file, exportConfig)
|
|
|
|
e.output.Success("Configuration exported to: %s", outputPath)
|
|
return nil
|
|
}
|
|
|
|
// sanitizeConfig removes sensitive information from config for export.
|
|
func (e *ConfigExporter) sanitizeConfig(config *internal.AppConfig) *internal.AppConfig {
|
|
// Create a copy of the config
|
|
sanitized := *config
|
|
|
|
// Remove sensitive information
|
|
sanitized.GitHubToken = "" // Never export tokens
|
|
sanitized.RepoOverrides = nil // Don't export repo overrides
|
|
|
|
// Remove empty/default values to keep the config clean
|
|
if sanitized.Organization == "" {
|
|
sanitized.Organization = ""
|
|
}
|
|
if sanitized.Repository == "" {
|
|
sanitized.Repository = ""
|
|
}
|
|
if sanitized.Version == "" {
|
|
sanitized.Version = ""
|
|
}
|
|
|
|
// Remove legacy fields if they match defaults
|
|
defaults := internal.DefaultAppConfig()
|
|
if sanitized.Template == defaults.Template {
|
|
sanitized.Template = ""
|
|
}
|
|
if sanitized.Header == defaults.Header {
|
|
sanitized.Header = ""
|
|
}
|
|
if sanitized.Footer == defaults.Footer {
|
|
sanitized.Footer = ""
|
|
}
|
|
if sanitized.Schema == defaults.Schema {
|
|
sanitized.Schema = ""
|
|
}
|
|
|
|
return &sanitized
|
|
}
|
|
|
|
// writeTOMLConfig writes a basic TOML configuration.
|
|
func (e *ConfigExporter) writeTOMLConfig(file *os.File, config *internal.AppConfig) {
|
|
e.writeRepositorySection(file, config)
|
|
e.writeTemplateSection(file, config)
|
|
e.writeFeaturesSection(file, config)
|
|
e.writeBehaviorSection(file, config)
|
|
e.writeWorkflowSection(file, config)
|
|
e.writePermissionsSection(file, config)
|
|
e.writeVariablesSection(file, config)
|
|
}
|
|
|
|
// writeRepositorySection writes the repository information section.
|
|
func (e *ConfigExporter) writeRepositorySection(file *os.File, config *internal.AppConfig) {
|
|
_, _ = fmt.Fprintf(file, "# Repository Information\n")
|
|
if config.Organization != "" {
|
|
_, _ = fmt.Fprintf(file, "organization = %q\n", config.Organization)
|
|
}
|
|
if config.Repository != "" {
|
|
_, _ = fmt.Fprintf(file, "repository = %q\n", config.Repository)
|
|
}
|
|
if config.Version != "" {
|
|
_, _ = fmt.Fprintf(file, "version = %q\n", config.Version)
|
|
}
|
|
}
|
|
|
|
// writeTemplateSection writes the template settings section.
|
|
func (e *ConfigExporter) writeTemplateSection(file *os.File, config *internal.AppConfig) {
|
|
_, _ = fmt.Fprintf(file, "\n# Template Settings\n")
|
|
_, _ = fmt.Fprintf(file, "theme = %q\n", config.Theme)
|
|
_, _ = fmt.Fprintf(file, "output_format = %q\n", config.OutputFormat)
|
|
_, _ = fmt.Fprintf(file, "output_dir = %q\n", config.OutputDir)
|
|
}
|
|
|
|
// writeFeaturesSection writes the features section.
|
|
func (e *ConfigExporter) writeFeaturesSection(file *os.File, config *internal.AppConfig) {
|
|
_, _ = fmt.Fprintf(file, "\n# Features\n")
|
|
_, _ = fmt.Fprintf(file, "analyze_dependencies = %t\n", config.AnalyzeDependencies)
|
|
_, _ = fmt.Fprintf(file, "show_security_info = %t\n", config.ShowSecurityInfo)
|
|
}
|
|
|
|
// writeBehaviorSection writes the behavior section.
|
|
func (e *ConfigExporter) writeBehaviorSection(file *os.File, config *internal.AppConfig) {
|
|
_, _ = fmt.Fprintf(file, "\n# Behavior\n")
|
|
_, _ = fmt.Fprintf(file, "verbose = %t\n", config.Verbose)
|
|
_, _ = fmt.Fprintf(file, "quiet = %t\n", config.Quiet)
|
|
}
|
|
|
|
// writeWorkflowSection writes the workflow requirements section.
|
|
func (e *ConfigExporter) writeWorkflowSection(file *os.File, config *internal.AppConfig) {
|
|
if len(config.RunsOn) == 0 {
|
|
return
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(file, "\n# Workflow Requirements\n")
|
|
_, _ = fmt.Fprintf(file, "runs_on = [")
|
|
for i, runner := range config.RunsOn {
|
|
if i > 0 {
|
|
_, _ = fmt.Fprintf(file, ", ")
|
|
}
|
|
_, _ = fmt.Fprintf(file, "%q", runner)
|
|
}
|
|
_, _ = fmt.Fprintf(file, "]\n")
|
|
}
|
|
|
|
// writePermissionsSection writes the permissions section.
|
|
func (e *ConfigExporter) writePermissionsSection(file *os.File, config *internal.AppConfig) {
|
|
if len(config.Permissions) == 0 {
|
|
return
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(file, "\n[permissions]\n")
|
|
for key, value := range config.Permissions {
|
|
_, _ = fmt.Fprintf(file, "%s = %q\n", key, value)
|
|
}
|
|
}
|
|
|
|
// writeVariablesSection writes the variables section.
|
|
func (e *ConfigExporter) writeVariablesSection(file *os.File, config *internal.AppConfig) {
|
|
if len(config.Variables) == 0 {
|
|
return
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(file, "\n[variables]\n")
|
|
for key, value := range config.Variables {
|
|
_, _ = fmt.Fprintf(file, "%s = %q\n", key, value)
|
|
}
|
|
}
|
|
|
|
// GetSupportedFormats returns the list of supported export formats.
|
|
func (e *ConfigExporter) GetSupportedFormats() []ExportFormat {
|
|
return []ExportFormat{FormatYAML, FormatJSON, FormatTOML}
|
|
}
|
|
|
|
// GetDefaultOutputPath returns the default output path for a given format.
|
|
func (e *ConfigExporter) GetDefaultOutputPath(format ExportFormat) (string, error) {
|
|
configPath, err := internal.GetConfigPath()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get config directory: %w", err)
|
|
}
|
|
|
|
dir := filepath.Dir(configPath)
|
|
|
|
switch format {
|
|
case FormatYAML:
|
|
return filepath.Join(dir, "config.yaml"), nil
|
|
case FormatJSON:
|
|
return filepath.Join(dir, "config.json"), nil
|
|
case FormatTOML:
|
|
return filepath.Join(dir, "config.toml"), nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported format: %s", format)
|
|
}
|
|
}
|