Files
gh-action-readme/internal/wizard/exporter.go
Ismo Vuorinen 7f80105ff5 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
2026-01-01 23:17:29 +02:00

291 lines
9.1 KiB
Go

// Package wizard provides configuration export functionality.
package wizard
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/goccy/go-yaml"
"github.com/ivuorinen/gh-action-readme/appconstants"
"github.com/ivuorinen/gh-action-readme/internal"
)
// ExportFormat represents the supported export formats.
type ExportFormat string
const (
// FormatYAML exports configuration as YAML.
FormatYAML ExportFormat = appconstants.OutputFormatYAML
// FormatJSON exports configuration as JSON.
FormatJSON ExportFormat = appconstants.OutputFormatJSON
// FormatTOML exports configuration as TOML.
FormatTOML ExportFormat = appconstants.OutputFormatTOML
)
// 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
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)
}
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)
}
}
// 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, appconstants.ConfigYAML), 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)
}
}
// 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, yaml.Indent(2))
// Add header comment
_, _ = 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(appconstants.MsgConfigurationExportedTo, 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(appconstants.MsgConfigurationExportedTo, 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(appconstants.MsgConfigHeader)
_, _ = file.WriteString(appconstants.MsgConfigWizardHeader)
// Basic TOML export (simplified version)
e.writeTOMLConfig(file, exportConfig)
e.output.Success(appconstants.MsgConfigurationExportedTo, 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, appconstants.FormatEnvVar, 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, appconstants.FormatEnvVar, key, value)
}
}