mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-01-26 03:04:10 +00:00
feat(lint): add many linters, make all the tests run fast! (#23)
* 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.
This commit is contained in:
2
internal/cache/cache.go
vendored
2
internal/cache/cache.go
vendored
@@ -233,6 +233,7 @@ func (c *Cache) loadFromDisk() error {
|
||||
if os.IsNotExist(err) {
|
||||
return nil // No cache file is fine
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to read cache file: %w", err)
|
||||
}
|
||||
|
||||
@@ -285,6 +286,7 @@ func (c *Cache) estimateSize(value any) int64 {
|
||||
if err != nil {
|
||||
return 100 // Default estimate
|
||||
}
|
||||
|
||||
return int64(len(jsonData))
|
||||
}
|
||||
|
||||
|
||||
46
internal/cache/cache_test.go
vendored
46
internal/cache/cache_test.go
vendored
@@ -1,8 +1,8 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -39,20 +39,13 @@ func TestNewCache(t *testing.T) {
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
originalXDGCache := os.Getenv("XDG_CACHE_HOME")
|
||||
_ = os.Setenv("XDG_CACHE_HOME", tmpDir)
|
||||
defer func() {
|
||||
if originalXDGCache != "" {
|
||||
_ = os.Setenv("XDG_CACHE_HOME", originalXDGCache)
|
||||
} else {
|
||||
_ = os.Unsetenv("XDG_CACHE_HOME")
|
||||
}
|
||||
}()
|
||||
t.Setenv("XDG_CACHE_HOME", tmpDir)
|
||||
|
||||
cache, err := NewCache(tt.config)
|
||||
|
||||
if tt.expectError {
|
||||
testutil.AssertError(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -111,6 +104,8 @@ func TestCache_SetAndGet(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Set value
|
||||
err := cache.Set(tt.key, tt.value)
|
||||
testutil.AssertNoError(t, err)
|
||||
@@ -168,6 +163,7 @@ func TestCache_GetOrSet(t *testing.T) {
|
||||
callCount := 0
|
||||
getter := func() (any, error) {
|
||||
callCount++
|
||||
|
||||
return fmt.Sprintf("generated-value-%d", callCount), nil
|
||||
}
|
||||
|
||||
@@ -193,7 +189,7 @@ func TestCache_GetOrSetError(t *testing.T) {
|
||||
|
||||
// Getter that returns error
|
||||
getter := func() (any, error) {
|
||||
return nil, fmt.Errorf("getter error")
|
||||
return nil, errors.New("getter error")
|
||||
}
|
||||
|
||||
value, err := cache.GetOrSet("error-key", getter)
|
||||
@@ -237,6 +233,7 @@ func TestCache_ConcurrentAccess(t *testing.T) {
|
||||
err := cache.Set(key, value)
|
||||
if err != nil {
|
||||
t.Errorf("error setting value: %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -244,11 +241,13 @@ func TestCache_ConcurrentAccess(t *testing.T) {
|
||||
retrieved, exists := cache.Get(key)
|
||||
if !exists {
|
||||
t.Errorf("expected key %s to exist", key)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if retrieved != value {
|
||||
t.Errorf("expected %s, got %s", value, retrieved)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -409,15 +408,7 @@ func TestCache_CleanupExpiredEntries(t *testing.T) {
|
||||
MaxSize: 1024 * 1024,
|
||||
}
|
||||
|
||||
originalXDGCache := os.Getenv("XDG_CACHE_HOME")
|
||||
_ = os.Setenv("XDG_CACHE_HOME", tmpDir)
|
||||
defer func() {
|
||||
if originalXDGCache != "" {
|
||||
_ = os.Setenv("XDG_CACHE_HOME", originalXDGCache)
|
||||
} else {
|
||||
_ = os.Unsetenv("XDG_CACHE_HOME")
|
||||
}
|
||||
}()
|
||||
t.Setenv("XDG_CACHE_HOME", tmpDir)
|
||||
|
||||
cache, err := NewCache(config)
|
||||
testutil.AssertNoError(t, err)
|
||||
@@ -453,12 +444,15 @@ func TestCache_ErrorHandling(t *testing.T) {
|
||||
{
|
||||
name: "invalid cache directory permissions",
|
||||
setupFunc: func(t *testing.T) *Cache {
|
||||
t.Helper()
|
||||
// This test would require special setup for permission testing
|
||||
// For now, we'll create a valid cache and test other error scenarios
|
||||
tmpDir, _ := testutil.TempDir(t)
|
||||
|
||||
return createTestCache(t, tmpDir)
|
||||
},
|
||||
testFunc: func(t *testing.T, cache *Cache) {
|
||||
t.Helper()
|
||||
// Test setting a value that might cause issues during marshaling
|
||||
// Circular reference would cause JSON marshal to fail, but
|
||||
// Go's JSON package handles most cases gracefully
|
||||
@@ -542,6 +536,8 @@ func TestCache_EstimateSize(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
size := cache.estimateSize(tt.value)
|
||||
if size < tt.minSize || size > tt.maxSize {
|
||||
t.Errorf("expected size between %d and %d, got %d", tt.minSize, tt.maxSize, size)
|
||||
@@ -554,15 +550,7 @@ func TestCache_EstimateSize(t *testing.T) {
|
||||
func createTestCache(t *testing.T, tmpDir string) *Cache {
|
||||
t.Helper()
|
||||
|
||||
originalXDGCache := os.Getenv("XDG_CACHE_HOME")
|
||||
_ = os.Setenv("XDG_CACHE_HOME", tmpDir)
|
||||
t.Cleanup(func() {
|
||||
if originalXDGCache != "" {
|
||||
_ = os.Setenv("XDG_CACHE_HOME", originalXDGCache)
|
||||
} else {
|
||||
_ = os.Unsetenv("XDG_CACHE_HOME")
|
||||
}
|
||||
})
|
||||
t.Setenv("XDG_CACHE_HOME", tmpDir)
|
||||
|
||||
cache, err := NewCache(DefaultConfig())
|
||||
testutil.AssertNoError(t, err)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/ivuorinen/gh-action-readme/internal/git"
|
||||
"github.com/ivuorinen/gh-action-readme/internal/validation"
|
||||
"github.com/ivuorinen/gh-action-readme/templates_embed"
|
||||
)
|
||||
|
||||
// AppConfig represents the application configuration that can be used at multiple levels.
|
||||
@@ -143,13 +144,22 @@ func FillMissing(action *ActionYML, defs DefaultValues) {
|
||||
}
|
||||
}
|
||||
|
||||
// resolveTemplatePath resolves a template path relative to the binary directory if it's not absolute.
|
||||
// resolveTemplatePath resolves a template path, preferring embedded templates.
|
||||
// For custom/absolute paths, falls back to filesystem.
|
||||
func resolveTemplatePath(templatePath string) string {
|
||||
if filepath.IsAbs(templatePath) {
|
||||
return templatePath
|
||||
}
|
||||
|
||||
// Check if template exists in current directory first (for tests)
|
||||
// Check if template is available in embedded filesystem first
|
||||
if templates_embed.IsEmbeddedTemplateAvailable(templatePath) {
|
||||
// Return a special marker to indicate this should use embedded templates
|
||||
// The actual template loading will handle embedded vs filesystem
|
||||
return templatePath
|
||||
}
|
||||
|
||||
// Fallback to filesystem resolution for custom templates
|
||||
// Check if template exists in current directory
|
||||
if _, err := os.Stat(templatePath); err == nil {
|
||||
return templatePath
|
||||
}
|
||||
@@ -579,5 +589,6 @@ func GetConfigPath() (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get XDG config file path: %w", err)
|
||||
}
|
||||
|
||||
return configDir, nil
|
||||
}
|
||||
|
||||
@@ -9,19 +9,6 @@ import (
|
||||
)
|
||||
|
||||
func TestInitConfig(t *testing.T) {
|
||||
// Save original environment
|
||||
originalXDGConfig := os.Getenv("XDG_CONFIG_HOME")
|
||||
originalHome := os.Getenv("HOME")
|
||||
defer func() {
|
||||
if originalXDGConfig != "" {
|
||||
_ = os.Setenv("XDG_CONFIG_HOME", originalXDGConfig)
|
||||
} else {
|
||||
_ = os.Unsetenv("XDG_CONFIG_HOME")
|
||||
}
|
||||
if originalHome != "" {
|
||||
_ = os.Setenv("HOME", originalHome)
|
||||
}
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -49,6 +36,7 @@ func TestInitConfig(t *testing.T) {
|
||||
name: "custom config file",
|
||||
configFile: "custom-config.yml",
|
||||
setupFunc: func(t *testing.T, tempDir string) {
|
||||
t.Helper()
|
||||
configPath := filepath.Join(tempDir, "custom-config.yml")
|
||||
testutil.WriteTestFile(t, configPath, testutil.MustReadFixture("professional-config.yml"))
|
||||
},
|
||||
@@ -67,6 +55,7 @@ func TestInitConfig(t *testing.T) {
|
||||
name: "invalid config file",
|
||||
configFile: "config.yml",
|
||||
setupFunc: func(t *testing.T, tempDir string) {
|
||||
t.Helper()
|
||||
configPath := filepath.Join(tempDir, "config.yml")
|
||||
testutil.WriteTestFile(t, configPath, "invalid: yaml: content: [")
|
||||
},
|
||||
@@ -85,8 +74,8 @@ func TestInitConfig(t *testing.T) {
|
||||
defer cleanup()
|
||||
|
||||
// Set XDG_CONFIG_HOME to our temp directory
|
||||
_ = os.Setenv("XDG_CONFIG_HOME", tmpDir)
|
||||
_ = os.Setenv("HOME", tmpDir)
|
||||
t.Setenv("XDG_CONFIG_HOME", tmpDir)
|
||||
t.Setenv("HOME", tmpDir)
|
||||
|
||||
if tt.setupFunc != nil {
|
||||
tt.setupFunc(t, tmpDir)
|
||||
@@ -102,6 +91,7 @@ func TestInitConfig(t *testing.T) {
|
||||
|
||||
if tt.expectError {
|
||||
testutil.AssertError(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -132,6 +122,7 @@ func TestLoadConfiguration(t *testing.T) {
|
||||
{
|
||||
name: "multi-level config hierarchy",
|
||||
setupFunc: func(t *testing.T, tempDir string) (string, string, string) {
|
||||
t.Helper()
|
||||
// Create global config
|
||||
globalConfigDir := filepath.Join(tempDir, ".config", "gh-action-readme")
|
||||
_ = os.MkdirAll(globalConfigDir, 0750) // #nosec G301 -- test directory permissions
|
||||
@@ -161,6 +152,7 @@ output_dir: output
|
||||
return globalConfigPath, repoRoot, currentDir
|
||||
},
|
||||
checkFunc: func(t *testing.T, config *AppConfig) {
|
||||
t.Helper()
|
||||
// Should have action-level overrides
|
||||
testutil.AssertEqual(t, "professional", config.Theme)
|
||||
testutil.AssertEqual(t, "output", config.OutputDir)
|
||||
@@ -173,9 +165,10 @@ output_dir: output
|
||||
{
|
||||
name: "environment variable overrides",
|
||||
setupFunc: func(t *testing.T, tempDir string) (string, string, string) {
|
||||
t.Helper()
|
||||
// Set environment variables
|
||||
_ = os.Setenv("GH_README_GITHUB_TOKEN", "env-token")
|
||||
_ = os.Setenv("GITHUB_TOKEN", "fallback-token")
|
||||
t.Setenv("GH_README_GITHUB_TOKEN", "env-token")
|
||||
t.Setenv("GITHUB_TOKEN", "fallback-token")
|
||||
|
||||
// Create config file
|
||||
configPath := filepath.Join(tempDir, "config.yml")
|
||||
@@ -184,14 +177,10 @@ theme: minimal
|
||||
github_token: config-token
|
||||
`)
|
||||
|
||||
t.Cleanup(func() {
|
||||
_ = os.Unsetenv("GH_README_GITHUB_TOKEN")
|
||||
_ = os.Unsetenv("GITHUB_TOKEN")
|
||||
})
|
||||
|
||||
return configPath, tempDir, tempDir
|
||||
},
|
||||
checkFunc: func(t *testing.T, config *AppConfig) {
|
||||
t.Helper()
|
||||
// Environment variable should override config file
|
||||
testutil.AssertEqual(t, "env-token", config.GitHubToken)
|
||||
testutil.AssertEqual(t, "minimal", config.Theme)
|
||||
@@ -200,9 +189,10 @@ github_token: config-token
|
||||
{
|
||||
name: "XDG compliance",
|
||||
setupFunc: func(t *testing.T, tempDir string) (string, string, string) {
|
||||
t.Helper()
|
||||
// Set XDG environment variables
|
||||
xdgConfigHome := filepath.Join(tempDir, "xdg-config")
|
||||
_ = os.Setenv("XDG_CONFIG_HOME", xdgConfigHome)
|
||||
t.Setenv("XDG_CONFIG_HOME", xdgConfigHome)
|
||||
|
||||
// Create XDG-compliant config
|
||||
configDir := filepath.Join(xdgConfigHome, "gh-action-readme")
|
||||
@@ -213,13 +203,10 @@ theme: github
|
||||
verbose: true
|
||||
`)
|
||||
|
||||
t.Cleanup(func() {
|
||||
_ = os.Unsetenv("XDG_CONFIG_HOME")
|
||||
})
|
||||
|
||||
return configPath, tempDir, tempDir
|
||||
},
|
||||
checkFunc: func(t *testing.T, config *AppConfig) {
|
||||
t.Helper()
|
||||
testutil.AssertEqual(t, "github", config.Theme)
|
||||
testutil.AssertEqual(t, true, config.Verbose)
|
||||
},
|
||||
@@ -227,6 +214,7 @@ verbose: true
|
||||
{
|
||||
name: "hidden config file discovery",
|
||||
setupFunc: func(t *testing.T, tempDir string) (string, string, string) {
|
||||
t.Helper()
|
||||
repoRoot := filepath.Join(tempDir, "repo")
|
||||
_ = os.MkdirAll(repoRoot, 0750) // #nosec G301 -- test directory permissions
|
||||
|
||||
@@ -249,6 +237,7 @@ verbose: true
|
||||
return "", repoRoot, repoRoot
|
||||
},
|
||||
checkFunc: func(t *testing.T, config *AppConfig) {
|
||||
t.Helper()
|
||||
// Should use the first found config (.ghreadme.yaml has priority)
|
||||
testutil.AssertEqual(t, "minimal", config.Theme)
|
||||
testutil.AssertEqual(t, "json", config.OutputFormat)
|
||||
@@ -262,15 +251,7 @@ verbose: true
|
||||
defer cleanup()
|
||||
|
||||
// Set HOME to temp directory for fallback
|
||||
originalHome := os.Getenv("HOME")
|
||||
_ = os.Setenv("HOME", tmpDir)
|
||||
defer func() {
|
||||
if originalHome != "" {
|
||||
_ = os.Setenv("HOME", originalHome)
|
||||
} else {
|
||||
_ = os.Unsetenv("HOME")
|
||||
}
|
||||
}()
|
||||
t.Setenv("HOME", tmpDir)
|
||||
|
||||
configFile, repoRoot, currentDir := tt.setupFunc(t, tmpDir)
|
||||
|
||||
@@ -278,6 +259,7 @@ verbose: true
|
||||
|
||||
if tt.expectError {
|
||||
testutil.AssertError(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -291,19 +273,6 @@ verbose: true
|
||||
}
|
||||
|
||||
func TestGetConfigPath(t *testing.T) {
|
||||
// Save original environment
|
||||
originalXDGConfig := os.Getenv("XDG_CONFIG_HOME")
|
||||
originalHome := os.Getenv("HOME")
|
||||
defer func() {
|
||||
if originalXDGConfig != "" {
|
||||
_ = os.Setenv("XDG_CONFIG_HOME", originalXDGConfig)
|
||||
} else {
|
||||
_ = os.Unsetenv("XDG_CONFIG_HOME")
|
||||
}
|
||||
if originalHome != "" {
|
||||
_ = os.Setenv("HOME", originalHome)
|
||||
}
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -312,17 +281,19 @@ func TestGetConfigPath(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "XDG_CONFIG_HOME set",
|
||||
setupFunc: func(_ *testing.T, tempDir string) {
|
||||
_ = os.Setenv("XDG_CONFIG_HOME", tempDir)
|
||||
_ = os.Unsetenv("HOME")
|
||||
setupFunc: func(t *testing.T, tempDir string) {
|
||||
t.Helper()
|
||||
t.Setenv("XDG_CONFIG_HOME", tempDir)
|
||||
t.Setenv("HOME", "")
|
||||
},
|
||||
contains: "gh-action-readme",
|
||||
},
|
||||
{
|
||||
name: "HOME fallback",
|
||||
setupFunc: func(_ *testing.T, tempDir string) {
|
||||
_ = os.Unsetenv("XDG_CONFIG_HOME")
|
||||
_ = os.Setenv("HOME", tempDir)
|
||||
setupFunc: func(t *testing.T, tempDir string) {
|
||||
t.Helper()
|
||||
t.Setenv("XDG_CONFIG_HOME", "")
|
||||
t.Setenv("HOME", tempDir)
|
||||
},
|
||||
contains: ".config",
|
||||
},
|
||||
@@ -352,15 +323,7 @@ func TestWriteDefaultConfig(t *testing.T) {
|
||||
defer cleanup()
|
||||
|
||||
// Set XDG_CONFIG_HOME to our temp directory
|
||||
originalXDGConfig := os.Getenv("XDG_CONFIG_HOME")
|
||||
_ = os.Setenv("XDG_CONFIG_HOME", tmpDir)
|
||||
defer func() {
|
||||
if originalXDGConfig != "" {
|
||||
_ = os.Setenv("XDG_CONFIG_HOME", originalXDGConfig)
|
||||
} else {
|
||||
_ = os.Unsetenv("XDG_CONFIG_HOME")
|
||||
}
|
||||
}()
|
||||
t.Setenv("XDG_CONFIG_HOME", tmpDir)
|
||||
|
||||
err := WriteDefaultConfig()
|
||||
testutil.AssertNoError(t, err)
|
||||
@@ -387,6 +350,7 @@ func TestWriteDefaultConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveThemeTemplate(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
theme string
|
||||
@@ -443,12 +407,14 @@ func TestResolveThemeTemplate(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
path := resolveThemeTemplate(tt.theme)
|
||||
|
||||
if tt.expectError {
|
||||
if path != "" {
|
||||
t.Errorf("expected empty path on error, got: %s", path)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -467,48 +433,11 @@ func TestResolveThemeTemplate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigTokenHierarchy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T) func()
|
||||
expectedToken string
|
||||
}{
|
||||
{
|
||||
name: "GH_README_GITHUB_TOKEN has highest priority",
|
||||
setupFunc: func(_ *testing.T) func() {
|
||||
_ = os.Setenv("GH_README_GITHUB_TOKEN", "priority-token")
|
||||
_ = os.Setenv("GITHUB_TOKEN", "fallback-token")
|
||||
return func() {
|
||||
_ = os.Unsetenv("GH_README_GITHUB_TOKEN")
|
||||
_ = os.Unsetenv("GITHUB_TOKEN")
|
||||
}
|
||||
},
|
||||
expectedToken: "priority-token",
|
||||
},
|
||||
{
|
||||
name: "GITHUB_TOKEN as fallback",
|
||||
setupFunc: func(_ *testing.T) func() {
|
||||
_ = os.Unsetenv("GH_README_GITHUB_TOKEN")
|
||||
_ = os.Setenv("GITHUB_TOKEN", "fallback-token")
|
||||
return func() {
|
||||
_ = os.Unsetenv("GITHUB_TOKEN")
|
||||
}
|
||||
},
|
||||
expectedToken: "fallback-token",
|
||||
},
|
||||
{
|
||||
name: "no environment variables",
|
||||
setupFunc: func(_ *testing.T) func() {
|
||||
_ = os.Unsetenv("GH_README_GITHUB_TOKEN")
|
||||
_ = os.Unsetenv("GITHUB_TOKEN")
|
||||
return func() {}
|
||||
},
|
||||
expectedToken: "",
|
||||
},
|
||||
}
|
||||
tests := testutil.GetGitHubTokenHierarchyTests()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cleanup := tt.setupFunc(t)
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
cleanup := tt.SetupFunc(t)
|
||||
defer cleanup()
|
||||
|
||||
tmpDir, tmpCleanup := testutil.TempDir(t)
|
||||
@@ -518,7 +447,7 @@ func TestConfigTokenHierarchy(t *testing.T) {
|
||||
config, err := LoadConfiguration("", tmpDir, tmpDir)
|
||||
testutil.AssertNoError(t, err)
|
||||
|
||||
testutil.AssertEqual(t, tt.expectedToken, config.GitHubToken)
|
||||
testutil.AssertEqual(t, tt.ExpectedToken, config.GitHubToken)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -547,22 +476,8 @@ verbose: true
|
||||
`)
|
||||
|
||||
// Set HOME and XDG_CONFIG_HOME to temp directory
|
||||
originalHome := os.Getenv("HOME")
|
||||
originalXDGConfig := os.Getenv("XDG_CONFIG_HOME")
|
||||
_ = os.Setenv("HOME", tmpDir)
|
||||
_ = os.Setenv("XDG_CONFIG_HOME", filepath.Join(tmpDir, ".config"))
|
||||
defer func() {
|
||||
if originalHome != "" {
|
||||
_ = os.Setenv("HOME", originalHome)
|
||||
} else {
|
||||
_ = os.Unsetenv("HOME")
|
||||
}
|
||||
if originalXDGConfig != "" {
|
||||
_ = os.Setenv("XDG_CONFIG_HOME", originalXDGConfig)
|
||||
} else {
|
||||
_ = os.Unsetenv("XDG_CONFIG_HOME")
|
||||
}
|
||||
}()
|
||||
t.Setenv("HOME", tmpDir)
|
||||
t.Setenv("XDG_CONFIG_HOME", filepath.Join(tmpDir, ".config"))
|
||||
|
||||
// Use the specific config file path instead of relying on XDG discovery
|
||||
configPath := filepath.Join(tmpDir, ".config", "gh-action-readme", "config.yaml")
|
||||
@@ -579,21 +494,6 @@ verbose: true
|
||||
|
||||
// TestGetGitHubToken tests GitHub token resolution with different priority levels.
|
||||
func TestGetGitHubToken(t *testing.T) {
|
||||
// Save and restore original environment
|
||||
originalToolToken := os.Getenv(EnvGitHubToken)
|
||||
originalStandardToken := os.Getenv(EnvGitHubTokenStandard)
|
||||
defer func() {
|
||||
if originalToolToken != "" {
|
||||
_ = os.Setenv(EnvGitHubToken, originalToolToken)
|
||||
} else {
|
||||
_ = os.Unsetenv(EnvGitHubToken)
|
||||
}
|
||||
if originalStandardToken != "" {
|
||||
_ = os.Setenv(EnvGitHubTokenStandard, originalStandardToken)
|
||||
} else {
|
||||
_ = os.Unsetenv(EnvGitHubTokenStandard)
|
||||
}
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -643,14 +543,14 @@ func TestGetGitHubToken(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Set up environment
|
||||
if tt.toolEnvToken != "" {
|
||||
_ = os.Setenv(EnvGitHubToken, tt.toolEnvToken)
|
||||
t.Setenv(EnvGitHubToken, tt.toolEnvToken)
|
||||
} else {
|
||||
_ = os.Unsetenv(EnvGitHubToken)
|
||||
t.Setenv(EnvGitHubToken, "")
|
||||
}
|
||||
if tt.stdEnvToken != "" {
|
||||
_ = os.Setenv(EnvGitHubTokenStandard, tt.stdEnvToken)
|
||||
t.Setenv(EnvGitHubTokenStandard, tt.stdEnvToken)
|
||||
} else {
|
||||
_ = os.Unsetenv(EnvGitHubTokenStandard)
|
||||
t.Setenv(EnvGitHubTokenStandard, "")
|
||||
}
|
||||
|
||||
config := &AppConfig{GitHubToken: tt.configToken}
|
||||
@@ -663,6 +563,7 @@ func TestGetGitHubToken(t *testing.T) {
|
||||
|
||||
// TestMergeMapFields tests the merging of map fields in configuration.
|
||||
func TestMergeMapFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
dst *AppConfig
|
||||
@@ -743,6 +644,7 @@ func TestMergeMapFields(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Deep copy dst to avoid modifying test data
|
||||
dst := &AppConfig{}
|
||||
if tt.dst.Permissions != nil {
|
||||
@@ -768,6 +670,7 @@ func TestMergeMapFields(t *testing.T) {
|
||||
|
||||
// TestMergeSliceFields tests the merging of slice fields in configuration.
|
||||
func TestMergeSliceFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
dst *AppConfig
|
||||
@@ -808,16 +711,19 @@ func TestMergeSliceFields(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
mergeSliceFields(tt.dst, tt.src)
|
||||
|
||||
// Compare slices manually since they can't be compared directly
|
||||
if len(tt.expected) != len(tt.dst.RunsOn) {
|
||||
t.Errorf("expected slice length %d, got %d", len(tt.expected), len(tt.dst.RunsOn))
|
||||
|
||||
return
|
||||
}
|
||||
for i, expected := range tt.expected {
|
||||
if i >= len(tt.dst.RunsOn) || tt.dst.RunsOn[i] != expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, tt.dst.RunsOn)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -126,6 +127,7 @@ func (cl *ConfigurationLoader) loadGlobalStep(config *AppConfig, configFile stri
|
||||
return fmt.Errorf("failed to load global config: %w", err)
|
||||
}
|
||||
cl.mergeConfigs(config, globalConfig, true) // Allow tokens for global config
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -149,6 +151,7 @@ func (cl *ConfigurationLoader) loadRepoConfigStep(config *AppConfig, repoRoot st
|
||||
return fmt.Errorf("failed to load repo config: %w", err)
|
||||
}
|
||||
cl.mergeConfigs(config, repoConfig, false) // No tokens in repo config
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -163,6 +166,7 @@ func (cl *ConfigurationLoader) loadActionConfigStep(config *AppConfig, actionDir
|
||||
return fmt.Errorf("failed to load action config: %w", err)
|
||||
}
|
||||
cl.mergeConfigs(config, actionConfig, false) // No tokens in action config
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -181,7 +185,7 @@ func (cl *ConfigurationLoader) LoadGlobalConfig(configFile string) (*AppConfig,
|
||||
// ValidateConfiguration validates a configuration for consistency and required values.
|
||||
func (cl *ConfigurationLoader) ValidateConfiguration(config *AppConfig) error {
|
||||
if config == nil {
|
||||
return fmt.Errorf("configuration cannot be nil")
|
||||
return errors.New("configuration cannot be nil")
|
||||
}
|
||||
|
||||
// Validate output format
|
||||
@@ -200,12 +204,12 @@ func (cl *ConfigurationLoader) ValidateConfiguration(config *AppConfig) error {
|
||||
|
||||
// Validate output directory
|
||||
if config.OutputDir == "" {
|
||||
return fmt.Errorf("output directory cannot be empty")
|
||||
return errors.New("output directory cannot be empty")
|
||||
}
|
||||
|
||||
// Validate mutually exclusive flags
|
||||
if config.Verbose && config.Quiet {
|
||||
return fmt.Errorf("verbose and quiet flags are mutually exclusive")
|
||||
return errors.New("verbose and quiet flags are mutually exclusive")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -373,7 +377,7 @@ func (cl *ConfigurationLoader) setViperDefaults(v *viper.Viper) {
|
||||
// validateTheme validates that a theme exists and is supported.
|
||||
func (cl *ConfigurationLoader) validateTheme(theme string) error {
|
||||
if theme == "" {
|
||||
return fmt.Errorf("theme cannot be empty")
|
||||
return errors.New("theme cannot be empty")
|
||||
}
|
||||
|
||||
// Check if it's a built-in theme
|
||||
@@ -399,6 +403,7 @@ func containsString(slice []string, str string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -410,6 +415,7 @@ func (cl *ConfigurationLoader) GetConfigurationSources() []ConfigurationSource {
|
||||
sources = append(sources, source)
|
||||
}
|
||||
}
|
||||
|
||||
return sources
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func TestNewConfigurationLoader(t *testing.T) {
|
||||
t.Parallel()
|
||||
loader := NewConfigurationLoader()
|
||||
|
||||
if loader == nil {
|
||||
@@ -38,6 +39,7 @@ func TestNewConfigurationLoader(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewConfigurationLoaderWithOptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
opts ConfigurationOptions
|
||||
@@ -75,6 +77,7 @@ func TestNewConfigurationLoaderWithOptions(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
loader := NewConfigurationLoaderWithOptions(tt.opts)
|
||||
|
||||
for _, expectedSource := range tt.expected {
|
||||
@@ -94,6 +97,7 @@ func TestNewConfigurationLoaderWithOptions(t *testing.T) {
|
||||
for _, expectedSource := range tt.expected {
|
||||
if source == expectedSource {
|
||||
expected = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -171,12 +175,10 @@ quiet: false
|
||||
},
|
||||
{
|
||||
name: "environment variable overrides",
|
||||
setupFunc: func(_ *testing.T, tempDir string) (string, string, string) {
|
||||
setupFunc: func(t *testing.T, tempDir string) (string, string, string) {
|
||||
t.Helper()
|
||||
// Set environment variables
|
||||
_ = os.Setenv("GH_README_GITHUB_TOKEN", "env-token")
|
||||
t.Cleanup(func() {
|
||||
_ = os.Unsetenv("GH_README_GITHUB_TOKEN")
|
||||
})
|
||||
t.Setenv("GH_README_GITHUB_TOKEN", "env-token")
|
||||
|
||||
// Create config file with different token
|
||||
configPath := filepath.Join(tempDir, "config.yml")
|
||||
@@ -245,15 +247,7 @@ verbose: true
|
||||
defer cleanup()
|
||||
|
||||
// Set HOME to temp directory for fallback
|
||||
originalHome := os.Getenv("HOME")
|
||||
_ = os.Setenv("HOME", tmpDir)
|
||||
defer func() {
|
||||
if originalHome != "" {
|
||||
_ = os.Setenv("HOME", originalHome)
|
||||
} else {
|
||||
_ = os.Unsetenv("HOME")
|
||||
}
|
||||
}()
|
||||
t.Setenv("HOME", tmpDir)
|
||||
|
||||
configFile, repoRoot, actionDir := tt.setupFunc(t, tmpDir)
|
||||
|
||||
@@ -272,6 +266,7 @@ verbose: true
|
||||
|
||||
if tt.expectError {
|
||||
testutil.AssertError(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -294,6 +289,7 @@ func TestConfigurationLoader_LoadGlobalConfig(t *testing.T) {
|
||||
{
|
||||
name: "valid global config",
|
||||
setupFunc: func(t *testing.T, tempDir string) string {
|
||||
t.Helper()
|
||||
configPath := filepath.Join(tempDir, "config.yaml")
|
||||
testutil.WriteTestFile(t, configPath, `
|
||||
theme: professional
|
||||
@@ -301,6 +297,7 @@ output_format: html
|
||||
github_token: test-token
|
||||
verbose: true
|
||||
`)
|
||||
|
||||
return configPath
|
||||
},
|
||||
checkFunc: func(_ *testing.T, config *AppConfig) {
|
||||
@@ -320,8 +317,10 @@ verbose: true
|
||||
{
|
||||
name: "invalid YAML",
|
||||
setupFunc: func(t *testing.T, tempDir string) string {
|
||||
t.Helper()
|
||||
configPath := filepath.Join(tempDir, "invalid.yaml")
|
||||
testutil.WriteTestFile(t, configPath, "invalid: yaml: content: [")
|
||||
|
||||
return configPath
|
||||
},
|
||||
expectError: true,
|
||||
@@ -334,15 +333,7 @@ verbose: true
|
||||
defer cleanup()
|
||||
|
||||
// Set HOME to temp directory
|
||||
originalHome := os.Getenv("HOME")
|
||||
_ = os.Setenv("HOME", tmpDir)
|
||||
defer func() {
|
||||
if originalHome != "" {
|
||||
_ = os.Setenv("HOME", originalHome)
|
||||
} else {
|
||||
_ = os.Unsetenv("HOME")
|
||||
}
|
||||
}()
|
||||
t.Setenv("HOME", tmpDir)
|
||||
|
||||
configFile := tt.setupFunc(t, tmpDir)
|
||||
|
||||
@@ -351,6 +342,7 @@ verbose: true
|
||||
|
||||
if tt.expectError {
|
||||
testutil.AssertError(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -364,6 +356,7 @@ verbose: true
|
||||
}
|
||||
|
||||
func TestConfigurationLoader_ValidateConfiguration(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
config *AppConfig
|
||||
@@ -442,6 +435,7 @@ func TestConfigurationLoader_ValidateConfiguration(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
loader := NewConfigurationLoader()
|
||||
err := loader.ValidateConfiguration(tt.config)
|
||||
|
||||
@@ -458,6 +452,7 @@ func TestConfigurationLoader_ValidateConfiguration(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigurationLoader_SourceManagement(t *testing.T) {
|
||||
t.Parallel()
|
||||
loader := NewConfigurationLoader()
|
||||
|
||||
// Test initial state
|
||||
@@ -487,6 +482,7 @@ func TestConfigurationLoader_SourceManagement(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigurationSource_String(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
source ConfigurationSource
|
||||
expected string
|
||||
@@ -510,48 +506,11 @@ func TestConfigurationSource_String(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigurationLoader_EnvironmentOverrides(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T) func()
|
||||
expectedToken string
|
||||
}{
|
||||
{
|
||||
name: "GH_README_GITHUB_TOKEN priority",
|
||||
setupFunc: func(_ *testing.T) func() {
|
||||
_ = os.Setenv("GH_README_GITHUB_TOKEN", "priority-token")
|
||||
_ = os.Setenv("GITHUB_TOKEN", "fallback-token")
|
||||
return func() {
|
||||
_ = os.Unsetenv("GH_README_GITHUB_TOKEN")
|
||||
_ = os.Unsetenv("GITHUB_TOKEN")
|
||||
}
|
||||
},
|
||||
expectedToken: "priority-token",
|
||||
},
|
||||
{
|
||||
name: "GITHUB_TOKEN fallback",
|
||||
setupFunc: func(_ *testing.T) func() {
|
||||
_ = os.Unsetenv("GH_README_GITHUB_TOKEN")
|
||||
_ = os.Setenv("GITHUB_TOKEN", "fallback-token")
|
||||
return func() {
|
||||
_ = os.Unsetenv("GITHUB_TOKEN")
|
||||
}
|
||||
},
|
||||
expectedToken: "fallback-token",
|
||||
},
|
||||
{
|
||||
name: "no environment variables",
|
||||
setupFunc: func(_ *testing.T) func() {
|
||||
_ = os.Unsetenv("GH_README_GITHUB_TOKEN")
|
||||
_ = os.Unsetenv("GITHUB_TOKEN")
|
||||
return func() {}
|
||||
},
|
||||
expectedToken: "",
|
||||
},
|
||||
}
|
||||
tests := testutil.GetGitHubTokenHierarchyTests()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cleanup := tt.setupFunc(t)
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
cleanup := tt.SetupFunc(t)
|
||||
defer cleanup()
|
||||
|
||||
tmpDir, tmpCleanup := testutil.TempDir(t)
|
||||
@@ -561,7 +520,7 @@ func TestConfigurationLoader_EnvironmentOverrides(t *testing.T) {
|
||||
config, err := loader.LoadConfiguration("", tmpDir, "")
|
||||
testutil.AssertNoError(t, err)
|
||||
|
||||
testutil.AssertEqual(t, tt.expectedToken, config.GitHubToken)
|
||||
testutil.AssertEqual(t, tt.ExpectedToken, config.GitHubToken)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -588,15 +547,7 @@ func TestConfigurationLoader_RepoOverrides(t *testing.T) {
|
||||
testutil.WriteTestFile(t, globalConfigPath, globalConfigContent)
|
||||
|
||||
// Set environment for XDG compliance
|
||||
originalHome := os.Getenv("HOME")
|
||||
_ = os.Setenv("HOME", tmpDir)
|
||||
defer func() {
|
||||
if originalHome != "" {
|
||||
_ = os.Setenv("HOME", originalHome)
|
||||
} else {
|
||||
_ = os.Unsetenv("HOME")
|
||||
}
|
||||
}()
|
||||
t.Setenv("HOME", tmpDir)
|
||||
|
||||
loader := NewConfigurationLoader()
|
||||
config, err := loader.LoadConfiguration(globalConfigPath, repoRoot, "")
|
||||
@@ -610,6 +561,7 @@ func TestConfigurationLoader_RepoOverrides(t *testing.T) {
|
||||
|
||||
// TestConfigurationLoader_ApplyRepoOverrides tests repo-specific overrides.
|
||||
func TestConfigurationLoader_ApplyRepoOverrides(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
config *AppConfig
|
||||
@@ -640,6 +592,7 @@ func TestConfigurationLoader_ApplyRepoOverrides(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -653,6 +606,7 @@ func TestConfigurationLoader_ApplyRepoOverrides(t *testing.T) {
|
||||
|
||||
// TestConfigurationLoader_LoadActionConfig tests action-specific configuration loading.
|
||||
func TestConfigurationLoader_LoadActionConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T, tmpDir string) string
|
||||
@@ -670,6 +624,7 @@ func TestConfigurationLoader_LoadActionConfig(t *testing.T) {
|
||||
{
|
||||
name: "action directory with config file",
|
||||
setupFunc: func(t *testing.T, tmpDir string) string {
|
||||
t.Helper()
|
||||
actionDir := filepath.Join(tmpDir, "action")
|
||||
_ = os.MkdirAll(actionDir, 0750) // #nosec G301 -- test directory permissions
|
||||
|
||||
@@ -679,6 +634,7 @@ theme: minimal
|
||||
output_format: json
|
||||
verbose: true
|
||||
`)
|
||||
|
||||
return actionDir
|
||||
},
|
||||
expectError: false,
|
||||
@@ -690,11 +646,13 @@ verbose: true
|
||||
{
|
||||
name: "action directory with malformed config file",
|
||||
setupFunc: func(t *testing.T, tmpDir string) string {
|
||||
t.Helper()
|
||||
actionDir := filepath.Join(tmpDir, "action")
|
||||
_ = os.MkdirAll(actionDir, 0750) // #nosec G301 -- test directory permissions
|
||||
|
||||
configPath := filepath.Join(actionDir, "config.yaml")
|
||||
testutil.WriteTestFile(t, configPath, "invalid yaml content:\n - broken [")
|
||||
|
||||
return actionDir
|
||||
},
|
||||
expectError: false, // Function may handle YAML errors gracefully
|
||||
@@ -705,6 +663,7 @@ verbose: true
|
||||
setupFunc: func(_ *testing.T, tmpDir string) string {
|
||||
actionDir := filepath.Join(tmpDir, "action")
|
||||
_ = os.MkdirAll(actionDir, 0750) // #nosec G301 -- test directory permissions
|
||||
|
||||
return actionDir
|
||||
},
|
||||
expectError: false,
|
||||
@@ -714,6 +673,7 @@ verbose: true
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -745,6 +705,7 @@ verbose: true
|
||||
|
||||
// TestConfigurationLoader_ValidateTheme tests theme validation edge cases.
|
||||
func TestConfigurationLoader_ValidateTheme(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
theme string
|
||||
@@ -789,6 +750,7 @@ func TestConfigurationLoader_ValidateTheme(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
loader := NewConfigurationLoader()
|
||||
err := loader.validateTheme(tt.theme)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package dependencies
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
@@ -145,7 +146,7 @@ func (a *Analyzer) AnalyzeActionFileWithProgress(
|
||||
progressCallback func(current, total int, message string),
|
||||
) ([]Dependency, error) {
|
||||
if progressCallback != nil {
|
||||
progressCallback(0, 1, fmt.Sprintf("Parsing %s", actionPath))
|
||||
progressCallback(0, 1, "Parsing "+actionPath)
|
||||
}
|
||||
|
||||
// Read and parse the action.yml file
|
||||
@@ -179,8 +180,10 @@ func (a *Analyzer) validateAndCheckComposite(
|
||||
if progressCallback != nil {
|
||||
progressCallback(1, 1, "No dependencies (non-composite action)")
|
||||
}
|
||||
|
||||
return []Dependency{}, false, nil
|
||||
}
|
||||
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
@@ -192,6 +195,7 @@ func (a *Analyzer) validateActionType(usingType string) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("invalid action runtime: %s", usingType)
|
||||
}
|
||||
|
||||
@@ -230,11 +234,13 @@ func (a *Analyzer) processStep(step CompositeStep, stepNumber int) *Dependency {
|
||||
// Log error but continue processing
|
||||
return nil
|
||||
}
|
||||
|
||||
return dep
|
||||
} else if step.Run != "" {
|
||||
// This is a shell script step
|
||||
return a.analyzeShellScript(step, stepNumber)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -361,6 +367,7 @@ func (a *Analyzer) parseUsesStatement(uses string) (owner, repo, version string,
|
||||
func (a *Analyzer) isCommitSHA(version string) bool {
|
||||
// Check if it's a 40-character hex string (full SHA) or 7+ character hex (short SHA)
|
||||
re := regexp.MustCompile(`^[a-f0-9]{7,40}$`)
|
||||
|
||||
return len(version) >= minSHALength && re.MatchString(version)
|
||||
}
|
||||
|
||||
@@ -368,6 +375,7 @@ func (a *Analyzer) isCommitSHA(version string) bool {
|
||||
func (a *Analyzer) isSemanticVersion(version string) bool {
|
||||
// Check for vX, vX.Y, vX.Y.Z format
|
||||
re := regexp.MustCompile(`^v?\d+(\.\d+)*(\.\d+)?(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$`)
|
||||
|
||||
return re.MatchString(version)
|
||||
}
|
||||
|
||||
@@ -379,6 +387,7 @@ func (a *Analyzer) isVersionPinned(version string) bool {
|
||||
return true
|
||||
}
|
||||
re := regexp.MustCompile(`^v?\d+\.\d+\.\d+`)
|
||||
|
||||
return re.MatchString(version)
|
||||
}
|
||||
|
||||
@@ -392,6 +401,7 @@ func (a *Analyzer) convertWithParams(with map[string]any) map[string]string {
|
||||
params[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
@@ -432,7 +442,7 @@ func (a *Analyzer) CheckOutdated(deps []Dependency) ([]OutdatedDependency, error
|
||||
// getLatestVersion fetches the latest release/tag for a repository.
|
||||
func (a *Analyzer) getLatestVersion(owner, repo string) (version, sha string, err error) {
|
||||
if a.GitHubClient == nil {
|
||||
return "", "", fmt.Errorf("GitHub client not available")
|
||||
return "", "", errors.New("GitHub client not available")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), apiCallTimeout)
|
||||
@@ -447,6 +457,7 @@ func (a *Analyzer) getLatestVersion(owner, repo string) (version, sha string, er
|
||||
// Try to get latest release first
|
||||
if version, sha, err := a.getLatestRelease(ctx, owner, repo); err == nil {
|
||||
a.cacheVersion(cacheKey, version, sha)
|
||||
|
||||
return version, sha, nil
|
||||
}
|
||||
|
||||
@@ -457,6 +468,7 @@ func (a *Analyzer) getLatestVersion(owner, repo string) (version, sha string, er
|
||||
}
|
||||
|
||||
a.cacheVersion(cacheKey, version, sha)
|
||||
|
||||
return version, sha, nil
|
||||
}
|
||||
|
||||
@@ -483,11 +495,12 @@ func (a *Analyzer) getCachedVersion(cacheKey string) (version, sha string, found
|
||||
func (a *Analyzer) getLatestRelease(ctx context.Context, owner, repo string) (version, sha string, err error) {
|
||||
release, _, err := a.GitHubClient.Repositories.GetLatestRelease(ctx, owner, repo)
|
||||
if err != nil || release.GetTagName() == "" {
|
||||
return "", "", fmt.Errorf("no release found")
|
||||
return "", "", errors.New("no release found")
|
||||
}
|
||||
|
||||
version = release.GetTagName()
|
||||
sha = a.getCommitSHAForTag(ctx, owner, repo, version)
|
||||
|
||||
return version, sha, nil
|
||||
}
|
||||
|
||||
@@ -497,6 +510,7 @@ func (a *Analyzer) getCommitSHAForTag(ctx context.Context, owner, repo, tagName
|
||||
if err != nil || tag.GetObject() == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return tag.GetObject().GetSHA()
|
||||
}
|
||||
|
||||
@@ -506,10 +520,11 @@ func (a *Analyzer) getLatestTag(ctx context.Context, owner, repo string) (versio
|
||||
PerPage: 10,
|
||||
})
|
||||
if err != nil || len(tags) == 0 {
|
||||
return "", "", fmt.Errorf("no releases or tags found")
|
||||
return "", "", errors.New("no releases or tags found")
|
||||
}
|
||||
|
||||
latestTag := tags[0]
|
||||
|
||||
return latestTag.GetName(), latestTag.GetCommit().GetSHA(), nil
|
||||
}
|
||||
|
||||
@@ -550,6 +565,7 @@ func (a *Analyzer) parseVersionParts(version string) []string {
|
||||
for len(parts) < versionPartsCount {
|
||||
parts = append(parts, "0")
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
@@ -564,6 +580,7 @@ func (a *Analyzer) determineUpdateType(currentParts, latestParts []string) strin
|
||||
if currentParts[2] != latestParts[2] {
|
||||
return updateTypePatch
|
||||
}
|
||||
|
||||
return updateTypeNone
|
||||
}
|
||||
|
||||
@@ -636,6 +653,7 @@ func (a *Analyzer) updateActionFile(filePath string, updates []PinnedUpdate) err
|
||||
indent := strings.Repeat(" ", len(line)-len(strings.TrimLeft(line, " ")))
|
||||
lines[i] = indent + usesFieldPrefix + update.NewUses
|
||||
update.LineNumber = i + 1 // Store line number for reference
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -652,8 +670,9 @@ func (a *Analyzer) updateActionFile(filePath string, updates []PinnedUpdate) err
|
||||
if err := a.validateActionFile(filePath); err != nil {
|
||||
// Rollback on validation failure
|
||||
if rollbackErr := os.Rename(backupPath, filePath); rollbackErr != nil {
|
||||
return fmt.Errorf("validation failed and rollback failed: %v (original error: %w)", rollbackErr, err)
|
||||
return fmt.Errorf("validation failed and rollback failed: %w (original error: %w)", rollbackErr, err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("validation failed, rolled back changes: %w", err)
|
||||
}
|
||||
|
||||
@@ -666,6 +685,7 @@ func (a *Analyzer) updateActionFile(filePath string, updates []PinnedUpdate) err
|
||||
// validateActionFile validates that an action.yml file is still valid after updates.
|
||||
func (a *Analyzer) validateActionFile(filePath string) error {
|
||||
_, err := a.parseCompositeAction(filePath)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -680,6 +700,7 @@ func (a *Analyzer) enrichWithGitHubData(dep *Dependency, owner, repo string) err
|
||||
if cached, exists := a.Cache.Get(cacheKey); exists {
|
||||
if repository, ok := cached.(*github.Repository); ok {
|
||||
dep.Description = repository.GetDescription()
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package dependencies
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -16,6 +16,8 @@ import (
|
||||
)
|
||||
|
||||
func TestAnalyzer_AnalyzeActionFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
actionYML string
|
||||
@@ -62,6 +64,8 @@ func TestAnalyzer_AnalyzeActionFile(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create temporary action file
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
@@ -85,6 +89,7 @@ func TestAnalyzer_AnalyzeActionFile(t *testing.T) {
|
||||
// Check error expectation
|
||||
if tt.expectError {
|
||||
testutil.AssertError(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
testutil.AssertNoError(t, err)
|
||||
@@ -100,6 +105,7 @@ func TestAnalyzer_AnalyzeActionFile(t *testing.T) {
|
||||
for i, expectedDep := range tt.expectedDeps {
|
||||
if i >= len(deps) {
|
||||
t.Errorf("expected dependency %s but got fewer dependencies", expectedDep)
|
||||
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(deps[i].Name+"@"+deps[i].Version, expectedDep) {
|
||||
@@ -115,6 +121,8 @@ func TestAnalyzer_AnalyzeActionFile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAnalyzer_ParseUsesStatement(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
uses string
|
||||
@@ -161,6 +169,8 @@ func TestAnalyzer_ParseUsesStatement(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
owner, repo, version, versionType := analyzer.parseUsesStatement(tt.uses)
|
||||
|
||||
testutil.AssertEqual(t, tt.expectedOwner, owner)
|
||||
@@ -172,6 +182,8 @@ func TestAnalyzer_ParseUsesStatement(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAnalyzer_VersionChecking(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
version string
|
||||
@@ -227,6 +239,8 @@ func TestAnalyzer_VersionChecking(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
isPinned := analyzer.isVersionPinned(tt.version)
|
||||
isCommitSHA := analyzer.isCommitSHA(tt.version)
|
||||
isSemantic := analyzer.isSemanticVersion(tt.version)
|
||||
@@ -239,6 +253,8 @@ func TestAnalyzer_VersionChecking(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAnalyzer_GetLatestVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create mock GitHub client with test responses
|
||||
mockResponses := testutil.MockGitHubResponses()
|
||||
githubClient := testutil.MockGitHubClient(mockResponses)
|
||||
@@ -277,10 +293,13 @@ func TestAnalyzer_GetLatestVersion(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
version, sha, err := analyzer.getLatestVersion(tt.owner, tt.repo)
|
||||
|
||||
if tt.expectError {
|
||||
testutil.AssertError(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -292,6 +311,8 @@ func TestAnalyzer_GetLatestVersion(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAnalyzer_CheckOutdated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create mock GitHub client
|
||||
mockResponses := testutil.MockGitHubResponses()
|
||||
githubClient := testutil.MockGitHubClient(mockResponses)
|
||||
@@ -349,6 +370,8 @@ func TestAnalyzer_CheckOutdated(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAnalyzer_CompareVersions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
analyzer := &Analyzer{}
|
||||
|
||||
tests := []struct {
|
||||
@@ -391,6 +414,8 @@ func TestAnalyzer_CompareVersions(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
updateType := analyzer.compareVersions(tt.current, tt.latest)
|
||||
testutil.AssertEqual(t, tt.expectedType, updateType)
|
||||
})
|
||||
@@ -398,6 +423,8 @@ func TestAnalyzer_CompareVersions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAnalyzer_GeneratePinnedUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -446,6 +473,8 @@ func TestAnalyzer_GeneratePinnedUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAnalyzer_WithCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Test that caching works properly
|
||||
mockResponses := testutil.MockGitHubResponses()
|
||||
githubClient := testutil.MockGitHubClient(mockResponses)
|
||||
@@ -470,12 +499,14 @@ func TestAnalyzer_WithCache(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAnalyzer_RateLimitHandling(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create mock client that returns rate limit error
|
||||
rateLimitResponse := &http.Response{
|
||||
StatusCode: 403,
|
||||
StatusCode: http.StatusForbidden,
|
||||
Header: http.Header{
|
||||
"X-RateLimit-Remaining": []string{"0"},
|
||||
"X-RateLimit-Reset": []string{fmt.Sprintf("%d", time.Now().Add(time.Hour).Unix())},
|
||||
"X-RateLimit-Reset": []string{strconv.FormatInt(time.Now().Add(time.Hour).Unix(), 10)},
|
||||
},
|
||||
Body: testutil.NewStringReader(`{"message": "API rate limit exceeded"}`),
|
||||
}
|
||||
@@ -508,6 +539,8 @@ func TestAnalyzer_RateLimitHandling(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAnalyzer_WithoutGitHubClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Test graceful degradation when GitHub client is not available
|
||||
analyzer := &Analyzer{
|
||||
GitHubClient: nil,
|
||||
@@ -546,6 +579,8 @@ func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
|
||||
// TestNewAnalyzer tests the analyzer constructor.
|
||||
func TestNewAnalyzer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create test dependencies
|
||||
mockResponses := testutil.MockGitHubResponses()
|
||||
githubClient := testutil.MockGitHubClient(mockResponses)
|
||||
@@ -597,6 +632,8 @@ func TestNewAnalyzer(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
analyzer := NewAnalyzer(tt.client, tt.repoInfo, tt.cache)
|
||||
|
||||
if tt.expectNotNil && analyzer == nil {
|
||||
|
||||
@@ -65,13 +65,13 @@ func (ce *ContextualError) Error() string {
|
||||
if len(ce.Suggestions) > 0 {
|
||||
b.WriteString("\n\nSuggestions:")
|
||||
for _, suggestion := range ce.Suggestions {
|
||||
b.WriteString(fmt.Sprintf("\n • %s", suggestion))
|
||||
b.WriteString("\n • " + suggestion)
|
||||
}
|
||||
}
|
||||
|
||||
// Add help URL
|
||||
if ce.HelpURL != "" {
|
||||
b.WriteString(fmt.Sprintf("\n\nFor more help: %s", ce.HelpURL))
|
||||
b.WriteString("\n\nFor more help: " + ce.HelpURL)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
@@ -120,6 +120,7 @@ func Wrap(err error, code ErrorCode, context string) *ContextualError {
|
||||
if ce.Context == "" {
|
||||
ce.Context = context
|
||||
}
|
||||
|
||||
return ce
|
||||
}
|
||||
|
||||
@@ -133,6 +134,7 @@ func Wrap(err error, code ErrorCode, context string) *ContextualError {
|
||||
// WithSuggestions adds suggestions to a ContextualError.
|
||||
func (ce *ContextualError) WithSuggestions(suggestions ...string) *ContextualError {
|
||||
ce.Suggestions = append(ce.Suggestions, suggestions...)
|
||||
|
||||
return ce
|
||||
}
|
||||
|
||||
@@ -144,12 +146,14 @@ func (ce *ContextualError) WithDetails(details map[string]string) *ContextualErr
|
||||
for k, v := range details {
|
||||
ce.Details[k] = v
|
||||
}
|
||||
|
||||
return ce
|
||||
}
|
||||
|
||||
// WithHelpURL adds a help URL to a ContextualError.
|
||||
func (ce *ContextualError) WithHelpURL(url string) *ContextualError {
|
||||
ce.HelpURL = url
|
||||
|
||||
return ce
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
)
|
||||
|
||||
func TestContextualError_Error(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err *ContextualError
|
||||
@@ -103,6 +105,8 @@ func TestContextualError_Error(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := tt.err.Error()
|
||||
|
||||
for _, expected := range tt.contains {
|
||||
@@ -119,6 +123,8 @@ func TestContextualError_Error(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestContextualError_Unwrap(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
originalErr := errors.New("original error")
|
||||
contextualErr := &ContextualError{
|
||||
Code: ErrCodeFileNotFound,
|
||||
@@ -131,6 +137,8 @@ func TestContextualError_Unwrap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestContextualError_Is(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
originalErr := errors.New("original error")
|
||||
contextualErr := &ContextualError{
|
||||
Code: ErrCodeFileNotFound,
|
||||
@@ -156,6 +164,8 @@ func TestContextualError_Is(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := New(ErrCodeFileNotFound, "test message")
|
||||
|
||||
if err.Code != ErrCodeFileNotFound {
|
||||
@@ -168,6 +178,8 @@ func TestNew(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWrap(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
originalErr := errors.New("original error")
|
||||
|
||||
// Test wrapping normal error
|
||||
@@ -204,6 +216,8 @@ func TestWrap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestContextualError_WithMethods(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := New(ErrCodeFileNotFound, "test error")
|
||||
|
||||
// Test WithSuggestions
|
||||
@@ -234,6 +248,8 @@ func TestContextualError_WithMethods(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetHelpURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
code ErrorCode
|
||||
contains string
|
||||
@@ -246,6 +262,8 @@ func TestGetHelpURL(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.code), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
url := GetHelpURL(tt.code)
|
||||
if !strings.Contains(url, tt.contains) {
|
||||
t.Errorf("GetHelpURL(%s) = %s, should contain %s", tt.code, url, tt.contains)
|
||||
|
||||
@@ -13,6 +13,7 @@ func GetSuggestions(code ErrorCode, context map[string]string) []string {
|
||||
if handler := getSuggestionHandler(code); handler != nil {
|
||||
return handler(context)
|
||||
}
|
||||
|
||||
return getDefaultSuggestions()
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ func getFileNotFoundSuggestions(context map[string]string) []string {
|
||||
|
||||
if path, ok := context["path"]; ok {
|
||||
suggestions = append(suggestions,
|
||||
fmt.Sprintf("Check if the file exists: %s", path),
|
||||
"Check if the file exists: "+path,
|
||||
"Verify the file path is correct",
|
||||
)
|
||||
|
||||
@@ -72,7 +73,7 @@ func getFileNotFoundSuggestions(context map[string]string) []string {
|
||||
if _, err := os.Stat(dir); err == nil {
|
||||
suggestions = append(suggestions,
|
||||
"Check for case sensitivity in the filename",
|
||||
fmt.Sprintf("Try: ls -la %s", dir),
|
||||
"Try: ls -la "+dir,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -98,14 +99,14 @@ func getPermissionSuggestions(context map[string]string) []string {
|
||||
|
||||
if path, ok := context["path"]; ok {
|
||||
suggestions = append(suggestions,
|
||||
fmt.Sprintf("Check file permissions: ls -la %s", path),
|
||||
fmt.Sprintf("Try changing permissions: chmod 644 %s", path),
|
||||
"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,
|
||||
fmt.Sprintf("For directories, try: chmod 755 %s", path),
|
||||
"For directories, try: chmod 755 "+path,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -168,7 +169,7 @@ func getInvalidActionSuggestions(context map[string]string) []string {
|
||||
|
||||
if missingFields, ok := context["missing_fields"]; ok {
|
||||
suggestions = append([]string{
|
||||
fmt.Sprintf("Missing required fields: %s", missingFields),
|
||||
"Missing required fields: " + missingFields,
|
||||
}, suggestions...)
|
||||
}
|
||||
|
||||
@@ -196,7 +197,7 @@ func getNoActionFilesSuggestions(context map[string]string) []string {
|
||||
|
||||
if dir, ok := context["directory"]; ok {
|
||||
suggestions = append(suggestions,
|
||||
fmt.Sprintf("Current directory: %s", dir),
|
||||
"Current directory: "+dir,
|
||||
fmt.Sprintf("Try: find %s -name 'action.y*ml' -type f", dir),
|
||||
)
|
||||
}
|
||||
@@ -274,8 +275,8 @@ func getConfigurationSuggestions(context map[string]string) []string {
|
||||
|
||||
if configPath, ok := context["config_path"]; ok {
|
||||
suggestions = append(suggestions,
|
||||
fmt.Sprintf("Config path: %s", configPath),
|
||||
fmt.Sprintf("Check if file exists: ls -la %s", configPath),
|
||||
"Config path: "+configPath,
|
||||
"Check if file exists: ls -la "+configPath,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -301,7 +302,7 @@ func getValidationSuggestions(context map[string]string) []string {
|
||||
|
||||
if fields, ok := context["invalid_fields"]; ok {
|
||||
suggestions = append([]string{
|
||||
fmt.Sprintf("Invalid fields: %s", fields),
|
||||
"Invalid fields: " + fields,
|
||||
"Check spelling and nesting of these fields",
|
||||
}, suggestions...)
|
||||
}
|
||||
@@ -333,14 +334,14 @@ func getTemplateSuggestions(context map[string]string) []string {
|
||||
|
||||
if templatePath, ok := context["template_path"]; ok {
|
||||
suggestions = append(suggestions,
|
||||
fmt.Sprintf("Template path: %s", templatePath),
|
||||
"Template path: "+templatePath,
|
||||
"Ensure template file exists and is readable",
|
||||
)
|
||||
}
|
||||
|
||||
if theme, ok := context["theme"]; ok {
|
||||
suggestions = append(suggestions,
|
||||
fmt.Sprintf("Current theme: %s", theme),
|
||||
"Current theme: "+theme,
|
||||
"Try using a different theme: --theme github",
|
||||
"Available themes: default, github, gitlab, minimal, professional",
|
||||
)
|
||||
@@ -359,9 +360,9 @@ func getFileWriteSuggestions(context map[string]string) []string {
|
||||
if outputPath, ok := context["output_path"]; ok {
|
||||
dir := filepath.Dir(outputPath)
|
||||
suggestions = append(suggestions,
|
||||
fmt.Sprintf("Output directory: %s", dir),
|
||||
fmt.Sprintf("Check permissions: ls -la %s", dir),
|
||||
fmt.Sprintf("Create directory if needed: mkdir -p %s", dir),
|
||||
"Output directory: "+dir,
|
||||
"Check permissions: ls -la "+dir,
|
||||
"Create directory if needed: mkdir -p "+dir,
|
||||
)
|
||||
|
||||
// Check if file already exists
|
||||
@@ -385,7 +386,7 @@ func getDependencyAnalysisSuggestions(context map[string]string) []string {
|
||||
|
||||
if action, ok := context["action"]; ok {
|
||||
suggestions = append(suggestions,
|
||||
fmt.Sprintf("Analyzing action: %s", action),
|
||||
"Analyzing action: "+action,
|
||||
"Only composite actions have analyzable dependencies",
|
||||
)
|
||||
}
|
||||
@@ -406,8 +407,8 @@ func getCacheAccessSuggestions(context map[string]string) []string {
|
||||
|
||||
if cachePath, ok := context["cache_path"]; ok {
|
||||
suggestions = append(suggestions,
|
||||
fmt.Sprintf("Cache path: %s", cachePath),
|
||||
fmt.Sprintf("Check permissions: ls -la %s", cachePath),
|
||||
"Cache path: "+cachePath,
|
||||
"Check permissions: ls -la "+cachePath,
|
||||
"You can disable cache temporarily with environment variables",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
)
|
||||
|
||||
func TestGetSuggestions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
code ErrorCode
|
||||
@@ -239,10 +241,13 @@ func TestGetSuggestions(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
suggestions := GetSuggestions(tt.code, tt.context)
|
||||
|
||||
if len(suggestions) == 0 {
|
||||
t.Error("GetSuggestions() returned empty slice")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -261,6 +266,8 @@ func TestGetSuggestions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetPermissionSuggestions_OSSpecific(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
context := map[string]string{"path": "/test/file"}
|
||||
suggestions := getPermissionSuggestions(context)
|
||||
|
||||
@@ -285,6 +292,8 @@ func TestGetPermissionSuggestions_OSSpecific(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetSuggestions_EmptyContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Test that all error codes work with empty context
|
||||
errorCodes := []ErrorCode{
|
||||
ErrCodeFileNotFound,
|
||||
@@ -305,6 +314,8 @@ func TestGetSuggestions_EmptyContext(t *testing.T) {
|
||||
|
||||
for _, code := range errorCodes {
|
||||
t.Run(string(code), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
suggestions := GetSuggestions(code, map[string]string{})
|
||||
if len(suggestions) == 0 {
|
||||
t.Errorf("GetSuggestions(%s, {}) returned empty slice", code)
|
||||
@@ -314,6 +325,8 @@ func TestGetSuggestions_EmptyContext(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetFileNotFoundSuggestions_ActionFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
context := map[string]string{
|
||||
"path": "/project/action.yml",
|
||||
}
|
||||
@@ -332,6 +345,8 @@ func TestGetFileNotFoundSuggestions_ActionFile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetInvalidYAMLSuggestions_TabError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
context := map[string]string{
|
||||
"error": "found character that cannot start any token, tab character",
|
||||
}
|
||||
@@ -346,6 +361,8 @@ func TestGetInvalidYAMLSuggestions_TabError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetGitHubAPISuggestions_StatusCodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
statusCodes := map[string]string{
|
||||
"401": "Authentication failed",
|
||||
"403": "Access forbidden",
|
||||
@@ -354,6 +371,8 @@ func TestGetGitHubAPISuggestions_StatusCodes(t *testing.T) {
|
||||
|
||||
for code, expectedText := range statusCodes {
|
||||
t.Run("status_"+code, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
context := map[string]string{"status_code": code}
|
||||
suggestions := getGitHubAPISuggestions(context)
|
||||
allSuggestions := strings.Join(suggestions, " ")
|
||||
|
||||
@@ -51,7 +51,7 @@ func (fem *FocusedErrorManager) HandleValidationError(file string, missingFields
|
||||
|
||||
fem.manager.ErrorWithContext(
|
||||
errors.ErrCodeValidation,
|
||||
fmt.Sprintf("Validation failed for %s", file),
|
||||
"Validation failed for "+file,
|
||||
context,
|
||||
)
|
||||
}
|
||||
@@ -133,6 +133,7 @@ func NewValidationComponent(errorManager ErrorManager, logger MessageLogger) *Va
|
||||
func (vc *ValidationComponent) ValidateAndReport(item string, isValid bool, err error) {
|
||||
if isValid {
|
||||
vc.logger.Success("Validation passed for: %s", item)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -144,7 +145,7 @@ func (vc *ValidationComponent) ValidateAndReport(item string, isValid bool, err
|
||||
}
|
||||
} else {
|
||||
vc.errorManager.ErrorWithSimpleFix(
|
||||
fmt.Sprintf("Validation failed for %s", item),
|
||||
"Validation failed for "+item,
|
||||
"Please check the item configuration and try again",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-github/v57/github"
|
||||
@@ -12,7 +14,7 @@ import (
|
||||
|
||||
"github.com/ivuorinen/gh-action-readme/internal/cache"
|
||||
"github.com/ivuorinen/gh-action-readme/internal/dependencies"
|
||||
"github.com/ivuorinen/gh-action-readme/internal/errors"
|
||||
errCodes "github.com/ivuorinen/gh-action-readme/internal/errors"
|
||||
"github.com/ivuorinen/gh-action-readme/internal/git"
|
||||
)
|
||||
|
||||
@@ -109,6 +111,7 @@ func (g *Generator) GenerateFromFile(actionPath string) error {
|
||||
}
|
||||
|
||||
outputDir := g.determineOutputDir(actionPath)
|
||||
|
||||
return g.generateByFormat(action, outputDir, actionPath)
|
||||
}
|
||||
|
||||
@@ -151,6 +154,7 @@ func (g *Generator) determineOutputDir(actionPath string) string {
|
||||
if g.Config.OutputDir == "" || g.Config.OutputDir == "." {
|
||||
return filepath.Dir(actionPath)
|
||||
}
|
||||
|
||||
return g.Config.OutputDir
|
||||
}
|
||||
|
||||
@@ -160,8 +164,10 @@ func (g *Generator) resolveOutputPath(outputDir, defaultFilename string) string
|
||||
if filepath.IsAbs(g.Config.OutputFilename) {
|
||||
return g.Config.OutputFilename
|
||||
}
|
||||
|
||||
return filepath.Join(outputDir, g.Config.OutputFilename)
|
||||
}
|
||||
|
||||
return filepath.Join(outputDir, defaultFilename)
|
||||
}
|
||||
|
||||
@@ -212,6 +218,7 @@ func (g *Generator) generateMarkdown(action *ActionYML, outputDir, actionPath st
|
||||
}
|
||||
|
||||
g.Output.Success("Generated README.md: %s", outputPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -254,6 +261,7 @@ func (g *Generator) generateHTML(action *ActionYML, outputDir, actionPath string
|
||||
}
|
||||
|
||||
g.Output.Success("Generated HTML: %s", outputPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -267,6 +275,7 @@ func (g *Generator) generateJSON(action *ActionYML, outputDir string) error {
|
||||
}
|
||||
|
||||
g.Output.Success("Generated JSON: %s", outputPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -298,6 +307,7 @@ func (g *Generator) generateASCIIDoc(action *ActionYML, outputDir, actionPath st
|
||||
}
|
||||
|
||||
g.Output.Success("Generated AsciiDoc: %s", outputPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -330,31 +340,33 @@ func (g *Generator) DiscoverActionFilesWithValidation(dir string, recursive bool
|
||||
actionFiles, err := g.DiscoverActionFiles(dir, recursive)
|
||||
if err != nil {
|
||||
g.Output.ErrorWithContext(
|
||||
errors.ErrCodeFileNotFound,
|
||||
fmt.Sprintf("failed to discover action files for %s", context),
|
||||
errCodes.ErrCodeFileNotFound,
|
||||
"failed to discover action files for "+context,
|
||||
map[string]string{
|
||||
"directory": dir,
|
||||
"recursive": fmt.Sprintf("%t", recursive),
|
||||
"recursive": strconv.FormatBool(recursive),
|
||||
"context": context,
|
||||
ContextKeyError: err.Error(),
|
||||
},
|
||||
)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if any files were found
|
||||
if len(actionFiles) == 0 {
|
||||
contextMsg := fmt.Sprintf("no GitHub Action files found for %s", context)
|
||||
contextMsg := "no GitHub Action files found for " + context
|
||||
g.Output.ErrorWithContext(
|
||||
errors.ErrCodeNoActionFiles,
|
||||
errCodes.ErrCodeNoActionFiles,
|
||||
contextMsg,
|
||||
map[string]string{
|
||||
"directory": dir,
|
||||
"recursive": fmt.Sprintf("%t", recursive),
|
||||
"recursive": strconv.FormatBool(recursive),
|
||||
"context": context,
|
||||
"suggestion": "Please run this command in a directory containing GitHub Action files (action.yml or action.yaml)",
|
||||
},
|
||||
)
|
||||
|
||||
return nil, fmt.Errorf("no action files found in directory: %s", dir)
|
||||
}
|
||||
|
||||
@@ -364,7 +376,7 @@ func (g *Generator) DiscoverActionFilesWithValidation(dir string, recursive bool
|
||||
// ProcessBatch processes multiple action.yml files.
|
||||
func (g *Generator) ProcessBatch(paths []string) error {
|
||||
if len(paths) == 0 {
|
||||
return fmt.Errorf("no action files to process")
|
||||
return errors.New("no action files to process")
|
||||
}
|
||||
|
||||
bar := g.Progress.CreateProgressBarForFiles("Processing files", paths)
|
||||
@@ -375,6 +387,7 @@ func (g *Generator) ProcessBatch(paths []string) error {
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf("encountered %d errors during batch processing", len(errors))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -396,6 +409,7 @@ func (g *Generator) processFiles(paths []string, bar *progressbar.ProgressBar) (
|
||||
|
||||
g.Progress.UpdateProgressBar(bar)
|
||||
}
|
||||
|
||||
return errors, successCount
|
||||
}
|
||||
|
||||
@@ -418,7 +432,7 @@ func (g *Generator) reportResults(successCount int, errors []string) {
|
||||
// ValidateFiles validates multiple action.yml files and reports results.
|
||||
func (g *Generator) ValidateFiles(paths []string) error {
|
||||
if len(paths) == 0 {
|
||||
return fmt.Errorf("no action files to validate")
|
||||
return errors.New("no action files to validate")
|
||||
}
|
||||
|
||||
bar := g.Progress.CreateProgressBarForFiles("Validating files", paths)
|
||||
@@ -440,8 +454,10 @@ func (g *Generator) ValidateFiles(paths []string) error {
|
||||
|
||||
if len(errors) > 0 || validationFailures > 0 {
|
||||
totalFailures := len(errors) + validationFailures
|
||||
|
||||
return fmt.Errorf("validation failed for %d files", totalFailures)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -459,15 +475,17 @@ func (g *Generator) validateFiles(paths []string, bar *progressbar.ProgressBar)
|
||||
if err != nil {
|
||||
errorMsg := fmt.Sprintf("failed to parse %s: %v", path, err)
|
||||
errors = append(errors, errorMsg)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
result := ValidateActionYML(action)
|
||||
result.MissingFields = append([]string{fmt.Sprintf("file: %s", path)}, result.MissingFields...)
|
||||
result.MissingFields = append([]string{"file: " + path}, result.MissingFields...)
|
||||
allResults = append(allResults, result)
|
||||
|
||||
g.Progress.UpdateProgressBar(bar)
|
||||
}
|
||||
|
||||
return allResults, errors
|
||||
}
|
||||
|
||||
@@ -490,6 +508,7 @@ func (g *Generator) countValidationStats(results []ValidationResult) (validFiles
|
||||
totalIssues += len(result.MissingFields) - 1 // Subtract file path entry
|
||||
}
|
||||
}
|
||||
|
||||
return validFiles, totalIssues
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@ package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/ivuorinen/gh-action-readme/testutil"
|
||||
@@ -13,6 +11,7 @@ import (
|
||||
// TestGenerator_ComprehensiveGeneration demonstrates the new table-driven testing framework
|
||||
// by testing generation across all fixtures, themes, and formats systematically.
|
||||
func TestGenerator_ComprehensiveGeneration(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Create test cases using the new helper functions
|
||||
cases := testutil.CreateGeneratorTestCases()
|
||||
|
||||
@@ -35,6 +34,7 @@ func TestGenerator_ComprehensiveGeneration(t *testing.T) {
|
||||
|
||||
// TestGenerator_AllValidFixtures tests generation with all valid fixtures.
|
||||
func TestGenerator_AllValidFixtures(t *testing.T) {
|
||||
t.Parallel()
|
||||
validFixtures := testutil.GetValidFixtures()
|
||||
|
||||
for _, fixture := range validFixtures {
|
||||
@@ -66,6 +66,7 @@ func TestGenerator_AllValidFixtures(t *testing.T) {
|
||||
|
||||
// TestGenerator_AllInvalidFixtures tests that invalid fixtures produce expected errors.
|
||||
func TestGenerator_AllInvalidFixtures(t *testing.T) {
|
||||
t.Parallel()
|
||||
invalidFixtures := testutil.GetInvalidFixtures()
|
||||
|
||||
for _, fixture := range invalidFixtures {
|
||||
@@ -107,8 +108,10 @@ func TestGenerator_AllInvalidFixtures(t *testing.T) {
|
||||
|
||||
// TestGenerator_AllThemes demonstrates theme testing using helper functions.
|
||||
func TestGenerator_AllThemes(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Use the helper function to test all themes
|
||||
testutil.TestAllThemes(t, func(t *testing.T, theme string) {
|
||||
t.Helper()
|
||||
// Create a simple action for testing
|
||||
actionPath := testutil.CreateTemporaryAction(t, "actions/javascript/simple.yml")
|
||||
|
||||
@@ -128,8 +131,10 @@ func TestGenerator_AllThemes(t *testing.T) {
|
||||
|
||||
// TestGenerator_AllFormats demonstrates format testing using helper functions.
|
||||
func TestGenerator_AllFormats(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Use the helper function to test all formats
|
||||
testutil.TestAllFormats(t, func(t *testing.T, format string) {
|
||||
t.Helper()
|
||||
// Create a simple action for testing
|
||||
actionPath := testutil.CreateTemporaryAction(t, "actions/javascript/simple.yml")
|
||||
|
||||
@@ -149,6 +154,7 @@ func TestGenerator_AllFormats(t *testing.T) {
|
||||
|
||||
// TestGenerator_ByActionType demonstrates testing by action type.
|
||||
func TestGenerator_ByActionType(t *testing.T) {
|
||||
t.Parallel()
|
||||
actionTypes := []testutil.ActionType{
|
||||
testutil.ActionTypeJavaScript,
|
||||
testutil.ActionTypeComposite,
|
||||
@@ -186,6 +192,7 @@ func TestGenerator_ByActionType(t *testing.T) {
|
||||
|
||||
// TestGenerator_WithMockEnvironment demonstrates testing with a complete mock environment.
|
||||
func TestGenerator_WithMockEnvironment(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Create a complete test environment
|
||||
envConfig := &testutil.EnvironmentConfig{
|
||||
ActionFixtures: []string{"actions/composite/with-dependencies.yml"},
|
||||
@@ -222,6 +229,7 @@ func TestGenerator_WithMockEnvironment(t *testing.T) {
|
||||
|
||||
// TestGenerator_FixtureValidation demonstrates fixture validation.
|
||||
func TestGenerator_FixtureValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Test that all valid fixtures pass validation
|
||||
validFixtures := testutil.GetValidFixtures()
|
||||
|
||||
@@ -236,6 +244,7 @@ func TestGenerator_FixtureValidation(t *testing.T) {
|
||||
|
||||
for _, fixtureName := range invalidFixtures {
|
||||
t.Run(fixtureName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testutil.AssertFixtureInvalid(t, fixtureName)
|
||||
})
|
||||
}
|
||||
@@ -257,6 +266,7 @@ func createGeneratorTestExecutor() testutil.TestExecutor {
|
||||
fixture, err := ctx.FixtureManager.LoadActionFixture(testCase.Fixture)
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("failed to load fixture %s: %w", testCase.Fixture, err)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -268,48 +278,19 @@ func createGeneratorTestExecutor() testutil.TestExecutor {
|
||||
// If we don't have an action file to test, just return success
|
||||
if actionPath == "" {
|
||||
result.Success = true
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Create generator configuration from test config
|
||||
config := createGeneratorConfigFromTestConfig(ctx.Config, ctx.TempDir)
|
||||
|
||||
// Save current working directory and change to project root for template resolution
|
||||
originalWd, err := os.Getwd()
|
||||
if err != nil {
|
||||
result.Error = fmt.Errorf("failed to get working directory: %w", err)
|
||||
return result
|
||||
}
|
||||
|
||||
// Use runtime.Caller to find project root relative to this file
|
||||
_, currentFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
result.Error = fmt.Errorf("failed to get current file path")
|
||||
return result
|
||||
}
|
||||
|
||||
// Get the project root (go up from internal/generator_comprehensive_test.go to project root)
|
||||
projectRoot := filepath.Dir(filepath.Dir(currentFile))
|
||||
if err := os.Chdir(projectRoot); err != nil {
|
||||
result.Error = fmt.Errorf("failed to change to project root %s: %w", projectRoot, err)
|
||||
return result
|
||||
}
|
||||
|
||||
// Debug: Log the working directory and template path
|
||||
currentWd, _ := os.Getwd()
|
||||
t.Logf("Test working directory: %s, template path: %s", currentWd, config.Template)
|
||||
|
||||
// Restore working directory after test
|
||||
defer func() {
|
||||
if err := os.Chdir(originalWd); err != nil {
|
||||
// Log error but don't fail the test
|
||||
t.Logf("Failed to restore working directory: %v", err)
|
||||
}
|
||||
}()
|
||||
// Debug: Log the template path (no working directory changes needed with embedded templates)
|
||||
t.Logf("Using template path: %s", config.Template)
|
||||
|
||||
// Create and run generator
|
||||
generator := NewGenerator(config)
|
||||
err = generator.GenerateFromFile(actionPath)
|
||||
err := generator.GenerateFromFile(actionPath)
|
||||
|
||||
if err != nil {
|
||||
result.Error = err
|
||||
@@ -352,24 +333,8 @@ func createGeneratorConfigFromTestConfig(testConfig *testutil.TestConfig, output
|
||||
config.Quiet = testConfig.Quiet
|
||||
}
|
||||
|
||||
// Set appropriate template path based on theme and output format
|
||||
config.Template = resolveTemplatePathForTest(config.Theme, config.OutputFormat)
|
||||
// Set appropriate template path based on theme - embedded templates will handle resolution
|
||||
config.Template = resolveThemeTemplate(config.Theme)
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// resolveTemplatePathForTest resolves the correct template path for testing.
|
||||
func resolveTemplatePathForTest(theme, _ string) string {
|
||||
switch theme {
|
||||
case "github":
|
||||
return "templates/themes/github/readme.tmpl"
|
||||
case "gitlab":
|
||||
return "templates/themes/gitlab/readme.tmpl"
|
||||
case "minimal":
|
||||
return "templates/themes/minimal/readme.tmpl"
|
||||
case "professional":
|
||||
return "templates/themes/professional/readme.tmpl"
|
||||
default:
|
||||
return "templates/readme.tmpl"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func TestGenerator_NewGenerator(t *testing.T) {
|
||||
t.Parallel()
|
||||
config := &AppConfig{
|
||||
Theme: "default",
|
||||
OutputFormat: "md",
|
||||
@@ -34,6 +35,7 @@ func TestGenerator_NewGenerator(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerator_DiscoverActionFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T, tmpDir string)
|
||||
@@ -44,6 +46,7 @@ func TestGenerator_DiscoverActionFiles(t *testing.T) {
|
||||
{
|
||||
name: "single action.yml in root",
|
||||
setupFunc: func(t *testing.T, tmpDir string) {
|
||||
t.Helper()
|
||||
fixture, err := testutil.LoadActionFixture("actions/javascript/simple.yml")
|
||||
testutil.AssertNoError(t, err)
|
||||
testutil.WriteTestFile(t, filepath.Join(tmpDir, "action.yml"), fixture.Content)
|
||||
@@ -54,6 +57,7 @@ func TestGenerator_DiscoverActionFiles(t *testing.T) {
|
||||
{
|
||||
name: "action.yaml variant",
|
||||
setupFunc: func(t *testing.T, tmpDir string) {
|
||||
t.Helper()
|
||||
fixture, err := testutil.LoadActionFixture("actions/javascript/simple.yml")
|
||||
testutil.AssertNoError(t, err)
|
||||
testutil.WriteTestFile(t, filepath.Join(tmpDir, "action.yaml"), fixture.Content)
|
||||
@@ -64,6 +68,7 @@ func TestGenerator_DiscoverActionFiles(t *testing.T) {
|
||||
{
|
||||
name: "both yml and yaml files",
|
||||
setupFunc: func(t *testing.T, tmpDir string) {
|
||||
t.Helper()
|
||||
simpleFixture, err := testutil.LoadActionFixture("actions/javascript/simple.yml")
|
||||
testutil.AssertNoError(t, err)
|
||||
minimalFixture, err := testutil.LoadActionFixture("minimal-action.yml")
|
||||
@@ -77,6 +82,7 @@ func TestGenerator_DiscoverActionFiles(t *testing.T) {
|
||||
{
|
||||
name: "recursive discovery",
|
||||
setupFunc: func(t *testing.T, tmpDir string) {
|
||||
t.Helper()
|
||||
simpleFixture, err := testutil.LoadActionFixture("actions/javascript/simple.yml")
|
||||
testutil.AssertNoError(t, err)
|
||||
compositeFixture, err := testutil.LoadActionFixture("actions/composite/basic.yml")
|
||||
@@ -92,6 +98,7 @@ func TestGenerator_DiscoverActionFiles(t *testing.T) {
|
||||
{
|
||||
name: "non-recursive skips subdirectories",
|
||||
setupFunc: func(t *testing.T, tmpDir string) {
|
||||
t.Helper()
|
||||
simpleFixture, err := testutil.LoadActionFixture("actions/javascript/simple.yml")
|
||||
testutil.AssertNoError(t, err)
|
||||
compositeFixture, err := testutil.LoadActionFixture("actions/composite/basic.yml")
|
||||
@@ -107,6 +114,7 @@ func TestGenerator_DiscoverActionFiles(t *testing.T) {
|
||||
{
|
||||
name: "no action files",
|
||||
setupFunc: func(t *testing.T, tmpDir string) {
|
||||
t.Helper()
|
||||
testutil.WriteTestFile(t, filepath.Join(tmpDir, "README.md"), "# Test")
|
||||
},
|
||||
recursive: false,
|
||||
@@ -122,6 +130,7 @@ func TestGenerator_DiscoverActionFiles(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -139,6 +148,7 @@ func TestGenerator_DiscoverActionFiles(t *testing.T) {
|
||||
|
||||
if tt.expectError {
|
||||
testutil.AssertError(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -160,6 +170,7 @@ func TestGenerator_DiscoverActionFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerator_GenerateFromFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
actionYML string
|
||||
@@ -218,6 +229,7 @@ func TestGenerator_GenerateFromFile(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -242,6 +254,7 @@ func TestGenerator_GenerateFromFile(t *testing.T) {
|
||||
|
||||
if tt.expectError {
|
||||
testutil.AssertError(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -260,6 +273,7 @@ func TestGenerator_GenerateFromFile(t *testing.T) {
|
||||
readmeFiles, _ := filepath.Glob(pattern)
|
||||
if len(readmeFiles) == 0 {
|
||||
t.Errorf("no output file was created for format %s", tt.outputFormat)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -289,11 +303,13 @@ func countREADMEFiles(t *testing.T, dir string) int {
|
||||
if strings.HasSuffix(path, "README.md") {
|
||||
count++
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("error walking directory: %v", err)
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -304,11 +320,13 @@ func logREADMELocations(t *testing.T, dir string) {
|
||||
if err == nil && strings.HasSuffix(path, "README.md") {
|
||||
t.Logf("Found README at: %s", path)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestGenerator_ProcessBatch(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T, tmpDir string) []string
|
||||
@@ -318,6 +336,7 @@ func TestGenerator_ProcessBatch(t *testing.T) {
|
||||
{
|
||||
name: "process multiple valid files",
|
||||
setupFunc: func(t *testing.T, tmpDir string) []string {
|
||||
t.Helper()
|
||||
// Create separate directories for each action
|
||||
dir1 := filepath.Join(tmpDir, "action1")
|
||||
dir2 := filepath.Join(tmpDir, "action2")
|
||||
@@ -334,6 +353,7 @@ func TestGenerator_ProcessBatch(t *testing.T) {
|
||||
}
|
||||
testutil.WriteTestFile(t, files[0], testutil.MustReadFixture("actions/javascript/simple.yml"))
|
||||
testutil.WriteTestFile(t, files[1], testutil.MustReadFixture("actions/composite/basic.yml"))
|
||||
|
||||
return files
|
||||
},
|
||||
expectError: false,
|
||||
@@ -342,6 +362,7 @@ func TestGenerator_ProcessBatch(t *testing.T) {
|
||||
{
|
||||
name: "handle mixed valid and invalid files",
|
||||
setupFunc: func(t *testing.T, tmpDir string) []string {
|
||||
t.Helper()
|
||||
// Create separate directories for mixed test too
|
||||
dir1 := filepath.Join(tmpDir, "valid-action")
|
||||
dir2 := filepath.Join(tmpDir, "invalid-action")
|
||||
@@ -358,6 +379,7 @@ func TestGenerator_ProcessBatch(t *testing.T) {
|
||||
}
|
||||
testutil.WriteTestFile(t, files[0], testutil.MustReadFixture("actions/javascript/simple.yml"))
|
||||
testutil.WriteTestFile(t, files[1], testutil.MustReadFixture("actions/invalid/invalid-using.yml"))
|
||||
|
||||
return files
|
||||
},
|
||||
expectError: true, // Invalid runtime configuration should cause batch to fail
|
||||
@@ -382,6 +404,7 @@ func TestGenerator_ProcessBatch(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -401,11 +424,13 @@ func TestGenerator_ProcessBatch(t *testing.T) {
|
||||
|
||||
if tt.expectError {
|
||||
testutil.AssertError(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -423,6 +448,7 @@ func TestGenerator_ProcessBatch(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerator_ValidateFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T, tmpDir string) []string
|
||||
@@ -431,12 +457,14 @@ func TestGenerator_ValidateFiles(t *testing.T) {
|
||||
{
|
||||
name: "all valid files",
|
||||
setupFunc: func(t *testing.T, tmpDir string) []string {
|
||||
t.Helper()
|
||||
files := []string{
|
||||
filepath.Join(tmpDir, "action1.yml"),
|
||||
filepath.Join(tmpDir, "action2.yml"),
|
||||
}
|
||||
testutil.WriteTestFile(t, files[0], testutil.MustReadFixture("actions/javascript/simple.yml"))
|
||||
testutil.WriteTestFile(t, files[1], testutil.MustReadFixture("minimal-action.yml"))
|
||||
|
||||
return files
|
||||
},
|
||||
expectError: false,
|
||||
@@ -444,12 +472,14 @@ func TestGenerator_ValidateFiles(t *testing.T) {
|
||||
{
|
||||
name: "files with validation issues",
|
||||
setupFunc: func(t *testing.T, tmpDir string) []string {
|
||||
t.Helper()
|
||||
files := []string{
|
||||
filepath.Join(tmpDir, "valid.yml"),
|
||||
filepath.Join(tmpDir, "invalid.yml"),
|
||||
}
|
||||
testutil.WriteTestFile(t, files[0], testutil.MustReadFixture("actions/javascript/simple.yml"))
|
||||
testutil.WriteTestFile(t, files[1], testutil.MustReadFixture("actions/invalid/missing-description.yml"))
|
||||
|
||||
return files
|
||||
},
|
||||
expectError: true, // Validation should fail for invalid runtime configuration
|
||||
@@ -465,6 +495,7 @@ func TestGenerator_ValidateFiles(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -484,6 +515,7 @@ func TestGenerator_ValidateFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerator_CreateDependencyAnalyzer(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
token string
|
||||
@@ -503,6 +535,7 @@ func TestGenerator_CreateDependencyAnalyzer(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
config := &AppConfig{
|
||||
GitHubToken: tt.token,
|
||||
Quiet: true,
|
||||
@@ -513,6 +546,7 @@ func TestGenerator_CreateDependencyAnalyzer(t *testing.T) {
|
||||
|
||||
if tt.expectError {
|
||||
testutil.AssertError(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -526,6 +560,7 @@ func TestGenerator_CreateDependencyAnalyzer(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerator_WithDifferentThemes(t *testing.T) {
|
||||
t.Parallel()
|
||||
themes := []string{"default", "github", "gitlab", "minimal", "professional"}
|
||||
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
@@ -539,19 +574,8 @@ func TestGenerator_WithDifferentThemes(t *testing.T) {
|
||||
|
||||
for _, theme := range themes {
|
||||
t.Run("theme_"+theme, func(t *testing.T) {
|
||||
// Change to tmpDir so templates can be found
|
||||
origDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get working directory: %v", err)
|
||||
}
|
||||
if err := os.Chdir(tmpDir); err != nil {
|
||||
t.Fatalf("failed to change directory: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Chdir(origDir); err != nil {
|
||||
t.Errorf("failed to restore directory: %v", err)
|
||||
}
|
||||
}()
|
||||
t.Parallel()
|
||||
// Templates are now embedded, no working directory changes needed
|
||||
|
||||
config := &AppConfig{
|
||||
Theme: theme,
|
||||
@@ -563,6 +587,7 @@ func TestGenerator_WithDifferentThemes(t *testing.T) {
|
||||
|
||||
if err := generator.GenerateFromFile(actionPath); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -581,6 +606,7 @@ func TestGenerator_WithDifferentThemes(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGenerator_ErrorHandling(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T, tmpDir string) (*Generator, string)
|
||||
@@ -589,6 +615,7 @@ func TestGenerator_ErrorHandling(t *testing.T) {
|
||||
{
|
||||
name: "invalid template path",
|
||||
setupFunc: func(t *testing.T, tmpDir string) (*Generator, string) {
|
||||
t.Helper()
|
||||
config := &AppConfig{
|
||||
Template: "/nonexistent/template.tmpl",
|
||||
OutputFormat: "md",
|
||||
@@ -598,6 +625,7 @@ func TestGenerator_ErrorHandling(t *testing.T) {
|
||||
generator := NewGenerator(config)
|
||||
actionPath := filepath.Join(tmpDir, "action.yml")
|
||||
testutil.WriteTestFile(t, actionPath, testutil.MustReadFixture("actions/javascript/simple.yml"))
|
||||
|
||||
return generator, actionPath
|
||||
},
|
||||
wantError: "template",
|
||||
@@ -605,6 +633,7 @@ func TestGenerator_ErrorHandling(t *testing.T) {
|
||||
{
|
||||
name: "permission denied on output directory",
|
||||
setupFunc: func(t *testing.T, tmpDir string) (*Generator, string) {
|
||||
t.Helper()
|
||||
// Set up test templates
|
||||
testutil.SetupTestTemplates(t, tmpDir)
|
||||
|
||||
@@ -621,6 +650,7 @@ func TestGenerator_ErrorHandling(t *testing.T) {
|
||||
generator := NewGenerator(config)
|
||||
actionPath := filepath.Join(tmpDir, "action.yml")
|
||||
testutil.WriteTestFile(t, actionPath, testutil.MustReadFixture("actions/javascript/simple.yml"))
|
||||
|
||||
return generator, actionPath
|
||||
},
|
||||
wantError: "permission denied",
|
||||
@@ -629,6 +659,7 @@ func TestGenerator_ErrorHandling(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -30,6 +31,7 @@ func (r *RepoInfo) GetRepositoryName() string {
|
||||
if r.Organization != "" && r.Repository != "" {
|
||||
return fmt.Sprintf("%s/%s", r.Organization, r.Repository)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -50,7 +52,7 @@ func FindRepositoryRoot(startPath string) (string, error) {
|
||||
parent := filepath.Dir(absPath)
|
||||
if parent == absPath {
|
||||
// Reached root without finding .git
|
||||
return "", fmt.Errorf("not a git repository")
|
||||
return "", errors.New("not a git repository")
|
||||
}
|
||||
absPath = parent
|
||||
}
|
||||
@@ -129,12 +131,14 @@ func getRemoteURLFromConfig(repoRoot string) (string, error) {
|
||||
// Check for [remote "origin"] section
|
||||
if strings.Contains(line, `[remote "origin"]`) {
|
||||
inOriginSection = true
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for new section
|
||||
if strings.HasPrefix(line, "[") && inOriginSection {
|
||||
inOriginSection = false
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -144,7 +148,7 @@ func getRemoteURLFromConfig(repoRoot string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no origin remote URL found in git config")
|
||||
return "", errors.New("no origin remote URL found in git config")
|
||||
}
|
||||
|
||||
// getDefaultBranch gets the default branch name.
|
||||
@@ -160,6 +164,7 @@ func getDefaultBranch(repoRoot string) string {
|
||||
return branch
|
||||
}
|
||||
}
|
||||
|
||||
return DefaultBranch // Default fallback
|
||||
}
|
||||
|
||||
@@ -182,6 +187,7 @@ func branchExists(repoRoot, branch string) bool {
|
||||
"refs/heads/"+branch,
|
||||
) // #nosec G204 -- branch name validated by git
|
||||
cmd.Dir = repoRoot
|
||||
|
||||
return cmd.Run() == nil
|
||||
}
|
||||
|
||||
@@ -225,5 +231,6 @@ func (r *RepoInfo) GenerateUsesStatement(actionName, version string) string {
|
||||
if actionName != "" {
|
||||
return fmt.Sprintf("your-org/%s@%s", actionName, version)
|
||||
}
|
||||
|
||||
return "your-org/your-action@v1"
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
)
|
||||
|
||||
func TestFindRepositoryRoot(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T, tmpDir string) string
|
||||
@@ -18,6 +20,7 @@ func TestFindRepositoryRoot(t *testing.T) {
|
||||
{
|
||||
name: "git repository with .git directory",
|
||||
setupFunc: func(t *testing.T, tmpDir string) string {
|
||||
t.Helper()
|
||||
// Create .git directory
|
||||
gitDir := filepath.Join(tmpDir, ".git")
|
||||
err := os.MkdirAll(gitDir, 0750) // #nosec G301 -- test directory permissions
|
||||
@@ -40,6 +43,7 @@ func TestFindRepositoryRoot(t *testing.T) {
|
||||
{
|
||||
name: "git repository with .git file",
|
||||
setupFunc: func(t *testing.T, tmpDir string) string {
|
||||
t.Helper()
|
||||
// Create .git file (for git worktrees)
|
||||
gitFile := filepath.Join(tmpDir, ".git")
|
||||
testutil.WriteTestFile(t, gitFile, "gitdir: /path/to/git/dir")
|
||||
@@ -52,12 +56,14 @@ func TestFindRepositoryRoot(t *testing.T) {
|
||||
{
|
||||
name: "no git repository",
|
||||
setupFunc: func(t *testing.T, tmpDir string) string {
|
||||
t.Helper()
|
||||
// Create subdirectory without .git
|
||||
subDir := filepath.Join(tmpDir, "subdir")
|
||||
err := os.MkdirAll(subDir, 0750) // #nosec G301 -- test directory permissions
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create subdirectory: %v", err)
|
||||
}
|
||||
|
||||
return subDir
|
||||
},
|
||||
expectError: true,
|
||||
@@ -65,6 +71,8 @@ func TestFindRepositoryRoot(t *testing.T) {
|
||||
{
|
||||
name: "nonexistent directory",
|
||||
setupFunc: func(_ *testing.T, tmpDir string) string {
|
||||
t.Helper()
|
||||
|
||||
return filepath.Join(tmpDir, "nonexistent")
|
||||
},
|
||||
expectError: true,
|
||||
@@ -73,6 +81,8 @@ func TestFindRepositoryRoot(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -82,6 +92,7 @@ func TestFindRepositoryRoot(t *testing.T) {
|
||||
|
||||
if tt.expectError {
|
||||
testutil.AssertError(t, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -107,6 +118,8 @@ func TestFindRepositoryRoot(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDetectGitRepository(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T, tmpDir string) string
|
||||
@@ -115,6 +128,7 @@ func TestDetectGitRepository(t *testing.T) {
|
||||
{
|
||||
name: "GitHub repository",
|
||||
setupFunc: func(t *testing.T, tmpDir string) string {
|
||||
t.Helper()
|
||||
// Create .git directory
|
||||
gitDir := filepath.Join(tmpDir, ".git")
|
||||
err := os.MkdirAll(gitDir, 0750) // #nosec G301 -- test directory permissions
|
||||
@@ -141,6 +155,7 @@ func TestDetectGitRepository(t *testing.T) {
|
||||
return tmpDir
|
||||
},
|
||||
checkFunc: func(t *testing.T, info *RepoInfo) {
|
||||
t.Helper()
|
||||
testutil.AssertEqual(t, "owner", info.Organization)
|
||||
testutil.AssertEqual(t, "repo", info.Repository)
|
||||
testutil.AssertEqual(t, "https://github.com/owner/repo.git", info.RemoteURL)
|
||||
@@ -149,6 +164,7 @@ func TestDetectGitRepository(t *testing.T) {
|
||||
{
|
||||
name: "SSH remote URL",
|
||||
setupFunc: func(t *testing.T, tmpDir string) string {
|
||||
t.Helper()
|
||||
gitDir := filepath.Join(tmpDir, ".git")
|
||||
err := os.MkdirAll(gitDir, 0750) // #nosec G301 -- test directory permissions
|
||||
if err != nil {
|
||||
@@ -165,6 +181,7 @@ func TestDetectGitRepository(t *testing.T) {
|
||||
return tmpDir
|
||||
},
|
||||
checkFunc: func(t *testing.T, info *RepoInfo) {
|
||||
t.Helper()
|
||||
testutil.AssertEqual(t, "owner", info.Organization)
|
||||
testutil.AssertEqual(t, "repo", info.Repository)
|
||||
testutil.AssertEqual(t, "git@github.com:owner/repo.git", info.RemoteURL)
|
||||
@@ -176,6 +193,7 @@ func TestDetectGitRepository(t *testing.T) {
|
||||
return tmpDir
|
||||
},
|
||||
checkFunc: func(t *testing.T, info *RepoInfo) {
|
||||
t.Helper()
|
||||
testutil.AssertEqual(t, false, info.IsGitRepo)
|
||||
testutil.AssertEqual(t, "", info.Organization)
|
||||
testutil.AssertEqual(t, "", info.Repository)
|
||||
@@ -184,6 +202,7 @@ func TestDetectGitRepository(t *testing.T) {
|
||||
{
|
||||
name: "git repository without origin remote",
|
||||
setupFunc: func(t *testing.T, tmpDir string) string {
|
||||
t.Helper()
|
||||
gitDir := filepath.Join(tmpDir, ".git")
|
||||
err := os.MkdirAll(gitDir, 0750) // #nosec G301 -- test directory permissions
|
||||
if err != nil {
|
||||
@@ -201,6 +220,7 @@ func TestDetectGitRepository(t *testing.T) {
|
||||
return tmpDir
|
||||
},
|
||||
checkFunc: func(t *testing.T, info *RepoInfo) {
|
||||
t.Helper()
|
||||
testutil.AssertEqual(t, true, info.IsGitRepo)
|
||||
testutil.AssertEqual(t, "", info.Organization)
|
||||
testutil.AssertEqual(t, "", info.Repository)
|
||||
@@ -210,6 +230,8 @@ func TestDetectGitRepository(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -226,6 +248,8 @@ func TestDetectGitRepository(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseGitHubURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
remoteURL string
|
||||
@@ -266,6 +290,8 @@ func TestParseGitHubURL(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
org, repo := parseGitHubURL(tt.remoteURL)
|
||||
|
||||
testutil.AssertEqual(t, tt.expectedOrg, org)
|
||||
@@ -275,6 +301,8 @@ func TestParseGitHubURL(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRepoInfo_GetRepositoryName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
repoInfo RepoInfo
|
||||
@@ -311,6 +339,8 @@ func TestRepoInfo_GetRepositoryName(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := tt.repoInfo.GetRepositoryName()
|
||||
testutil.AssertEqual(t, tt.expected, result)
|
||||
})
|
||||
|
||||
@@ -12,8 +12,10 @@ func CreateAnalyzer(generator *internal.Generator, output *internal.ColoredOutpu
|
||||
analyzer, err := generator.CreateDependencyAnalyzer()
|
||||
if err != nil {
|
||||
output.Warning("Could not create dependency analyzer: %v", err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return analyzer
|
||||
}
|
||||
|
||||
@@ -24,5 +26,6 @@ func CreateAnalyzerOrExit(generator *internal.Generator, output *internal.Colore
|
||||
// Error already logged, just exit
|
||||
return nil
|
||||
}
|
||||
|
||||
return analyzer
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
)
|
||||
|
||||
func TestCreateAnalyzer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setupConfig func() *internal.AppConfig
|
||||
@@ -48,6 +50,8 @@ func TestCreateAnalyzer(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
config := tt.setupConfig()
|
||||
generator := internal.NewGenerator(config)
|
||||
|
||||
@@ -74,6 +78,8 @@ func TestCreateAnalyzer(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateAnalyzerOrExit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Only test success case since failure case calls os.Exit
|
||||
t.Run("successful analyzer creation", func(t *testing.T) {
|
||||
config := &internal.AppConfig{
|
||||
@@ -103,6 +109,8 @@ func TestCreateAnalyzerOrExit(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateAnalyzer_Integration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Test integration with actual generator functionality
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -15,6 +15,7 @@ func GetCurrentDir() (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error getting current directory: %w", err)
|
||||
}
|
||||
|
||||
return currentDir, nil
|
||||
}
|
||||
|
||||
@@ -31,12 +32,14 @@ func SetupGeneratorContext(config *internal.AppConfig) (*internal.Generator, str
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return generator, currentDir, nil
|
||||
}
|
||||
|
||||
// FindGitRepoRoot finds git repository root with standardized error handling.
|
||||
func FindGitRepoRoot(currentDir string) string {
|
||||
repoRoot, _ := git.FindRepositoryRoot(currentDir)
|
||||
|
||||
return repoRoot
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
)
|
||||
|
||||
func TestGetCurrentDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("successfully get current directory", func(t *testing.T) {
|
||||
currentDir, err := GetCurrentDir()
|
||||
|
||||
@@ -33,6 +35,8 @@ func TestGetCurrentDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSetupGeneratorContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config *internal.AppConfig
|
||||
@@ -71,6 +75,8 @@ func TestSetupGeneratorContext(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
generator, currentDir, err := SetupGeneratorContext(tt.config)
|
||||
|
||||
// Verify no error occurred
|
||||
@@ -79,6 +85,7 @@ func TestSetupGeneratorContext(t *testing.T) {
|
||||
// Verify generator was created
|
||||
if generator == nil {
|
||||
t.Error("expected generator to be created")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -100,6 +107,8 @@ func TestSetupGeneratorContext(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFindGitRepoRoot(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T, tmpDir string) string
|
||||
@@ -108,6 +117,7 @@ func TestFindGitRepoRoot(t *testing.T) {
|
||||
{
|
||||
name: "directory with git repository",
|
||||
setupFunc: func(t *testing.T, tmpDir string) string {
|
||||
t.Helper()
|
||||
// Create .git directory
|
||||
gitDir := filepath.Join(tmpDir, ".git")
|
||||
err := os.MkdirAll(gitDir, 0750) // #nosec G301 -- test directory permissions
|
||||
@@ -133,6 +143,7 @@ func TestFindGitRepoRoot(t *testing.T) {
|
||||
{
|
||||
name: "nested directory in git repository",
|
||||
setupFunc: func(t *testing.T, tmpDir string) string {
|
||||
t.Helper()
|
||||
// Create .git directory at root
|
||||
gitDir := filepath.Join(tmpDir, ".git")
|
||||
err := os.MkdirAll(gitDir, 0750) // #nosec G301 -- test directory permissions
|
||||
@@ -151,6 +162,8 @@ func TestFindGitRepoRoot(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -172,7 +185,11 @@ func TestFindGitRepoRoot(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetGitRepoRootAndInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("valid git repository with complete info", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -187,6 +204,8 @@ func TestGetGitRepoRootAndInfo(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("git repository but info detection fails", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -201,6 +220,8 @@ func TestGetGitRepoRootAndInfo(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("directory without git repository", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -220,6 +241,7 @@ func TestGetGitRepoRootAndInfo(t *testing.T) {
|
||||
|
||||
// Helper functions to reduce complexity.
|
||||
func setupCompleteGitRepo(t *testing.T, tmpDir string) string {
|
||||
t.Helper()
|
||||
// Create .git directory
|
||||
gitDir := filepath.Join(tmpDir, ".git")
|
||||
err := os.MkdirAll(gitDir, 0750) // #nosec G301 -- test directory permissions
|
||||
@@ -245,6 +267,7 @@ func setupCompleteGitRepo(t *testing.T, tmpDir string) string {
|
||||
}
|
||||
|
||||
func setupMinimalGitRepo(t *testing.T, tmpDir string) string {
|
||||
t.Helper()
|
||||
// Create .git directory but with minimal content
|
||||
gitDir := filepath.Join(tmpDir, ".git")
|
||||
err := os.MkdirAll(gitDir, 0750) // #nosec G301 -- test directory permissions
|
||||
@@ -254,6 +277,7 @@ func setupMinimalGitRepo(t *testing.T, tmpDir string) string {
|
||||
}
|
||||
|
||||
func verifyRepoRoot(t *testing.T, repoRoot, tmpDir string) {
|
||||
t.Helper()
|
||||
if repoRoot != "" && !strings.Contains(repoRoot, tmpDir) {
|
||||
t.Errorf("expected repo root to be within %s, got %s", tmpDir, repoRoot)
|
||||
}
|
||||
@@ -261,7 +285,11 @@ func verifyRepoRoot(t *testing.T, repoRoot, tmpDir string) {
|
||||
|
||||
// Test error handling in GetGitRepoRootAndInfo.
|
||||
func TestGetGitRepoRootAndInfo_ErrorHandling(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("nonexistent directory", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
nonexistentPath := "/this/path/should/not/exist"
|
||||
repoRoot, gitInfo, err := GetGitRepoRootAndInfo(nonexistentPath)
|
||||
|
||||
|
||||
@@ -31,5 +31,6 @@ func (w *HTMLWriter) Write(output string, path string) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ type MockProgressManager struct {
|
||||
|
||||
func (m *MockProgressManager) CreateProgressBar(description string, total int) *progressbar.ProgressBar {
|
||||
m.CreateProgressBarCalls = append(m.CreateProgressBarCalls, formatMessage("%s (total: %d)", description, total))
|
||||
|
||||
return nil // Return nil for mock to avoid actual progress bar
|
||||
}
|
||||
|
||||
@@ -109,6 +110,7 @@ func (m *MockProgressManager) CreateProgressBarForFiles(description string, file
|
||||
m.CreateProgressBarForFilesCalls,
|
||||
formatMessage("%s (files: %d)", description, len(files)),
|
||||
)
|
||||
|
||||
return nil // Return nil for mock to avoid actual progress bar
|
||||
}
|
||||
|
||||
@@ -151,6 +153,7 @@ func formatMessage(format string, args ...any) string {
|
||||
result = strings.Replace(result, "%d", toString(arg), 1)
|
||||
result = strings.Replace(result, "%v", toString(arg), 1)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -183,11 +186,13 @@ func formatInt(i int) string {
|
||||
if negative {
|
||||
result = "-" + result
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Test that demonstrates improved testability with focused interfaces.
|
||||
func TestFocusedInterfaces_SimpleLogger(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockLogger := &MockMessageLogger{}
|
||||
simpleLogger := NewSimpleLogger(mockLogger)
|
||||
|
||||
@@ -216,6 +221,7 @@ func TestFocusedInterfaces_SimpleLogger(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFocusedInterfaces_SimpleLogger_WithFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockLogger := &MockMessageLogger{}
|
||||
simpleLogger := NewSimpleLogger(mockLogger)
|
||||
|
||||
@@ -235,6 +241,7 @@ func TestFocusedInterfaces_SimpleLogger_WithFailure(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFocusedInterfaces_ErrorManager(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockReporter := &MockErrorReporter{}
|
||||
mockFormatter := &MockErrorFormatter{}
|
||||
mockManager := &mockErrorManager{
|
||||
@@ -257,6 +264,7 @@ func TestFocusedInterfaces_ErrorManager(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFocusedInterfaces_TaskProgress(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockReporter := &MockProgressReporter{}
|
||||
taskProgress := NewTaskProgress(mockReporter)
|
||||
|
||||
@@ -274,6 +282,7 @@ func TestFocusedInterfaces_TaskProgress(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFocusedInterfaces_ConfigAwareComponent(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
quietMode bool
|
||||
@@ -293,6 +302,7 @@ func TestFocusedInterfaces_ConfigAwareComponent(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockConfig := &MockOutputConfig{QuietMode: tt.quietMode}
|
||||
component := NewConfigAwareComponent(mockConfig)
|
||||
|
||||
@@ -306,6 +316,7 @@ func TestFocusedInterfaces_ConfigAwareComponent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFocusedInterfaces_CompositeOutputWriter(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Create a composite mock that implements OutputWriter
|
||||
mockLogger := &MockMessageLogger{}
|
||||
mockProgress := &MockProgressReporter{}
|
||||
@@ -338,6 +349,7 @@ func TestFocusedInterfaces_CompositeOutputWriter(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFocusedInterfaces_GeneratorWithDependencyInjection(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Create focused mocks
|
||||
mockOutput := &mockCompleteOutput{
|
||||
logger: &MockMessageLogger{},
|
||||
@@ -436,8 +448,10 @@ func (m *MockErrorFormatter) FormatContextualError(err *errors.ContextualError)
|
||||
if err != nil {
|
||||
formatted := err.Error()
|
||||
m.FormatContextualErrorCalls = append(m.FormatContextualErrorCalls, formatted)
|
||||
|
||||
return formatted
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package internal
|
||||
import "testing"
|
||||
|
||||
func TestFillMissing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a := &ActionYML{}
|
||||
defs := DefaultValues{
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func TestParseActionYML_Valid(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Create temporary action file using fixture
|
||||
actionPath := testutil.CreateTemporaryAction(t, "actions/javascript/simple.yml")
|
||||
action, err := ParseActionYML(actionPath)
|
||||
@@ -25,6 +26,7 @@ func TestParseActionYML_Valid(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseActionYML_MissingFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := ParseActionYML("notfound/action.yml")
|
||||
if err == nil {
|
||||
t.Error("expected error on missing file")
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func TestRenderReadme(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Set up test templates
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -3,6 +3,7 @@ package internal
|
||||
import "testing"
|
||||
|
||||
func TestValidateActionYML_Required(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a := &ActionYML{
|
||||
Name: "",
|
||||
@@ -16,6 +17,7 @@ func TestValidateActionYML_Required(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateActionYML_Valid(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := &ActionYML{
|
||||
Name: "MyAction",
|
||||
Description: "desc",
|
||||
|
||||
@@ -199,6 +199,7 @@ func (co *ColoredOutput) formatMainError(err *errors.ContextualError) string {
|
||||
if co.NoColor {
|
||||
return "❌ " + mainMsg
|
||||
}
|
||||
|
||||
return color.RedString("❌ ") + mainMsg
|
||||
}
|
||||
|
||||
@@ -237,7 +238,7 @@ func (co *ColoredOutput) formatSuggestionsSection(suggestions []string) []string
|
||||
|
||||
for _, suggestion := range suggestions {
|
||||
if co.NoColor {
|
||||
parts = append(parts, fmt.Sprintf(" • %s", suggestion))
|
||||
parts = append(parts, " • "+suggestion)
|
||||
} else {
|
||||
parts = append(parts, fmt.Sprintf(" %s %s",
|
||||
color.YellowString("•"),
|
||||
@@ -251,8 +252,9 @@ func (co *ColoredOutput) formatSuggestionsSection(suggestions []string) []string
|
||||
// formatHelpURLSection formats the help URL section.
|
||||
func (co *ColoredOutput) formatHelpURLSection(helpURL string) string {
|
||||
if co.NoColor {
|
||||
return fmt.Sprintf("\nFor more help: %s", helpURL)
|
||||
return "\nFor more help: " + helpURL
|
||||
}
|
||||
|
||||
return fmt.Sprintf("\n%s: %s",
|
||||
color.New(color.Bold).Sprint("For more help"),
|
||||
color.BlueString(helpURL))
|
||||
|
||||
@@ -52,6 +52,7 @@ func ParseActionYML(path string) (*ActionYML, error) {
|
||||
if err := dec.Decode(&a); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func TestProgressBarManager_CreateProgressBar(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
quiet bool
|
||||
@@ -46,6 +47,7 @@ func TestProgressBarManager_CreateProgressBar(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
pm := NewProgressBarManager(tt.quiet)
|
||||
bar := pm.CreateProgressBar(tt.description, tt.total)
|
||||
|
||||
@@ -63,6 +65,7 @@ func TestProgressBarManager_CreateProgressBar(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProgressBarManager_CreateProgressBarForFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
pm := NewProgressBarManager(false)
|
||||
files := []string{"file1.yml", "file2.yml", "file3.yml"}
|
||||
|
||||
@@ -73,7 +76,8 @@ func TestProgressBarManager_CreateProgressBarForFiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgressBarManager_FinishProgressBar(_ *testing.T) {
|
||||
func TestProgressBarManager_FinishProgressBar(t *testing.T) {
|
||||
t.Parallel()
|
||||
pm := NewProgressBarManager(false)
|
||||
|
||||
// Test with nil bar (should not panic)
|
||||
@@ -86,7 +90,8 @@ func TestProgressBarManager_FinishProgressBar(_ *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgressBarManager_UpdateProgressBar(_ *testing.T) {
|
||||
func TestProgressBarManager_UpdateProgressBar(t *testing.T) {
|
||||
t.Parallel()
|
||||
pm := NewProgressBarManager(false)
|
||||
|
||||
// Test with nil bar (should not panic)
|
||||
@@ -100,6 +105,7 @@ func TestProgressBarManager_UpdateProgressBar(_ *testing.T) {
|
||||
}
|
||||
|
||||
func TestProgressBarManager_ProcessWithProgressBar(t *testing.T) {
|
||||
t.Parallel()
|
||||
pm := NewProgressBarManager(false)
|
||||
items := []string{"item1", "item2", "item3"}
|
||||
|
||||
@@ -122,6 +128,7 @@ func TestProgressBarManager_ProcessWithProgressBar(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProgressBarManager_ProcessWithProgressBar_QuietMode(t *testing.T) {
|
||||
t.Parallel()
|
||||
pm := NewProgressBarManager(true) // quiet mode
|
||||
items := []string{"item1", "item2"}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package internal
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
@@ -13,6 +12,7 @@ import (
|
||||
"github.com/ivuorinen/gh-action-readme/internal/dependencies"
|
||||
"github.com/ivuorinen/gh-action-readme/internal/git"
|
||||
"github.com/ivuorinen/gh-action-readme/internal/validation"
|
||||
"github.com/ivuorinen/gh-action-readme/templates_embed"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -70,6 +70,7 @@ func getGitOrg(data any) string {
|
||||
return td.Config.Organization
|
||||
}
|
||||
}
|
||||
|
||||
return defaultOrgPlaceholder
|
||||
}
|
||||
|
||||
@@ -83,6 +84,7 @@ func getGitRepo(data any) string {
|
||||
return td.Config.Repository
|
||||
}
|
||||
}
|
||||
|
||||
return defaultRepoPlaceholder
|
||||
}
|
||||
|
||||
@@ -101,6 +103,7 @@ func getGitUsesString(data any) string {
|
||||
}
|
||||
|
||||
version := formatVersion(getActionVersion(data))
|
||||
|
||||
return buildUsesString(td, org, repo, version)
|
||||
}
|
||||
|
||||
@@ -118,6 +121,7 @@ func formatVersion(version string) string {
|
||||
if !strings.HasPrefix(version, "@") {
|
||||
return "@" + version
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
@@ -129,6 +133,7 @@ func buildUsesString(td *TemplateData, org, repo, version string) string {
|
||||
return fmt.Sprintf("%s/%s/%s%s", org, repo, actionName, version)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/%s%s", org, repo, version)
|
||||
}
|
||||
|
||||
@@ -139,6 +144,7 @@ func getActionVersion(data any) string {
|
||||
return td.Config.Version
|
||||
}
|
||||
}
|
||||
|
||||
return "v1"
|
||||
}
|
||||
|
||||
@@ -217,7 +223,7 @@ func analyzeDependencies(actionPath string, config *AppConfig, gitInfo git.RepoI
|
||||
|
||||
// RenderReadme renders a README using a Go template and the parsed action.yml data.
|
||||
func RenderReadme(action any, opts TemplateOptions) (string, error) {
|
||||
tmplContent, err := os.ReadFile(opts.TemplatePath)
|
||||
tmplContent, err := templates_embed.ReadTemplate(opts.TemplatePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -229,11 +235,11 @@ func RenderReadme(action any, opts TemplateOptions) (string, error) {
|
||||
}
|
||||
var head, foot string
|
||||
if opts.HeaderPath != "" {
|
||||
h, _ := os.ReadFile(opts.HeaderPath)
|
||||
h, _ := templates_embed.ReadTemplate(opts.HeaderPath)
|
||||
head = string(h)
|
||||
}
|
||||
if opts.FooterPath != "" {
|
||||
f, _ := os.ReadFile(opts.FooterPath)
|
||||
f, _ := templates_embed.ReadTemplate(opts.FooterPath)
|
||||
foot = string(f)
|
||||
}
|
||||
// Wrap template output in header/footer
|
||||
@@ -243,6 +249,7 @@ func RenderReadme(action any, opts TemplateOptions) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
buf.WriteString(foot)
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
@@ -254,5 +261,6 @@ func RenderReadme(action any, opts TemplateOptions) (string, error) {
|
||||
if err := tmpl.Execute(buf, action); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ func GetBinaryDir() (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
return filepath.Dir(executable), nil
|
||||
}
|
||||
|
||||
@@ -21,5 +22,6 @@ func EnsureAbsolutePath(path string) (string, error) {
|
||||
if filepath.IsAbs(path) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
return filepath.Abs(path)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
// CleanVersionString removes common prefixes and normalizes version strings.
|
||||
func CleanVersionString(version string) string {
|
||||
cleaned := strings.TrimSpace(version)
|
||||
|
||||
return strings.TrimPrefix(cleaned, "v")
|
||||
}
|
||||
|
||||
@@ -40,6 +41,7 @@ func SanitizeActionName(name string) string {
|
||||
func TrimAndNormalize(input string) string {
|
||||
// Remove leading/trailing whitespace and normalize internal whitespace
|
||||
re := regexp.MustCompile(`\s+`)
|
||||
|
||||
return re.ReplaceAllString(strings.TrimSpace(input), " ")
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
func IsCommitSHA(version string) bool {
|
||||
// Check if it's a 40-character hex string (full SHA) or 7+ character hex (short SHA)
|
||||
re := regexp.MustCompile(`^[a-f0-9]{7,40}$`)
|
||||
|
||||
return len(version) >= 7 && re.MatchString(version)
|
||||
}
|
||||
|
||||
@@ -20,6 +21,7 @@ func IsCommitSHA(version string) bool {
|
||||
func IsSemanticVersion(version string) bool {
|
||||
// Check for vX.Y.Z format (requires major.minor.patch)
|
||||
re := regexp.MustCompile(`^v?\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$`)
|
||||
|
||||
return re.MatchString(version)
|
||||
}
|
||||
|
||||
@@ -39,6 +41,7 @@ func ValidateGitBranch(repoRoot, branch string) bool {
|
||||
"refs/heads/"+branch,
|
||||
) // #nosec G204 -- branch name validated by git
|
||||
cmd.Dir = repoRoot
|
||||
|
||||
return cmd.Run() == nil
|
||||
}
|
||||
|
||||
@@ -61,5 +64,6 @@ func ValidateActionYMLPath(path string) error {
|
||||
// IsGitRepository checks if the given path is within a git repository.
|
||||
func IsGitRepository(path string) bool {
|
||||
_, err := git.FindRepositoryRoot(path)
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
)
|
||||
|
||||
func TestValidateActionYMLPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T, tmpDir string) string
|
||||
@@ -18,8 +20,10 @@ func TestValidateActionYMLPath(t *testing.T) {
|
||||
{
|
||||
name: "valid action.yml file",
|
||||
setupFunc: func(t *testing.T, tmpDir string) string {
|
||||
t.Helper()
|
||||
actionPath := filepath.Join(tmpDir, "action.yml")
|
||||
testutil.WriteTestFile(t, actionPath, testutil.MustReadFixture("actions/javascript/simple.yml"))
|
||||
|
||||
return actionPath
|
||||
},
|
||||
expectError: false,
|
||||
@@ -27,8 +31,10 @@ func TestValidateActionYMLPath(t *testing.T) {
|
||||
{
|
||||
name: "valid action.yaml file",
|
||||
setupFunc: func(t *testing.T, tmpDir string) string {
|
||||
t.Helper()
|
||||
actionPath := filepath.Join(tmpDir, "action.yaml")
|
||||
testutil.WriteTestFile(t, actionPath, testutil.MustReadFixture("minimal-action.yml"))
|
||||
|
||||
return actionPath
|
||||
},
|
||||
expectError: false,
|
||||
@@ -43,8 +49,10 @@ func TestValidateActionYMLPath(t *testing.T) {
|
||||
{
|
||||
name: "file with wrong extension",
|
||||
setupFunc: func(t *testing.T, tmpDir string) string {
|
||||
t.Helper()
|
||||
actionPath := filepath.Join(tmpDir, "action.txt")
|
||||
testutil.WriteTestFile(t, actionPath, testutil.MustReadFixture("actions/javascript/simple.yml"))
|
||||
|
||||
return actionPath
|
||||
},
|
||||
expectError: true,
|
||||
@@ -60,6 +68,8 @@ func TestValidateActionYMLPath(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -77,6 +87,8 @@ func TestValidateActionYMLPath(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIsCommitSHA(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
version string
|
||||
@@ -116,6 +128,8 @@ func TestIsCommitSHA(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := IsCommitSHA(tt.version)
|
||||
testutil.AssertEqual(t, tt.expected, result)
|
||||
})
|
||||
@@ -123,6 +137,8 @@ func TestIsCommitSHA(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIsSemanticVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
version string
|
||||
@@ -172,6 +188,8 @@ func TestIsSemanticVersion(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := IsSemanticVersion(tt.version)
|
||||
testutil.AssertEqual(t, tt.expected, result)
|
||||
})
|
||||
@@ -179,6 +197,8 @@ func TestIsSemanticVersion(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIsVersionPinned(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
version string
|
||||
@@ -223,6 +243,8 @@ func TestIsVersionPinned(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := IsVersionPinned(tt.version)
|
||||
testutil.AssertEqual(t, tt.expected, result)
|
||||
})
|
||||
@@ -230,6 +252,8 @@ func TestIsVersionPinned(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateGitBranch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T, tmpDir string) (string, string)
|
||||
@@ -252,6 +276,7 @@ func TestValidateGitBranch(t *testing.T) {
|
||||
merge = refs/heads/main
|
||||
`
|
||||
testutil.WriteTestFile(t, filepath.Join(gitDir, "config"), configContent)
|
||||
|
||||
return tmpDir, "main"
|
||||
},
|
||||
expected: true, // This may vary based on actual git repo state
|
||||
@@ -274,6 +299,8 @@ func TestValidateGitBranch(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -288,6 +315,8 @@ func TestValidateGitBranch(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIsGitRepository(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T, tmpDir string) string
|
||||
@@ -298,6 +327,7 @@ func TestIsGitRepository(t *testing.T) {
|
||||
setupFunc: func(_ *testing.T, tmpDir string) string {
|
||||
gitDir := filepath.Join(tmpDir, ".git")
|
||||
_ = os.MkdirAll(gitDir, 0750) // #nosec G301 -- test directory permissions
|
||||
|
||||
return tmpDir
|
||||
},
|
||||
expected: true,
|
||||
@@ -305,8 +335,10 @@ func TestIsGitRepository(t *testing.T) {
|
||||
{
|
||||
name: "directory with .git file",
|
||||
setupFunc: func(t *testing.T, tmpDir string) string {
|
||||
t.Helper()
|
||||
gitFile := filepath.Join(tmpDir, ".git")
|
||||
testutil.WriteTestFile(t, gitFile, "gitdir: /path/to/git/dir")
|
||||
|
||||
return tmpDir
|
||||
},
|
||||
expected: true,
|
||||
@@ -329,6 +361,8 @@ func TestIsGitRepository(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpDir, cleanup := testutil.TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -340,6 +374,8 @@ func TestIsGitRepository(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCleanVersionString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
@@ -374,6 +410,8 @@ func TestCleanVersionString(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := CleanVersionString(tt.input)
|
||||
testutil.AssertEqual(t, tt.expected, result)
|
||||
})
|
||||
@@ -381,6 +419,8 @@ func TestCleanVersionString(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseGitHubURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
@@ -421,6 +461,8 @@ func TestParseGitHubURL(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
org, repo := ParseGitHubURL(tt.url)
|
||||
testutil.AssertEqual(t, tt.expectedOrg, org)
|
||||
testutil.AssertEqual(t, tt.expectedRepo, repo)
|
||||
@@ -429,6 +471,8 @@ func TestParseGitHubURL(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSanitizeActionName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
@@ -457,7 +501,9 @@ func TestSanitizeActionName(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(_ *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := SanitizeActionName(tt.input)
|
||||
// The exact behavior may vary, so we'll just verify it doesn't panic
|
||||
_ = result
|
||||
@@ -466,6 +512,8 @@ func TestSanitizeActionName(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetBinaryDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir, err := GetBinaryDir()
|
||||
testutil.AssertNoError(t, err)
|
||||
|
||||
@@ -480,6 +528,8 @@ func TestGetBinaryDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEnsureAbsolutePath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
@@ -509,6 +559,8 @@ func TestEnsureAbsolutePath(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result, err := EnsureAbsolutePath(tt.input)
|
||||
|
||||
if tt.input == "" {
|
||||
|
||||
@@ -89,5 +89,6 @@ func isValidRuntime(runtime string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package wizard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -88,7 +89,7 @@ func (d *ProjectDetector) DetectProjectSettings() (*DetectedSettings, error) {
|
||||
// detectRepositoryInfo detects repository information from git.
|
||||
func (d *ProjectDetector) detectRepositoryInfo(settings *DetectedSettings) error {
|
||||
if d.repoRoot == "" {
|
||||
return fmt.Errorf("not in a git repository")
|
||||
return errors.New("not in a git repository")
|
||||
}
|
||||
|
||||
repoInfo, err := git.DetectRepository(d.repoRoot)
|
||||
@@ -103,6 +104,7 @@ func (d *ProjectDetector) detectRepositoryInfo(settings *DetectedSettings) error
|
||||
settings.Version = d.detectVersion()
|
||||
|
||||
d.output.Success("Detected repository: %s/%s", settings.Organization, settings.Repository)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -221,6 +223,7 @@ func (d *ProjectDetector) findActionFiles(dir string, recursive bool) ([]string,
|
||||
if recursive {
|
||||
return d.findActionFilesRecursive(dir)
|
||||
}
|
||||
|
||||
return d.findActionFilesInDirectory(dir)
|
||||
}
|
||||
|
||||
@@ -253,6 +256,7 @@ func (d *ProjectDetector) handleDirectory(info os.FileInfo) error {
|
||||
if strings.HasPrefix(name, ".") || name == "node_modules" || name == "vendor" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -366,6 +370,7 @@ func (d *ProjectDetector) analyzeProjectFiles() map[string]string {
|
||||
}
|
||||
|
||||
d.setDefaultProjectType(characteristics)
|
||||
|
||||
return characteristics
|
||||
}
|
||||
|
||||
@@ -425,6 +430,7 @@ func (d *ProjectDetector) setDefaultProjectType(characteristics map[string]strin
|
||||
// getCurrentActionFiles gets action files in current directory only.
|
||||
func (d *ProjectDetector) getCurrentActionFiles() []string {
|
||||
actionFiles, _ := d.findActionFiles(d.currentDir, false)
|
||||
|
||||
return actionFiles
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func TestProjectDetector_analyzeProjectFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Create temporary directory for testing
|
||||
tempDir := t.TempDir()
|
||||
|
||||
@@ -50,6 +51,7 @@ func TestProjectDetector_analyzeProjectFiles(t *testing.T) {
|
||||
for _, validType := range validTypes {
|
||||
if projectType == validType {
|
||||
typeValid = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -63,6 +65,7 @@ func TestProjectDetector_analyzeProjectFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProjectDetector_detectVersionFromPackageJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create package.json with version
|
||||
@@ -90,6 +93,7 @@ func TestProjectDetector_detectVersionFromPackageJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProjectDetector_detectVersionFromFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create VERSION file
|
||||
@@ -112,6 +116,7 @@ func TestProjectDetector_detectVersionFromFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProjectDetector_findActionFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create action files
|
||||
@@ -167,6 +172,7 @@ func TestProjectDetector_findActionFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProjectDetector_isActionFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
output := internal.NewColoredOutput(true)
|
||||
detector := &ProjectDetector{
|
||||
output: output,
|
||||
@@ -186,6 +192,7 @@ func TestProjectDetector_isActionFile(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.filename, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
result := detector.isActionFile(tt.filename)
|
||||
if result != tt.expected {
|
||||
t.Errorf("isActionFile(%s) = %v, want %v", tt.filename, result, tt.expected)
|
||||
@@ -195,6 +202,7 @@ func TestProjectDetector_isActionFile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProjectDetector_suggestConfiguration(t *testing.T) {
|
||||
t.Parallel()
|
||||
output := internal.NewColoredOutput(true)
|
||||
detector := &ProjectDetector{
|
||||
output: output,
|
||||
@@ -242,6 +250,7 @@ func TestProjectDetector_suggestConfiguration(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
detector.suggestConfiguration(tt.settings)
|
||||
if tt.settings.SuggestedTheme != tt.expected {
|
||||
t.Errorf("Expected theme %s, got %s", tt.expected, tt.settings.SuggestedTheme)
|
||||
|
||||
@@ -80,6 +80,7 @@ func (e *ConfigExporter) exportYAML(config *internal.AppConfig, outputPath strin
|
||||
}
|
||||
|
||||
e.output.Success("Configuration exported to: %s", outputPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -104,6 +105,7 @@ func (e *ConfigExporter) exportJSON(config *internal.AppConfig, outputPath strin
|
||||
}
|
||||
|
||||
e.output.Success("Configuration exported to: %s", outputPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -129,6 +131,7 @@ func (e *ConfigExporter) exportTOML(config *internal.AppConfig, outputPath strin
|
||||
e.writeTOMLConfig(file, exportConfig)
|
||||
|
||||
e.output.Success("Configuration exported to: %s", outputPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func TestConfigExporter_ExportConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
output := internal.NewColoredOutput(true) // quiet mode for testing
|
||||
exporter := NewConfigExporter(output)
|
||||
|
||||
@@ -20,13 +21,22 @@ func TestConfigExporter_ExportConfig(t *testing.T) {
|
||||
config := createTestConfig()
|
||||
|
||||
// Test YAML export
|
||||
t.Run("export YAML", testYAMLExport(exporter, config))
|
||||
t.Run("export YAML", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testYAMLExport(exporter, config)(t)
|
||||
})
|
||||
|
||||
// Test JSON export
|
||||
t.Run("export JSON", testJSONExport(exporter, config))
|
||||
t.Run("export JSON", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testJSONExport(exporter, config)(t)
|
||||
})
|
||||
|
||||
// Test TOML export
|
||||
t.Run("export TOML", testTOMLExport(exporter, config))
|
||||
t.Run("export TOML", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testTOMLExport(exporter, config)(t)
|
||||
})
|
||||
}
|
||||
|
||||
// createTestConfig creates a test configuration for testing.
|
||||
@@ -49,6 +59,7 @@ func createTestConfig() *internal.AppConfig {
|
||||
// testYAMLExport tests YAML export functionality.
|
||||
func testYAMLExport(exporter *ConfigExporter, config *internal.AppConfig) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
t.Helper()
|
||||
tempDir := t.TempDir()
|
||||
outputPath := filepath.Join(tempDir, "config.yaml")
|
||||
|
||||
@@ -65,6 +76,7 @@ func testYAMLExport(exporter *ConfigExporter, config *internal.AppConfig) func(*
|
||||
// testJSONExport tests JSON export functionality.
|
||||
func testJSONExport(exporter *ConfigExporter, config *internal.AppConfig) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
t.Helper()
|
||||
tempDir := t.TempDir()
|
||||
outputPath := filepath.Join(tempDir, "config.json")
|
||||
|
||||
@@ -81,6 +93,7 @@ func testJSONExport(exporter *ConfigExporter, config *internal.AppConfig) func(*
|
||||
// testTOMLExport tests TOML export functionality.
|
||||
func testTOMLExport(exporter *ConfigExporter, config *internal.AppConfig) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
t.Helper()
|
||||
tempDir := t.TempDir()
|
||||
outputPath := filepath.Join(tempDir, "config.toml")
|
||||
|
||||
@@ -96,6 +109,7 @@ func testTOMLExport(exporter *ConfigExporter, config *internal.AppConfig) func(*
|
||||
|
||||
// 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")
|
||||
}
|
||||
@@ -103,6 +117,7 @@ func verifyFileExists(t *testing.T, outputPath string) {
|
||||
|
||||
// verifyYAMLContent verifies YAML content is valid and contains expected data.
|
||||
func verifyYAMLContent(t *testing.T, outputPath string, expected *internal.AppConfig) {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(outputPath) // #nosec G304 -- test output path
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read output file: %v", err)
|
||||
@@ -123,6 +138,7 @@ func verifyYAMLContent(t *testing.T, outputPath string, expected *internal.AppCo
|
||||
|
||||
// verifyJSONContent verifies JSON content is valid and contains expected data.
|
||||
func verifyJSONContent(t *testing.T, outputPath string, expected *internal.AppConfig) {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(outputPath) // #nosec G304 -- test output path
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read output file: %v", err)
|
||||
@@ -143,6 +159,7 @@ func verifyJSONContent(t *testing.T, outputPath string, expected *internal.AppCo
|
||||
|
||||
// verifyTOMLContent verifies TOML content contains expected fields.
|
||||
func verifyTOMLContent(t *testing.T, outputPath string) {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(outputPath) // #nosec G304 -- test output path
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read output file: %v", err)
|
||||
@@ -158,6 +175,7 @@ func verifyTOMLContent(t *testing.T, outputPath string) {
|
||||
}
|
||||
|
||||
func TestConfigExporter_sanitizeConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
output := internal.NewColoredOutput(true)
|
||||
exporter := NewConfigExporter(output)
|
||||
|
||||
@@ -191,6 +209,7 @@ func TestConfigExporter_sanitizeConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigExporter_GetSupportedFormats(t *testing.T) {
|
||||
t.Parallel()
|
||||
output := internal.NewColoredOutput(true)
|
||||
exporter := NewConfigExporter(output)
|
||||
|
||||
@@ -215,6 +234,7 @@ func TestConfigExporter_GetSupportedFormats(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigExporter_GetDefaultOutputPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
output := internal.NewColoredOutput(true)
|
||||
exporter := NewConfigExporter(output)
|
||||
|
||||
@@ -229,6 +249,7 @@ func TestConfigExporter_GetDefaultOutputPath(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.format), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
path, err := exporter.GetDefaultOutputPath(tt.format)
|
||||
if err != nil {
|
||||
t.Fatalf("GetDefaultOutputPath() error = %v", err)
|
||||
|
||||
@@ -105,6 +105,7 @@ func (v *ConfigValidator) ValidateField(fieldName, value string) *ValidationResu
|
||||
}
|
||||
|
||||
result.Valid = len(result.Errors) == 0
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -116,6 +117,7 @@ func (v *ConfigValidator) validateOrganization(org string, result *ValidationRes
|
||||
Message: "Organization is empty - will use auto-detected value",
|
||||
Value: org,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -139,6 +141,7 @@ func (v *ConfigValidator) validateRepository(repo string, result *ValidationResu
|
||||
Message: "Repository is empty - will use auto-detected value",
|
||||
Value: repo,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -181,6 +184,7 @@ func (v *ConfigValidator) validateTheme(theme string, result *ValidationResult)
|
||||
for _, validTheme := range validThemes {
|
||||
if theme == validTheme {
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -192,7 +196,7 @@ func (v *ConfigValidator) validateTheme(theme string, result *ValidationResult)
|
||||
Value: theme,
|
||||
})
|
||||
result.Suggestions = append(result.Suggestions,
|
||||
fmt.Sprintf("Valid themes: %s", strings.Join(validThemes, ", ")))
|
||||
"Valid themes: "+strings.Join(validThemes, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +208,7 @@ func (v *ConfigValidator) validateOutputFormat(format string, result *Validation
|
||||
for _, validFormat := range validFormats {
|
||||
if format == validFormat {
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -215,7 +220,7 @@ func (v *ConfigValidator) validateOutputFormat(format string, result *Validation
|
||||
Value: format,
|
||||
})
|
||||
result.Suggestions = append(result.Suggestions,
|
||||
fmt.Sprintf("Valid formats: %s", strings.Join(validFormats, ", ")))
|
||||
"Valid formats: "+strings.Join(validFormats, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,6 +232,7 @@ func (v *ConfigValidator) validateOutputDir(dir string, result *ValidationResult
|
||||
Message: "Output directory cannot be empty",
|
||||
Value: dir,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -314,9 +320,10 @@ func (v *ConfigValidator) validatePermissions(permissions map[string]string, res
|
||||
if !permissionExists {
|
||||
result.Warnings = append(result.Warnings, ValidationWarning{
|
||||
Field: "permissions",
|
||||
Message: fmt.Sprintf("Unknown permission: %s", permission),
|
||||
Message: "Unknown permission: " + permission,
|
||||
Value: value,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -325,6 +332,7 @@ func (v *ConfigValidator) validatePermissions(permissions map[string]string, res
|
||||
for _, validVal := range validValues {
|
||||
if value == validVal {
|
||||
validValue = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -332,7 +340,7 @@ func (v *ConfigValidator) validatePermissions(permissions map[string]string, res
|
||||
if !validValue {
|
||||
result.Errors = append(result.Errors, ValidationError{
|
||||
Field: "permissions",
|
||||
Message: fmt.Sprintf("Invalid value for permission %s", permission),
|
||||
Message: "Invalid value for permission " + permission,
|
||||
Value: value,
|
||||
})
|
||||
result.Suggestions = append(result.Suggestions,
|
||||
@@ -351,6 +359,7 @@ func (v *ConfigValidator) validateRunsOn(runsOn []string, result *ValidationResu
|
||||
})
|
||||
result.Suggestions = append(result.Suggestions,
|
||||
"Consider specifying at least one runner (e.g., ubuntu-latest)")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -366,6 +375,7 @@ func (v *ConfigValidator) validateRunsOn(runsOn []string, result *ValidationResu
|
||||
for _, validRunner := range validRunners {
|
||||
if runner == validRunner {
|
||||
isValid = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -375,7 +385,7 @@ func (v *ConfigValidator) validateRunsOn(runsOn []string, result *ValidationResu
|
||||
if !strings.HasPrefix(runner, "self-hosted") {
|
||||
result.Warnings = append(result.Warnings, ValidationWarning{
|
||||
Field: "runs_on",
|
||||
Message: fmt.Sprintf("Unknown runner: %s", runner),
|
||||
Message: "Unknown runner: " + runner,
|
||||
Value: runner,
|
||||
})
|
||||
result.Suggestions = append(result.Suggestions,
|
||||
@@ -398,9 +408,10 @@ func (v *ConfigValidator) validateVariables(variables map[string]string, result
|
||||
if strings.EqualFold(key, reserved) {
|
||||
result.Warnings = append(result.Warnings, ValidationWarning{
|
||||
Field: "variables",
|
||||
Message: fmt.Sprintf("Variable name conflicts with GitHub environment variable: %s", key),
|
||||
Message: "Variable name conflicts with GitHub environment variable: " + key,
|
||||
Value: value,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -409,7 +420,7 @@ func (v *ConfigValidator) validateVariables(variables map[string]string, result
|
||||
if !v.isValidVariableName(key) {
|
||||
result.Errors = append(result.Errors, ValidationError{
|
||||
Field: "variables",
|
||||
Message: fmt.Sprintf("Invalid variable name: %s", key),
|
||||
Message: "Invalid variable name: " + key,
|
||||
Value: value,
|
||||
})
|
||||
result.Suggestions = append(result.Suggestions,
|
||||
@@ -427,6 +438,7 @@ func (v *ConfigValidator) isValidGitHubName(name string) bool {
|
||||
// GitHub names can contain alphanumeric characters and hyphens
|
||||
// Cannot start or end with hyphen
|
||||
matched, _ := regexp.MatchString(`^[a-zA-Z0-9]([a-zA-Z0-9\-_]*[a-zA-Z0-9])?$`, name)
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
@@ -437,6 +449,7 @@ func (v *ConfigValidator) isValidSemanticVersion(version string) bool {
|
||||
`(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?` +
|
||||
`(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`
|
||||
matched, _ := regexp.MatchString(pattern, version)
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
@@ -462,6 +475,7 @@ func (v *ConfigValidator) isValidVariableName(name string) bool {
|
||||
// Variable names should start with letter or underscore
|
||||
// and contain only letters, numbers, and underscores
|
||||
matched, _ := regexp.MatchString(`^[a-zA-Z_][a-zA-Z0-9_]*$`, name)
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func TestConfigValidator_ValidateConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
output := internal.NewColoredOutput(true) // quiet mode for testing
|
||||
validator := NewConfigValidator(output)
|
||||
|
||||
@@ -74,6 +75,7 @@ func TestConfigValidator_ValidateConfig(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
result := validator.ValidateConfig(tt.config)
|
||||
|
||||
if result.Valid != tt.expectValid {
|
||||
@@ -92,6 +94,7 @@ func TestConfigValidator_ValidateConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigValidator_ValidateField(t *testing.T) {
|
||||
t.Parallel()
|
||||
output := internal.NewColoredOutput(true)
|
||||
validator := NewConfigValidator(output)
|
||||
|
||||
@@ -115,6 +118,7 @@ func TestConfigValidator_ValidateField(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
result := validator.ValidateField(tt.fieldName, tt.value)
|
||||
|
||||
if result.Valid != tt.expectValid {
|
||||
@@ -125,6 +129,7 @@ func TestConfigValidator_ValidateField(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigValidator_isValidGitHubName(t *testing.T) {
|
||||
t.Parallel()
|
||||
output := internal.NewColoredOutput(true)
|
||||
validator := NewConfigValidator(output)
|
||||
|
||||
@@ -146,6 +151,7 @@ func TestConfigValidator_isValidGitHubName(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := validator.isValidGitHubName(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("isValidGitHubName(%q) = %v, want %v", tt.input, got, tt.want)
|
||||
@@ -155,6 +161,7 @@ func TestConfigValidator_isValidGitHubName(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigValidator_isValidSemanticVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
output := internal.NewColoredOutput(true)
|
||||
validator := NewConfigValidator(output)
|
||||
|
||||
@@ -175,6 +182,7 @@ func TestConfigValidator_isValidSemanticVersion(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := validator.isValidSemanticVersion(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("isValidSemanticVersion(%q) = %v, want %v", tt.input, got, tt.want)
|
||||
@@ -184,6 +192,7 @@ func TestConfigValidator_isValidSemanticVersion(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigValidator_isValidGitHubToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
output := internal.NewColoredOutput(true)
|
||||
validator := NewConfigValidator(output)
|
||||
|
||||
@@ -204,6 +213,7 @@ func TestConfigValidator_isValidGitHubToken(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := validator.isValidGitHubToken(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("isValidGitHubToken(%q) = %v, want %v", tt.input, got, tt.want)
|
||||
@@ -213,6 +223,7 @@ func TestConfigValidator_isValidGitHubToken(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigValidator_isValidVariableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
output := internal.NewColoredOutput(true)
|
||||
validator := NewConfigValidator(output)
|
||||
|
||||
@@ -234,6 +245,7 @@ func TestConfigValidator_isValidVariableName(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := validator.isValidVariableName(tt.input)
|
||||
if got != tt.want {
|
||||
t.Errorf("isValidVariableName(%q) = %v, want %v", tt.input, got, tt.want)
|
||||
|
||||
@@ -3,6 +3,7 @@ package wizard
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -60,6 +61,7 @@ func (w *ConfigWizard) Run() (*internal.AppConfig, error) {
|
||||
}
|
||||
|
||||
w.output.Success("\n✅ Configuration completed successfully!")
|
||||
|
||||
return w.config, nil
|
||||
}
|
||||
|
||||
@@ -218,6 +220,7 @@ func (w *ConfigWizard) configureGitHubIntegration() {
|
||||
existingToken := internal.GetGitHubToken(w.config)
|
||||
if existingToken != "" {
|
||||
w.output.Success("GitHub token already configured ✓")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -231,6 +234,7 @@ func (w *ConfigWizard) configureGitHubIntegration() {
|
||||
if !setupToken {
|
||||
w.output.Info("You can set up the token later using environment variables:")
|
||||
w.output.Printf(" export GITHUB_TOKEN=your_personal_access_token")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -284,8 +288,9 @@ func (w *ConfigWizard) confirmConfiguration() error {
|
||||
w.output.Info("")
|
||||
confirmed := w.promptYesNo("Save this configuration?", true)
|
||||
if !confirmed {
|
||||
return fmt.Errorf("configuration canceled by user")
|
||||
return errors.New("configuration canceled by user")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -302,6 +307,7 @@ func (w *ConfigWizard) promptWithDefault(prompt, defaultValue string) string {
|
||||
if input == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return input
|
||||
}
|
||||
|
||||
@@ -314,6 +320,7 @@ func (w *ConfigWizard) promptSensitive(prompt string) string {
|
||||
if w.scanner.Scan() {
|
||||
return strings.TrimSpace(w.scanner.Text())
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -337,6 +344,7 @@ func (w *ConfigWizard) promptYesNo(prompt string, defaultValue bool) bool {
|
||||
return defaultValue
|
||||
default:
|
||||
w.output.Warning("Please answer 'y' or 'n'. Using default.")
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user