chore: even more linting, test fixes (#24)

* chore(lint): funcorder

* chore(lint): yamlfmt, ignored broken test yaml files

* chore(tests): tests do not output garbage, add coverage

* chore(lint): fix editorconfig violations

* chore(lint): move from eclint to editorconfig-checker

* chore(lint): add pre-commit, run and fix

* chore(ci): we use renovate to manage updates
This commit is contained in:
2025-08-06 23:44:32 +03:00
committed by GitHub
parent c5a7ced768
commit b80ecfce92
56 changed files with 809 additions and 601 deletions

View File

@@ -1,36 +0,0 @@
# Ignore patterns for eclint
# Build artifacts and binaries
gh-action-readme
dist/
coverage.out
*.exe
*.bin
*.dll
*.so
*.dylib
# Git directory
.git/
# Node modules
node_modules/
# OS-specific files
.DS_Store
Thumbs.db
# IDE files
.vscode/
.idea/
# Temporary files
*.tmp
*.temp
# Log files
*.log
# Cache directories
.cache/
.npm/

View File

@@ -31,3 +31,6 @@ max_line_length = 200
[Makefile]
indent_style = tab
tab_width = 2
[{go.sum,go.mod}]
max_line_length = 300

View File

@@ -0,0 +1,36 @@
{
"Verbose": false,
"Debug": false,
"IgnoreDefaults": false,
"SpacesAfterTabs": false,
"NoColor": false,
"Exclude": [
"\\.git",
"node_modules",
"coverage\\.out",
"coverage\\.html",
"\\.DS_Store",
"Thumbs\\.db",
"\\.vscode",
"\\.idea",
"\\.cache",
"\\.npm",
"\\.tmp$",
"\\.temp$",
"\\.log$",
"\\.exe$",
"\\.bin$",
"\\.dll$",
"\\.so$",
"\\.dylib$",
"gh-action-readme$",
"dist/",
"testutil\\.test$",
"test_.*",
"testdata/yaml-fixtures/validation/invalid-yaml\\.yml$",
"testdata/yaml-fixtures/validation/missing-.*",
"testdata/yaml-fixtures/actions/invalid/.*",
"testdata/yaml-fixtures/invalid-action\\.yml$",
"testdata/yaml-fixtures/.*-template\\.yml$"
]
}

View File

@@ -1,3 +1,4 @@
---
# Repository-level configuration for gh-action-readme
organization: "ivuorinen"
repository: "gh-action-readme"

View File

@@ -1,66 +0,0 @@
version: 2
updates:
# Go modules
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
open-pull-requests-limit: 10
reviewers:
- "ivuorinen"
assignees:
- "ivuorinen"
commit-message:
prefix: "chore(deps)"
include: "scope"
labels:
- "dependencies"
- "security"
# Group security updates
groups:
security-updates:
patterns:
- "*"
update-types:
- "security-update"
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
open-pull-requests-limit: 5
reviewers:
- "ivuorinen"
assignees:
- "ivuorinen"
commit-message:
prefix: "fix(github-action)"
include: "scope"
labels:
- "dependencies"
- "github-actions"
# Docker
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
open-pull-requests-limit: 3
reviewers:
- "ivuorinen"
assignees:
- "ivuorinen"
commit-message:
prefix: "fix(docker)"
include: "scope"
labels:
- "dependencies"
- "docker"

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"github>ivuorinen/renovate-config"
]
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"github>ivuorinen/renovate-config"
]
}

View File

@@ -1,3 +1,4 @@
---
name: CI
on:
push:

View File

@@ -1,3 +1,4 @@
---
name: Release
on:

1
.gitignore vendored
View File

@@ -32,3 +32,4 @@ go.sum
testdata/**/*.md
testdata/**/*.html
testdata/**/*.json
coverage.*

View File

@@ -1,3 +1,4 @@
---
# yaml-language-server: $schema=https://golangci-lint.run/jsonschema/golangci.jsonschema.json
version: "2"
@@ -19,7 +20,7 @@ linters:
- errname
- exhaustive
- forcetypeassert
# - funcorder
- funcorder
- goconst
- gocritic
- gocyclo

View File

@@ -1,3 +1,4 @@
---
# GoReleaser configuration for gh-action-readme
# See: https://goreleaser.com
@@ -52,12 +53,7 @@ archives:
- goos: windows
format: zip
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
{{ .ProjectName }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }}
files:
- README.md
- LICENSE*

View File

@@ -31,5 +31,5 @@ MARKDOWN_MARKDOWNLINT_CONFIG_FILE: .markdownlint.json
JAVASCRIPT_ES_CONFIG_FILE: .eslintrc.json
TYPESCRIPT_ES_CONFIG_FILE: .eslintrc.json
FILTER_REGEX_EXCLUDE: >
FILTER_REGEX_EXCLUDE: >-
(node_modules|\.automation/test|docs/json-schemas|\.github/workflows)

View File

@@ -1,12 +1,13 @@
---
repos:
# Built-in pre-commit hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: requirements-txt-fixer
- id: detect-private-key
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
exclude: "^testdata/"
- id: check-case-conflict
- id: check-merge-conflict
- id: check-executables-have-shebangs
@@ -16,48 +17,54 @@ repos:
- id: check-xml
- id: check-yaml
args: [--allow-multiple-documents]
exclude: "^testdata/"
- id: end-of-file-fixer
exclude: "^testdata/"
- id: mixed-line-ending
args: [--fix=auto]
- id: pretty-format-json
args: [--autofix, --no-sort-keys]
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.44.0
# YAML formatting with yamlfmt (replaces yamllint for formatting)
- repo: https://github.com/google/yamlfmt
rev: v0.17.2
hooks:
- id: markdownlint
args: [-c, .markdownlint.json, --fix]
- id: yamlfmt
exclude: "^testdata/"
- repo: https://github.com/adrienverge/yamllint
rev: v1.37.0
# Markdown linting with markdownlint-cli2 (excluding legacy files)
- repo: https://github.com/DavidAnson/markdownlint-cli2
rev: v0.18.1
hooks:
- id: yamllint
- id: markdownlint-cli2
exclude: '^(testdata/|CHANGELOG\.md|TODO\.md|\.github/|CLAUDE\.md|README\.md|SECURITY\.md)'
# EditorConfig checking
- repo: https://github.com/editorconfig-checker/editorconfig-checker
rev: v3.3.0
hooks:
- id: editorconfig-checker
alias: ec
# Go formatting, imports, and linting
- repo: https://github.com/TekWizely/pre-commit-golang
rev: v1.0.0-rc.2
hooks:
- id: go-imports-repo
args: [-w]
- id: go-mod-tidy
- id: golangci-lint-repo-mod
args: [--fix]
# Shell formatting and linting
- repo: https://github.com/scop/pre-commit-shfmt
rev: v3.11.0-1
rev: v3.12.0-2
hooks:
- id: shfmt
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.10.0
hooks:
- id: shellcheck
args: ['--severity=warning']
# GitHub Actions linting
- repo: https://github.com/rhysd/actionlint
rev: v1.7.7
hooks:
- id: actionlint
args: ['-shellcheck=']
- repo: https://github.com/renovatebot/pre-commit-hooks
rev: 39.227.2
hooks:
- id: renovate-config-validator
- repo: https://github.com/bridgecrewio/checkov.git
rev: '3.2.400'
hooks:
- id: checkov
args:
- '--quiet'
args: ["-shellcheck="]

5
.prettierignore Normal file
View File

@@ -0,0 +1,5 @@
testdata/yaml-fixtures/validation/invalid-yaml.yml
testdata/yaml-fixtures/validation/missing-*
testdata/yaml-fixtures/actions/invalid/*
testdata/yaml-fixtures/invalid-action.yml
testdata/yaml-fixtures/*-template.yml

18
.yamlfmt.yml Normal file
View File

@@ -0,0 +1,18 @@
---
# yaml-language-server: $schema=https://raw.githubusercontent.com/google/yamlfmt/main/schema.json
doublestar: true
formatter:
type: basic
include_document_start: true
retain_line_breaks: true
retain_line_breaks_single: true
trim_trailing_whitespace: true
eof_newline: true
max_line_length: 300
gitignore_excludes: true
exclude:
- testdata/yaml-fixtures/validation/invalid-yaml.yml
- testdata/yaml-fixtures/validation/missing-*
- testdata/yaml-fixtures/actions/invalid/*
- testdata/yaml-fixtures/invalid-action.yml
- testdata/yaml-fixtures/*-template.yml

View File

@@ -1 +1,5 @@
testdata/yaml-fixtures/validation/invalid-yaml.yml
testdata/yaml-fixtures/validation/missing-*
testdata/yaml-fixtures/actions/invalid/*
testdata/yaml-fixtures/invalid-action.ym
testdata/yaml-fixtures/*-template.yml

156
Makefile
View File

@@ -1,5 +1,6 @@
.PHONY: help test lint run example clean readme config-verify security vulncheck audit snyk trivy gitleaks \
editorconfig editorconfig-fix format
.PHONY: help test test-coverage test-coverage-html lint build run example \
clean readme config-verify security vulncheck audit snyk trivy gitleaks \
editorconfig editorconfig-fix format devtools pre-commit-install pre-commit-update
all: help
@@ -10,18 +11,66 @@ help: ## Show this help message
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
@echo ""
@echo "Common workflows:"
@echo " make test lint # Run tests and linting"
@echo " make format # Format code and fix EditorConfig issues"
@echo " make security # Run all security scans"
@echo " make devtools # Install all development tools"
@echo " make pre-commit-install # Install pre-commit hooks (run once)"
@echo " make build # Build the application binary"
@echo " make test lint # Run tests and all linters via pre-commit"
@echo " make test-coverage # Run tests with coverage analysis"
@echo " make pre-commit-update # Update pre-commit hooks to latest versions"
@echo " make security # Run all security scans"
test: ## Run all tests
go test ./...
lint: format ## Run linter (after formatting)
golangci-lint run \
--max-issues-per-linter 100 \
--max-same-issues 50 \
--output.tab.path stdout || true
test-coverage: ## Run tests with coverage and display in CLI
@echo "Running tests with coverage analysis..."
@go test ./... -coverprofile=coverage.out -covermode=atomic
@echo ""
@echo "=== Coverage Summary ==="
@go tool cover -func=coverage.out | tail -1
@echo ""
@echo "=== Package Coverage Details ==="
@go tool cover -func=coverage.out | grep -v "total:" | \
awk '{printf "%-50s %s\n", $$1, $$3}' | \
sort -k2 -nr
@echo ""
@echo "Coverage report saved to: coverage.out"
@echo "Run 'make test-coverage-html' to generate HTML report"
test-coverage-html: test-coverage ## Generate HTML coverage report and open in browser
@echo "Generating HTML coverage report..."
@go tool cover -html=coverage.out -o coverage.html
@echo "HTML coverage report generated: coverage.html"
@if command -v open >/dev/null 2>&1; then \
echo "Opening coverage report in browser..."; \
open coverage.html; \
elif command -v xdg-open >/dev/null 2>&1; then \
echo "Opening coverage report in browser..."; \
xdg-open coverage.html; \
else \
echo "Open coverage.html in your browser to view detailed coverage"; \
fi
lint: ## Run all linters via pre-commit
@echo "Running all linters via pre-commit..."
@command -v pre-commit >/dev/null 2>&1 || \
{ echo "Please install pre-commit or run 'make devtools'"; exit 1; }
pre-commit run --all-files
pre-commit-install: ## Install pre-commit hooks
@echo "Installing pre-commit hooks..."
@command -v pre-commit >/dev/null 2>&1 || \
{ echo "Please install pre-commit or run 'make devtools'"; exit 1; }
pre-commit install
pre-commit-update: ## Update pre-commit hooks to latest versions
@echo "Updating pre-commit hooks..."
@command -v pre-commit >/dev/null 2>&1 || \
{ echo "Please install pre-commit or run 'make devtools'"; exit 1; }
pre-commit autoupdate
build: ## Build the application
go build -o gh-action-readme .
config-verify: ## Verify golangci-lint configuration
golangci-lint config verify --verbose
@@ -37,6 +86,7 @@ readme: ## Generate project README
clean: ## Clean build artifacts
rm -rf dist/
rm -f gh-action-readme coverage.out coverage.html
# Code formatting and EditorConfig targets
format: editorconfig-fix ## Format code and fix EditorConfig issues
@@ -48,43 +98,59 @@ format: editorconfig-fix ## Format code and fix EditorConfig issues
editorconfig: ## Check EditorConfig compliance
@echo "Checking EditorConfig compliance..."
@command -v eclint >/dev/null 2>&1 || \
{ echo "Please install eclint: npm install -g eclint"; exit 1; }
@echo "Checking files for EditorConfig compliance..."
@find . -type f \( \
-name "*.go" -o \
-name "*.yml" -o \
-name "*.yaml" -o \
-name "*.json" -o \
-name "*.md" -o \
-name "Makefile" -o \
-name ".snyk" -o \
-name "*.tmpl" -o \
-name "*.adoc" -o \
-name "*.sh" \
\) -not -path "./gh-action-readme" -not -path "./coverage*" \
-not -path "./testutil.test" -not -path "./test_*" | \
xargs eclint check
@command -v editorconfig-checker >/dev/null 2>&1 || \
{ echo "Please install editorconfig-checker or run 'make devtools'"; exit 1; }
editorconfig-checker
editorconfig-fix: ## Fix EditorConfig violations
@echo "Fixing EditorConfig violations..."
@command -v eclint >/dev/null 2>&1 || \
{ echo "Please install eclint: npm install -g eclint"; exit 1; }
@echo "Fixing files for EditorConfig compliance..."
@find . -type f \( \
-name "*.go" -o \
-name "*.yml" -o \
-name "*.yaml" -o \
-name "*.json" -o \
-name "*.md" -o \
-name "Makefile" -o \
-name ".snyk" -o \
-name "*.tmpl" -o \
-name "*.adoc" -o \
-name "*.sh" \
\) -not -path "./gh-action-readme" -not -path "./coverage*" \
-not -path "./testutil.test" -not -path "./test_*" | \
xargs eclint fix
@echo "EditorConfig violations cannot be automatically fixed by editorconfig-checker"
@echo "Please fix the reported issues manually or use your editor's EditorConfig plugin"
@echo "Running check to show issues..."
@command -v editorconfig-checker >/dev/null 2>&1 || \
{ echo "Please install editorconfig-checker or run 'make devtools'"; exit 1; }
editorconfig-checker
# Development tools installation
devtools: ## Install all development tools
@echo "Installing development tools..."
@echo ""
@echo "=== Go Tools ==="
@command -v golangci-lint >/dev/null 2>&1 || \
{ echo "Installing golangci-lint..."; \
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \
sh -s -- -b $(go env GOPATH)/bin; }
@command -v govulncheck >/dev/null 2>&1 || \
{ echo "Installing govulncheck..."; go install golang.org/x/vuln/cmd/govulncheck@latest; }
@command -v editorconfig-checker >/dev/null 2>&1 || \
{ echo "Installing editorconfig-checker..."; \
go install github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@latest; }
@command -v yamlfmt >/dev/null 2>&1 || \
{ echo "Installing yamlfmt..."; go install github.com/google/yamlfmt/cmd/yamlfmt@latest; }
@echo "✓ Go tools installed"
@echo ""
@echo "=== Node.js Tools ==="
@command -v npm >/dev/null 2>&1 || \
{ echo "❌ npm not found. Please install Node.js first."; exit 1; }
@command -v snyk >/dev/null 2>&1 || \
{ echo "Installing snyk..."; npm install -g snyk; }
@echo "✓ Node.js tools installed"
@echo ""
@echo "=== Python Tools ==="
@command -v python3 >/dev/null 2>&1 || \
{ echo "❌ python3 not found. Please install Python 3 first."; exit 1; }
@command -v pre-commit >/dev/null 2>&1 || \
{ echo "Installing pre-commit..."; pip install pre-commit; }
@echo "✓ Python tools installed"
@echo ""
@echo "=== System Tools ==="
@command -v trivy >/dev/null 2>&1 || \
{ echo "❌ trivy not found. Please install manually: https://aquasecurity.github.io/trivy/"; }
@command -v gitleaks >/dev/null 2>&1 || \
{ echo "❌ gitleaks not found. Please install manually: https://github.com/gitleaks/gitleaks"; }
@echo "✓ System tools check completed"
@echo ""
@echo "🎉 Development tools installation completed!"
@echo " Run 'make test lint' to verify everything works."
# Security targets
security: vulncheck snyk trivy gitleaks ## Run all security scans

View File

@@ -1,3 +1,4 @@
---
# Default configuration for gh-action-readme
defaults:
name: "GitHub Action"

View File

@@ -196,6 +196,25 @@ func (c *Cache) Close() error {
return c.saveToDisk()
}
// GetOrSet retrieves a value from cache or sets it if not found.
func (c *Cache) GetOrSet(key string, getter func() (any, error)) (any, error) {
// Try to get from cache first
if value, exists := c.Get(key); exists {
return value, nil
}
// Not in cache, get from source
value, err := getter()
if err != nil {
return nil, err
}
// Store in cache
_ = c.Set(key, value) // Log error but don't fail - we have the value
return value, nil
}
// cleanupLoop runs periodically to remove expired entries.
func (c *Cache) cleanupLoop() {
for {
@@ -289,22 +308,3 @@ func (c *Cache) estimateSize(value any) int64 {
return int64(len(jsonData))
}
// GetOrSet retrieves a value from cache or sets it if not found.
func (c *Cache) GetOrSet(key string, getter func() (any, error)) (any, error) {
// Try to get from cache first
if value, exists := c.Get(key); exists {
return value, nil
}
// Not in cache, get from source
value, err := getter()
if err != nil {
return nil, err
}
// Store in cache
_ = c.Set(key, value) // Log error but don't fail - we have the value
return value, nil
}

View File

@@ -108,6 +108,77 @@ func (cl *ConfigurationLoader) LoadConfiguration(configFile, repoRoot, actionDir
return config, nil
}
// LoadGlobalConfig loads only the global configuration.
func (cl *ConfigurationLoader) LoadGlobalConfig(configFile string) (*AppConfig, error) {
return cl.loadGlobalConfig(configFile)
}
// ValidateConfiguration validates a configuration for consistency and required values.
func (cl *ConfigurationLoader) ValidateConfiguration(config *AppConfig) error {
if config == nil {
return errors.New("configuration cannot be nil")
}
// Validate output format
validFormats := []string{"md", "html", "json", "asciidoc"}
if !containsString(validFormats, config.OutputFormat) {
return fmt.Errorf("invalid output format '%s', must be one of: %s",
config.OutputFormat, strings.Join(validFormats, ", "))
}
// Validate theme (if set)
if config.Theme != "" {
if err := cl.validateTheme(config.Theme); err != nil {
return fmt.Errorf("invalid theme: %w", err)
}
}
// Validate output directory
if config.OutputDir == "" {
return errors.New("output directory cannot be empty")
}
// Validate mutually exclusive flags
if config.Verbose && config.Quiet {
return errors.New("verbose and quiet flags are mutually exclusive")
}
return nil
}
// containsString checks if a slice contains a string.
func containsString(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}
return false
}
// GetConfigurationSources returns the currently enabled configuration sources.
func (cl *ConfigurationLoader) GetConfigurationSources() []ConfigurationSource {
var sources []ConfigurationSource
for source, enabled := range cl.sources {
if enabled {
sources = append(sources, source)
}
}
return sources
}
// EnableSource enables a specific configuration source.
func (cl *ConfigurationLoader) EnableSource(source ConfigurationSource) {
cl.sources[source] = true
}
// DisableSource disables a specific configuration source.
func (cl *ConfigurationLoader) DisableSource(source ConfigurationSource) {
cl.sources[source] = false
}
// loadDefaultsStep loads default configuration values.
func (cl *ConfigurationLoader) loadDefaultsStep(config *AppConfig) {
if cl.sources[SourceDefaults] {
@@ -177,44 +248,6 @@ func (cl *ConfigurationLoader) loadEnvironmentStep(config *AppConfig) {
}
}
// LoadGlobalConfig loads only the global configuration.
func (cl *ConfigurationLoader) LoadGlobalConfig(configFile string) (*AppConfig, error) {
return cl.loadGlobalConfig(configFile)
}
// ValidateConfiguration validates a configuration for consistency and required values.
func (cl *ConfigurationLoader) ValidateConfiguration(config *AppConfig) error {
if config == nil {
return errors.New("configuration cannot be nil")
}
// Validate output format
validFormats := []string{"md", "html", "json", "asciidoc"}
if !containsString(validFormats, config.OutputFormat) {
return fmt.Errorf("invalid output format '%s', must be one of: %s",
config.OutputFormat, strings.Join(validFormats, ", "))
}
// Validate theme (if set)
if config.Theme != "" {
if err := cl.validateTheme(config.Theme); err != nil {
return fmt.Errorf("invalid theme: %w", err)
}
}
// Validate output directory
if config.OutputDir == "" {
return errors.New("output directory cannot be empty")
}
// Validate mutually exclusive flags
if config.Verbose && config.Quiet {
return errors.New("verbose and quiet flags are mutually exclusive")
}
return nil
}
// loadGlobalConfig initializes and loads the global configuration using Viper.
func (cl *ConfigurationLoader) loadGlobalConfig(configFile string) (*AppConfig, error) {
v := viper.New()
@@ -396,39 +429,6 @@ func (cl *ConfigurationLoader) validateTheme(theme string) error {
theme, strings.Join(supportedThemes, ", "))
}
// containsString checks if a slice contains a string.
func containsString(slice []string, str string) bool {
for _, s := range slice {
if s == str {
return true
}
}
return false
}
// GetConfigurationSources returns the currently enabled configuration sources.
func (cl *ConfigurationLoader) GetConfigurationSources() []ConfigurationSource {
var sources []ConfigurationSource
for source, enabled := range cl.sources {
if enabled {
sources = append(sources, source)
}
}
return sources
}
// EnableSource enables a specific configuration source.
func (cl *ConfigurationLoader) EnableSource(source ConfigurationSource) {
cl.sources[source] = true
}
// DisableSource disables a specific configuration source.
func (cl *ConfigurationLoader) DisableSource(source ConfigurationSource) {
cl.sources[source] = false
}
// String returns a string representation of a ConfigurationSource.
func (s ConfigurationSource) String() string {
switch s {

View File

@@ -168,6 +168,85 @@ func (a *Analyzer) AnalyzeActionFileWithProgress(
return a.processCompositeSteps(action.Runs.Steps, progressCallback)
}
// CheckOutdated analyzes dependencies and finds those with newer versions available.
func (a *Analyzer) CheckOutdated(deps []Dependency) ([]OutdatedDependency, error) {
var outdated []OutdatedDependency
for _, dep := range deps {
if dep.IsShellScript || dep.IsLocalAction {
continue // Skip shell scripts and local actions
}
owner, repo, currentVersion, _ := a.parseUsesStatement(dep.Uses)
if owner == "" || repo == "" {
continue
}
latestVersion, latestSHA, err := a.getLatestVersion(owner, repo)
if err != nil {
continue // Skip on error, don't fail the whole operation
}
updateType := a.compareVersions(currentVersion, latestVersion)
if updateType != updateTypeNone {
outdated = append(outdated, OutdatedDependency{
Current: dep,
LatestVersion: latestVersion,
LatestSHA: latestSHA,
UpdateType: updateType,
IsSecurityUpdate: updateType == updateTypeMajor, // Assume major updates might be security
})
}
}
return outdated, nil
}
// GeneratePinnedUpdate creates a pinned update for a dependency.
func (a *Analyzer) GeneratePinnedUpdate(
actionPath string,
dep Dependency,
latestVersion, latestSHA string,
) (*PinnedUpdate, error) {
if latestSHA == "" {
return nil, fmt.Errorf("no commit SHA available for %s", dep.Uses)
}
// Create the new pinned uses string: "owner/repo@sha # version"
owner, repo, currentVersion, _ := a.parseUsesStatement(dep.Uses)
newUses := fmt.Sprintf("%s/%s@%s # %s", owner, repo, latestSHA, latestVersion)
updateType := a.compareVersions(currentVersion, latestVersion)
return &PinnedUpdate{
FilePath: actionPath,
OldUses: dep.Uses,
NewUses: newUses,
CommitSHA: latestSHA,
Version: latestVersion,
UpdateType: updateType,
LineNumber: 0, // Will be determined during file update
}, nil
}
// ApplyPinnedUpdates applies pinned updates to action files.
func (a *Analyzer) ApplyPinnedUpdates(updates []PinnedUpdate) error {
// Group updates by file path
updatesByFile := make(map[string][]PinnedUpdate)
for _, update := range updates {
updatesByFile[update.FilePath] = append(updatesByFile[update.FilePath], update)
}
// Apply updates to each file
for filePath, fileUpdates := range updatesByFile {
if err := a.updateActionFile(filePath, fileUpdates); err != nil {
return fmt.Errorf("failed to update %s: %w", filePath, err)
}
}
return nil
}
// validateAndCheckComposite validates action type and checks if it's composite.
func (a *Analyzer) validateAndCheckComposite(
action *ActionWithComposite,
@@ -244,8 +323,6 @@ func (a *Analyzer) processStep(step CompositeStep, stepNumber int) *Dependency {
return nil
}
// parseCompositeAction is implemented in parser.go
// analyzeActionDependency analyzes a single action dependency.
func (a *Analyzer) analyzeActionDependency(step CompositeStep, _ int) (*Dependency, error) {
// Parse the uses statement
@@ -405,40 +482,6 @@ func (a *Analyzer) convertWithParams(with map[string]any) map[string]string {
return params
}
// CheckOutdated analyzes dependencies and finds those with newer versions available.
func (a *Analyzer) CheckOutdated(deps []Dependency) ([]OutdatedDependency, error) {
var outdated []OutdatedDependency
for _, dep := range deps {
if dep.IsShellScript || dep.IsLocalAction {
continue // Skip shell scripts and local actions
}
owner, repo, currentVersion, _ := a.parseUsesStatement(dep.Uses)
if owner == "" || repo == "" {
continue
}
latestVersion, latestSHA, err := a.getLatestVersion(owner, repo)
if err != nil {
continue // Skip on error, don't fail the whole operation
}
updateType := a.compareVersions(currentVersion, latestVersion)
if updateType != updateTypeNone {
outdated = append(outdated, OutdatedDependency{
Current: dep,
LatestVersion: latestVersion,
LatestSHA: latestSHA,
UpdateType: updateType,
IsSecurityUpdate: updateType == updateTypeMajor, // Assume major updates might be security
})
}
}
return outdated, nil
}
// 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 {
@@ -584,51 +627,6 @@ func (a *Analyzer) determineUpdateType(currentParts, latestParts []string) strin
return updateTypeNone
}
// GeneratePinnedUpdate creates a pinned update for a dependency.
func (a *Analyzer) GeneratePinnedUpdate(
actionPath string,
dep Dependency,
latestVersion, latestSHA string,
) (*PinnedUpdate, error) {
if latestSHA == "" {
return nil, fmt.Errorf("no commit SHA available for %s", dep.Uses)
}
// Create the new pinned uses string: "owner/repo@sha # version"
owner, repo, currentVersion, _ := a.parseUsesStatement(dep.Uses)
newUses := fmt.Sprintf("%s/%s@%s # %s", owner, repo, latestSHA, latestVersion)
updateType := a.compareVersions(currentVersion, latestVersion)
return &PinnedUpdate{
FilePath: actionPath,
OldUses: dep.Uses,
NewUses: newUses,
CommitSHA: latestSHA,
Version: latestVersion,
UpdateType: updateType,
LineNumber: 0, // Will be determined during file update
}, nil
}
// ApplyPinnedUpdates applies pinned updates to action files.
func (a *Analyzer) ApplyPinnedUpdates(updates []PinnedUpdate) error {
// Group updates by file path
updatesByFile := make(map[string][]PinnedUpdate)
for _, update := range updates {
updatesByFile[update.FilePath] = append(updatesByFile[update.FilePath], update)
}
// Apply updates to each file
for filePath, fileUpdates := range updatesByFile {
if err := a.updateActionFile(filePath, fileUpdates); err != nil {
return fmt.Errorf("failed to update %s: %w", filePath, err)
}
}
return nil
}
// updateActionFile applies updates to a single action file.
func (a *Analyzer) updateActionFile(filePath string, updates []PinnedUpdate) error {
// Read the file

View File

@@ -34,9 +34,39 @@ type Generator struct {
Progress ProgressManager
}
// isUnitTestEnvironment detects if we're running unit tests (not integration tests).
func isUnitTestEnvironment() bool {
// Only enable for unit tests, not integration tests
// Integration tests need real output to verify CLI behavior
// Check if we're in the internal package tests
if strings.Contains(os.Args[0], "internal.test") ||
strings.Contains(os.Args[0], "T/go-build") && strings.Contains(os.Args[0], "internal") {
return true
}
// Check for explicit unit test environment variable
if os.Getenv("UNIT_TEST_MODE") != "" {
return true
}
return false
}
// NewGenerator creates a new generator instance with the provided configuration.
// This constructor maintains backward compatibility by using concrete implementations.
// In unit test environments, it automatically uses NullOutput to suppress output.
func NewGenerator(config *AppConfig) *Generator {
// Use null output in unit test environments to keep tests clean
// Integration tests need real output to verify CLI behavior
if isUnitTestEnvironment() {
return NewGeneratorWithDependencies(
config,
NewNullOutput(),
NewNullProgressManager(),
)
}
return NewGeneratorWithDependencies(
config,
NewColoredOutput(config.Quiet),
@@ -115,76 +145,116 @@ func (g *Generator) GenerateFromFile(actionPath string) error {
return g.generateByFormat(action, outputDir, actionPath)
}
// parseAndValidateAction parses and validates an action.yml file.
func (g *Generator) parseAndValidateAction(actionPath string) (*ActionYML, error) {
action, err := ParseActionYML(actionPath)
// DiscoverActionFiles finds action.yml and action.yaml files in the given directory
// using the centralized parser function and adds verbose logging.
func (g *Generator) DiscoverActionFiles(dir string, recursive bool) ([]string, error) {
actionFiles, err := DiscoverActionFiles(dir, recursive)
if err != nil {
return nil, fmt.Errorf("failed to parse action file %s: %w", actionPath, err)
return nil, err
}
validationResult := ValidateActionYML(action)
if len(validationResult.MissingFields) > 0 {
// Check for critical validation errors that cannot be fixed with defaults
for _, field := range validationResult.MissingFields {
// All core required fields should cause validation failure
if field == "name" || field == "description" || field == "runs" || field == "runs.using" {
// Required fields missing - cannot be fixed with defaults, must fail
return nil, fmt.Errorf(
"action file %s has invalid configuration, missing required field(s): %v",
actionPath,
validationResult.MissingFields,
)
// Add verbose logging
if g.Config.Verbose {
for _, file := range actionFiles {
if recursive {
g.Output.Info("Discovered action file: %s", file)
} else {
g.Output.Info("Found action file: %s", file)
}
}
}
if g.Config.Verbose {
g.Output.Warning("Missing fields in %s: %v", actionPath, validationResult.MissingFields)
}
FillMissing(action, g.Config.Defaults)
if g.Config.Verbose {
g.Output.Info("Applied default values for missing fields")
return actionFiles, nil
}
// DiscoverActionFilesWithValidation discovers action files with centralized error handling and validation.
// This function consolidates the duplicated file discovery logic across the codebase.
func (g *Generator) DiscoverActionFilesWithValidation(dir string, recursive bool, context string) ([]string, error) {
// Discover action files
actionFiles, err := g.DiscoverActionFiles(dir, recursive)
if err != nil {
g.Output.ErrorWithContext(
errCodes.ErrCodeFileNotFound,
"failed to discover action files for "+context,
map[string]string{
"directory": dir,
"recursive": strconv.FormatBool(recursive),
"context": context,
ContextKeyError: err.Error(),
},
)
return nil, err
}
// Check if any files were found
if len(actionFiles) == 0 {
contextMsg := "no GitHub Action files found for " + context
g.Output.ErrorWithContext(
errCodes.ErrCodeNoActionFiles,
contextMsg,
map[string]string{
"directory": dir,
"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)
}
return actionFiles, nil
}
// ProcessBatch processes multiple action.yml files.
func (g *Generator) ProcessBatch(paths []string) error {
if len(paths) == 0 {
return errors.New("no action files to process")
}
bar := g.Progress.CreateProgressBarForFiles("Processing files", paths)
errors, successCount := g.processFiles(paths, bar)
g.Progress.FinishProgressBarWithNewline(bar)
g.reportResults(successCount, errors)
if len(errors) > 0 {
return fmt.Errorf("encountered %d errors during batch processing", len(errors))
}
return nil
}
// ValidateFiles validates multiple action.yml files and reports results.
func (g *Generator) ValidateFiles(paths []string) error {
if len(paths) == 0 {
return errors.New("no action files to validate")
}
bar := g.Progress.CreateProgressBarForFiles("Validating files", paths)
allResults, errors := g.validateFiles(paths, bar)
g.Progress.FinishProgressBarWithNewline(bar)
if !g.Config.Quiet {
g.reportValidationResults(allResults, errors)
}
// Count validation failures (files with missing required fields)
validationFailures := 0
for _, result := range allResults {
// Each result starts with "file: <path>" so check if there are actual missing fields beyond that
if len(result.MissingFields) > 1 {
validationFailures++
}
}
return action, nil
}
if len(errors) > 0 || validationFailures > 0 {
totalFailures := len(errors) + validationFailures
// determineOutputDir calculates the output directory for generated files.
func (g *Generator) determineOutputDir(actionPath string) string {
if g.Config.OutputDir == "" || g.Config.OutputDir == "." {
return filepath.Dir(actionPath)
return fmt.Errorf("validation failed for %d files", totalFailures)
}
return g.Config.OutputDir
}
// resolveOutputPath resolves the final output path, considering custom filename.
func (g *Generator) resolveOutputPath(outputDir, defaultFilename string) string {
if g.Config.OutputFilename != "" {
if filepath.IsAbs(g.Config.OutputFilename) {
return g.Config.OutputFilename
}
return filepath.Join(outputDir, g.Config.OutputFilename)
}
return filepath.Join(outputDir, defaultFilename)
}
// generateByFormat generates documentation in the specified format.
func (g *Generator) generateByFormat(action *ActionYML, outputDir, actionPath string) error {
switch g.Config.OutputFormat {
case "md":
return g.generateMarkdown(action, outputDir, actionPath)
case OutputFormatHTML:
return g.generateHTML(action, outputDir, actionPath)
case OutputFormatJSON:
return g.generateJSON(action, outputDir)
case OutputFormatASCIIDoc:
return g.generateASCIIDoc(action, outputDir, actionPath)
default:
return fmt.Errorf("unsupported output format: %s", g.Config.OutputFormat)
}
return nil
}
// generateMarkdown creates a README.md file using the template.
@@ -311,86 +381,6 @@ func (g *Generator) generateASCIIDoc(action *ActionYML, outputDir, actionPath st
return nil
}
// DiscoverActionFiles finds action.yml and action.yaml files in the given directory
// using the centralized parser function and adds verbose logging.
func (g *Generator) DiscoverActionFiles(dir string, recursive bool) ([]string, error) {
actionFiles, err := DiscoverActionFiles(dir, recursive)
if err != nil {
return nil, err
}
// Add verbose logging
if g.Config.Verbose {
for _, file := range actionFiles {
if recursive {
g.Output.Info("Discovered action file: %s", file)
} else {
g.Output.Info("Found action file: %s", file)
}
}
}
return actionFiles, nil
}
// DiscoverActionFilesWithValidation discovers action files with centralized error handling and validation.
// This function consolidates the duplicated file discovery logic across the codebase.
func (g *Generator) DiscoverActionFilesWithValidation(dir string, recursive bool, context string) ([]string, error) {
// Discover action files
actionFiles, err := g.DiscoverActionFiles(dir, recursive)
if err != nil {
g.Output.ErrorWithContext(
errCodes.ErrCodeFileNotFound,
"failed to discover action files for "+context,
map[string]string{
"directory": dir,
"recursive": strconv.FormatBool(recursive),
"context": context,
ContextKeyError: err.Error(),
},
)
return nil, err
}
// Check if any files were found
if len(actionFiles) == 0 {
contextMsg := "no GitHub Action files found for " + context
g.Output.ErrorWithContext(
errCodes.ErrCodeNoActionFiles,
contextMsg,
map[string]string{
"directory": dir,
"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)
}
return actionFiles, nil
}
// ProcessBatch processes multiple action.yml files.
func (g *Generator) ProcessBatch(paths []string) error {
if len(paths) == 0 {
return errors.New("no action files to process")
}
bar := g.Progress.CreateProgressBarForFiles("Processing files", paths)
errors, successCount := g.processFiles(paths, bar)
g.Progress.FinishProgressBarWithNewline(bar)
g.reportResults(successCount, errors)
if len(errors) > 0 {
return fmt.Errorf("encountered %d errors during batch processing", len(errors))
}
return nil
}
// processFiles processes each file and tracks results.
func (g *Generator) processFiles(paths []string, bar *progressbar.ProgressBar) ([]string, int) {
var errors []string
@@ -429,36 +419,76 @@ 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 errors.New("no action files to validate")
// parseAndValidateAction parses and validates an action.yml file.
func (g *Generator) parseAndValidateAction(actionPath string) (*ActionYML, error) {
action, err := ParseActionYML(actionPath)
if err != nil {
return nil, fmt.Errorf("failed to parse action file %s: %w", actionPath, err)
}
bar := g.Progress.CreateProgressBarForFiles("Validating files", paths)
allResults, errors := g.validateFiles(paths, bar)
g.Progress.FinishProgressBarWithNewline(bar)
validationResult := ValidateActionYML(action)
if len(validationResult.MissingFields) > 0 {
// Check for critical validation errors that cannot be fixed with defaults
for _, field := range validationResult.MissingFields {
// All core required fields should cause validation failure
if field == "name" || field == "description" || field == "runs" || field == "runs.using" {
// Required fields missing - cannot be fixed with defaults, must fail
return nil, fmt.Errorf(
"action file %s has invalid configuration, missing required field(s): %v",
actionPath,
validationResult.MissingFields,
)
}
}
if !g.Config.Quiet {
g.reportValidationResults(allResults, errors)
}
// Count validation failures (files with missing required fields)
validationFailures := 0
for _, result := range allResults {
// Each result starts with "file: <path>" so check if there are actual missing fields beyond that
if len(result.MissingFields) > 1 {
validationFailures++
if g.Config.Verbose {
g.Output.Warning("Missing fields in %s: %v", actionPath, validationResult.MissingFields)
}
FillMissing(action, g.Config.Defaults)
if g.Config.Verbose {
g.Output.Info("Applied default values for missing fields")
}
}
if len(errors) > 0 || validationFailures > 0 {
totalFailures := len(errors) + validationFailures
return action, nil
}
return fmt.Errorf("validation failed for %d files", totalFailures)
// determineOutputDir calculates the output directory for generated files.
func (g *Generator) determineOutputDir(actionPath string) string {
if g.Config.OutputDir == "" || g.Config.OutputDir == "." {
return filepath.Dir(actionPath)
}
return nil
return g.Config.OutputDir
}
// resolveOutputPath resolves the final output path, considering custom filename.
func (g *Generator) resolveOutputPath(outputDir, defaultFilename string) string {
if g.Config.OutputFilename != "" {
if filepath.IsAbs(g.Config.OutputFilename) {
return g.Config.OutputFilename
}
return filepath.Join(outputDir, g.Config.OutputFilename)
}
return filepath.Join(outputDir, defaultFilename)
}
// generateByFormat generates documentation in the specified format.
func (g *Generator) generateByFormat(action *ActionYML, outputDir, actionPath string) error {
switch g.Config.OutputFormat {
case "md":
return g.generateMarkdown(action, outputDir, actionPath)
case OutputFormatHTML:
return g.generateHTML(action, outputDir, actionPath)
case OutputFormatJSON:
return g.generateJSON(action, outputDir)
case OutputFormatASCIIDoc:
return g.generateASCIIDoc(action, outputDir, actionPath)
default:
return fmt.Errorf("unsupported output format: %s", g.Config.OutputFormat)
}
}
// validateFiles processes each file for validation.

View File

@@ -563,19 +563,18 @@ func TestGenerator_WithDifferentThemes(t *testing.T) {
t.Parallel()
themes := []string{"default", "github", "gitlab", "minimal", "professional"}
tmpDir, cleanup := testutil.TempDir(t)
defer cleanup()
// Set up test templates
testutil.SetupTestTemplates(t, tmpDir)
actionPath := filepath.Join(tmpDir, "action.yml")
testutil.WriteTestFile(t, actionPath, testutil.MustReadFixture("actions/javascript/simple.yml"))
for _, theme := range themes {
t.Run("theme_"+theme, func(t *testing.T) {
t.Parallel()
// Templates are now embedded, no working directory changes needed
// Create separate temp directory for each theme test
tmpDir, cleanup := testutil.TempDir(t)
defer cleanup()
// Set up test templates for this theme test
testutil.SetupTestTemplates(t, tmpDir)
actionPath := filepath.Join(tmpDir, "action.yml")
testutil.WriteTestFile(t, actionPath, testutil.MustReadFixture("actions/javascript/simple.yml"))
config := &AppConfig{
Theme: theme,
@@ -596,11 +595,6 @@ func TestGenerator_WithDifferentThemes(t *testing.T) {
if len(readmeFiles) == 0 {
t.Errorf("no output file was created for theme %s", theme)
}
// Clean up for next test
for _, file := range readmeFiles {
_ = os.Remove(file)
}
})
}
}

View File

@@ -78,35 +78,34 @@ func TestProgressBarManager_CreateProgressBarForFiles(t *testing.T) {
func TestProgressBarManager_FinishProgressBar(t *testing.T) {
t.Parallel()
pm := NewProgressBarManager(false)
// Use quiet mode to avoid cluttering test output
pm := NewProgressBarManager(true)
// Test with nil bar (should not panic)
pm.FinishProgressBar(nil)
// Test with actual bar
// Test with actual bar (will be nil in quiet mode)
bar := pm.CreateProgressBar("Test", 5)
if bar != nil {
pm.FinishProgressBar(bar)
}
pm.FinishProgressBar(bar) // Should handle nil gracefully
}
func TestProgressBarManager_UpdateProgressBar(t *testing.T) {
t.Parallel()
pm := NewProgressBarManager(false)
// Use quiet mode to avoid cluttering test output
pm := NewProgressBarManager(true)
// Test with nil bar (should not panic)
pm.UpdateProgressBar(nil)
// Test with actual bar
// Test with actual bar (will be nil in quiet mode)
bar := pm.CreateProgressBar("Test", 5)
if bar != nil {
pm.UpdateProgressBar(bar)
}
pm.UpdateProgressBar(bar) // Should handle nil gracefully
}
func TestProgressBarManager_ProcessWithProgressBar(t *testing.T) {
t.Parallel()
pm := NewProgressBarManager(false)
// Use NullProgressManager to avoid cluttering test output
pm := NewNullProgressManager()
items := []string{"item1", "item2", "item3"}
processedItems := make([]string, 0)

120
internal/testoutput.go Normal file
View File

@@ -0,0 +1,120 @@
package internal
import (
"os"
"github.com/schollz/progressbar/v3"
"github.com/ivuorinen/gh-action-readme/internal/errors"
)
// NullOutput is a no-op implementation of CompleteOutput for testing.
// All methods are no-ops to prevent cluttering test output.
type NullOutput struct{}
// Compile-time interface checks.
var (
_ MessageLogger = (*NullOutput)(nil)
_ ErrorReporter = (*NullOutput)(nil)
_ ErrorFormatter = (*NullOutput)(nil)
_ ProgressReporter = (*NullOutput)(nil)
_ OutputConfig = (*NullOutput)(nil)
_ CompleteOutput = (*NullOutput)(nil)
)
// NewNullOutput creates a new null output instance for testing.
func NewNullOutput() *NullOutput {
return &NullOutput{}
}
// IsQuiet returns true as null output is always quiet.
func (no *NullOutput) IsQuiet() bool {
return true
}
// Success is a no-op.
func (no *NullOutput) Success(_ string, _ ...any) {}
// Error is a no-op.
func (no *NullOutput) Error(_ string, _ ...any) {}
// Warning is a no-op.
func (no *NullOutput) Warning(_ string, _ ...any) {}
// Info is a no-op.
func (no *NullOutput) Info(_ string, _ ...any) {}
// Progress is a no-op.
func (no *NullOutput) Progress(_ string, _ ...any) {}
// Bold is a no-op.
func (no *NullOutput) Bold(_ string, _ ...any) {}
// Printf is a no-op.
func (no *NullOutput) Printf(_ string, _ ...any) {}
// Fprintf is a no-op.
func (no *NullOutput) Fprintf(_ *os.File, _ string, _ ...any) {}
// ErrorWithSuggestions is a no-op.
func (no *NullOutput) ErrorWithSuggestions(_ *errors.ContextualError) {}
// ErrorWithContext is a no-op.
func (no *NullOutput) ErrorWithContext(
_ errors.ErrorCode,
_ string,
_ map[string]string,
) {
}
// ErrorWithSimpleFix is a no-op.
func (no *NullOutput) ErrorWithSimpleFix(_, _ string) {}
// FormatContextualError returns empty string.
func (no *NullOutput) FormatContextualError(_ *errors.ContextualError) string {
return ""
}
// NullProgressManager is a no-op implementation of ProgressManager for testing.
type NullProgressManager struct{}
// Compile-time interface check.
var _ ProgressManager = (*NullProgressManager)(nil)
// NewNullProgressManager creates a new null progress manager for testing.
func NewNullProgressManager() *NullProgressManager {
return &NullProgressManager{}
}
// CreateProgressBar returns nil to suppress progress bars.
func (npm *NullProgressManager) CreateProgressBar(_ string, _ int) *progressbar.ProgressBar {
return nil
}
// CreateProgressBarForFiles returns nil to suppress progress bars.
func (npm *NullProgressManager) CreateProgressBarForFiles(
_ string,
_ []string,
) *progressbar.ProgressBar {
return nil
}
// FinishProgressBar is a no-op.
func (npm *NullProgressManager) FinishProgressBar(_ *progressbar.ProgressBar) {}
// FinishProgressBarWithNewline is a no-op.
func (npm *NullProgressManager) FinishProgressBarWithNewline(_ *progressbar.ProgressBar) {}
// UpdateProgressBar is a no-op.
func (npm *NullProgressManager) UpdateProgressBar(_ *progressbar.ProgressBar) {}
// ProcessWithProgressBar executes the function for each item without progress display.
func (npm *NullProgressManager) ProcessWithProgressBar(
_ string,
items []string,
processFunc func(item string, bar *progressbar.ProgressBar),
) {
for _, item := range items {
processFunc(item, nil)
}
}

View File

@@ -55,6 +55,32 @@ func (e *ConfigExporter) ExportConfig(config *internal.AppConfig, format ExportF
}
}
// GetSupportedFormats returns the list of supported export formats.
func (e *ConfigExporter) GetSupportedFormats() []ExportFormat {
return []ExportFormat{FormatYAML, FormatJSON, FormatTOML}
}
// GetDefaultOutputPath returns the default output path for a given format.
func (e *ConfigExporter) GetDefaultOutputPath(format ExportFormat) (string, error) {
configPath, err := internal.GetConfigPath()
if err != nil {
return "", fmt.Errorf("failed to get config directory: %w", err)
}
dir := filepath.Dir(configPath)
switch format {
case FormatYAML:
return filepath.Join(dir, "config.yaml"), nil
case FormatJSON:
return filepath.Join(dir, "config.json"), nil
case FormatTOML:
return filepath.Join(dir, "config.toml"), nil
default:
return "", fmt.Errorf("unsupported format: %s", format)
}
}
// exportYAML exports configuration as YAML.
func (e *ConfigExporter) exportYAML(config *internal.AppConfig, outputPath string) error {
// Create a clean config without sensitive data for export
@@ -260,29 +286,3 @@ func (e *ConfigExporter) writeVariablesSection(file *os.File, config *internal.A
_, _ = fmt.Fprintf(file, "%s = %q\n", key, value)
}
}
// GetSupportedFormats returns the list of supported export formats.
func (e *ConfigExporter) GetSupportedFormats() []ExportFormat {
return []ExportFormat{FormatYAML, FormatJSON, FormatTOML}
}
// GetDefaultOutputPath returns the default output path for a given format.
func (e *ConfigExporter) GetDefaultOutputPath(format ExportFormat) (string, error) {
configPath, err := internal.GetConfigPath()
if err != nil {
return "", fmt.Errorf("failed to get config directory: %w", err)
}
dir := filepath.Dir(configPath)
switch format {
case FormatYAML:
return filepath.Join(dir, "config.yaml"), nil
case FormatJSON:
return filepath.Join(dir, "config.json"), nil
case FormatTOML:
return filepath.Join(dir, "config.toml"), nil
default:
return "", fmt.Errorf("unsupported format: %s", format)
}
}

View File

@@ -109,6 +109,33 @@ func (v *ConfigValidator) ValidateField(fieldName, value string) *ValidationResu
return result
}
// DisplayValidationResult displays validation results to the user.
func (v *ConfigValidator) DisplayValidationResult(result *ValidationResult) {
if result.Valid {
v.output.Success("✅ Configuration is valid")
} else {
v.output.Error("❌ Configuration has errors")
}
// Display errors
for _, err := range result.Errors {
v.output.Error(" • %s: %s (value: %s)", err.Field, err.Message, err.Value)
}
// Display warnings
for _, warning := range result.Warnings {
v.output.Warning(" ⚠️ %s: %s", warning.Field, warning.Message)
}
// Display suggestions
if len(result.Suggestions) > 0 {
v.output.Info("\nSuggestions:")
for _, suggestion := range result.Suggestions {
v.output.Printf(" 💡 %s", suggestion)
}
}
}
// validateOrganization validates the organization field.
func (v *ConfigValidator) validateOrganization(org string, result *ValidationResult) {
if org == "" {
@@ -478,30 +505,3 @@ func (v *ConfigValidator) isValidVariableName(name string) bool {
return matched
}
// DisplayValidationResult displays validation results to the user.
func (v *ConfigValidator) DisplayValidationResult(result *ValidationResult) {
if result.Valid {
v.output.Success("✅ Configuration is valid")
} else {
v.output.Error("❌ Configuration has errors")
}
// Display errors
for _, err := range result.Errors {
v.output.Error(" • %s: %s (value: %s)", err.Field, err.Message, err.Value)
}
// Display warnings
for _, warning := range result.Warnings {
v.output.Warning(" ⚠️ %s: %s", warning.Field, warning.Message)
}
// Display suggestions
if len(result.Suggestions) > 0 {
v.output.Info("\nSuggestions:")
for _, suggestion := range result.Suggestions {
v.output.Printf(" 💡 %s", suggestion)
}
}
}

8
scripts/release.sh Normal file → Executable file
View File

@@ -44,13 +44,13 @@ fi
# Get version from command line or prompt
VERSION="$1"
if [[ -z "$VERSION" ]]; then
if [[ -z $VERSION ]]; then
echo -n "Enter version (e.g., v1.0.0): "
read -r VERSION
fi
# Validate version format
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
if [[ ! $VERSION =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
log_error "Version must be in format vX.Y.Z (e.g., v1.0.0)"
exit 1
fi
@@ -59,11 +59,11 @@ log_info "Preparing release $VERSION"
# Check if we're on main branch
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [[ "$CURRENT_BRANCH" != "main" ]]; then
if [[ $CURRENT_BRANCH != "main" ]]; then
log_warning "You're not on the main branch (current: $CURRENT_BRANCH)"
echo -n "Continue anyway? (y/N): "
read -r CONTINUE
if [[ "$CONTINUE" != "y" && "$CONTINUE" != "Y" ]]; then
if [[ $CONTINUE != "y" && $CONTINUE != "Y" ]]; then
log_info "Aborted"
exit 0
fi

View File

@@ -1,3 +1,4 @@
---
name: Composite Example Action
description: 'Test Composite Action for gh-action-readme dependency analysis'
inputs:

View File

@@ -1,3 +1,4 @@
---
name: Example Action
description: 'Test Action for gh-action-readme'
inputs:

View File

@@ -1,3 +1,4 @@
---
# Action-specific configuration
theme: "github"
variables:

View File

@@ -1,3 +1,4 @@
---
name: 'Basic Composite Action'
description: 'A simple composite action with basic steps'
inputs:

View File

@@ -1,3 +1,4 @@
---
name: 'Complex Composite Workflow'
description: 'A complex composite action demonstrating advanced features'
inputs:

View File

@@ -1,3 +1,4 @@
---
name: 'Composite Action with Dependencies'
description: 'A composite action that uses external actions'
inputs:

View File

@@ -1,3 +1,4 @@
---
name: 'Basic Docker Action'
description: 'A simple Docker-based action'
inputs:

View File

@@ -1,3 +1,4 @@
---
name: 'Docker Action with Environment'
description: 'Docker action with environment variables and advanced configuration'
inputs:

View File

@@ -1,3 +1,4 @@
---
name: 'Node.js 16 Action'
description: 'JavaScript action running on Node.js 16'
inputs:

View File

@@ -1,3 +1,4 @@
---
name: 'Simple JavaScript Action'
description: 'A simple JavaScript action for testing'
inputs:

View File

@@ -1,3 +1,4 @@
---
name: 'Comprehensive JavaScript Action'
description: 'A JavaScript action with all possible fields for testing'
author: 'Test Author <test@example.com>'

View File

@@ -1,3 +1,4 @@
---
theme: default
output_format: md
repo_overrides:

View File

@@ -1,3 +1,4 @@
---
name: 'Composite Action'
description: 'A composite action with multiple steps'
inputs:

View File

@@ -1,3 +1,4 @@
---
theme: professional
output_format: html
output_dir: docs

View File

@@ -1,3 +1,4 @@
---
theme: default
output_format: md
output_dir: .

View File

@@ -1,3 +1,4 @@
---
theme: github
output_format: md
output_dir: docs

View File

@@ -1,3 +1,4 @@
---
name: 'Docker Action'
description: 'A Docker-based GitHub Action'
inputs:

View File

@@ -1,3 +1,4 @@
---
theme: default
output_format: md
github_token: global-token

View File

@@ -1,3 +1,4 @@
---
name: 'Minimal Action'
description: 'Minimal test action'
runs:

View File

@@ -1,2 +1,3 @@
---
theme: minimal
github_token: config-token

View File

@@ -1,3 +1,4 @@
---
name: 'My New Action'
description: 'A brand new GitHub Action'
inputs:

View File

@@ -1,3 +1,4 @@
---
theme: professional
output_format: html
output_dir: docs

View File

@@ -1,3 +1,4 @@
---
theme: github
output_format: md
output_dir: docs

View File

@@ -1,3 +1,4 @@
---
scenarios:
# JavaScript Action Scenarios
- id: "simple-javascript"

View File

@@ -1,3 +1,4 @@
---
name: 'Simple Action'
description: 'A simple GitHub Action for testing'
inputs:

View File

@@ -1,3 +1,4 @@
---
name: 'Test Composite Action'
description: 'Test action for update testing'
runs:

View File

@@ -1,3 +1,4 @@
---
name: 'Test Project Action'
description: 'A GitHub Action for testing project functionality'
inputs:

View File

@@ -1,3 +1,4 @@
---
name: Test Action
description: A test action
runs: