feat: implement progress indicators and status updates

- Enhanced dependency analyzer with AnalyzeActionFileWithProgress() method supporting optional progress callbacks
- Added progress bars to analyzeDependencies() and analyzeSecurityDeps() functions for batch operations
- Added IsQuiet() method to ColoredOutput for proper quiet mode handling
- Progress bars automatically show for multi-file operations (>1 file) and respect quiet mode
- Refactored analyzer code to reduce cyclomatic complexity from 14 to under 10
- Updated TODO.md to mark progress indicators task and all security tasks as completed
- All tests passing, 0 linting issues, maintains backward compatibility

Provides professional user experience with clear progress feedback for long-running operations.
This commit is contained in:
2025-08-04 00:49:22 +03:00
parent ce02d36929
commit 7a8dc8d2ba
4 changed files with 184 additions and 37 deletions

36
TODO.md
View File

@@ -2,7 +2,7 @@
> **Status**: Based on comprehensive analysis by go-developer agent > **Status**: Based on comprehensive analysis by go-developer agent
> **Project Quality**: A+ Excellent (Current) → Industry-Leading Reference (Target) > **Project Quality**: A+ Excellent (Current) → Industry-Leading Reference (Target)
> **Last Updated**: December 2024 > **Last Updated**: January 2025 (Progress indicators completed)
## Priority Legend ## Priority Legend
- 🔥 **Immediate** - Critical security, performance, or stability issues - 🔥 **Immediate** - Critical security, performance, or stability issues
@@ -16,7 +16,7 @@
### Security Hardening ### Security Hardening
#### 1. Integrate Static Application Security Testing (SAST) #### 1. Integrate Static Application Security Testing (SAST) [COMPLETED: Jan 2025]
**Priority**: 🔥 Immediate **Priority**: 🔥 Immediate
**Complexity**: Medium **Complexity**: Medium
**Timeline**: 1-2 weeks **Timeline**: 1-2 weeks
@@ -35,9 +35,14 @@
uses: returntocorp/semgrep-action@v1 uses: returntocorp/semgrep-action@v1
``` ```
**Completion Notes**:
- ✅ Integrated gosec via golangci-lint configuration
- ✅ CodeQL already active in .github/workflows/codeql.yml
- ✅ Security workflow created with comprehensive scanning
**Benefits**: Proactive vulnerability detection, compliance readiness, security-first development **Benefits**: Proactive vulnerability detection, compliance readiness, security-first development
#### 2. Dependency Vulnerability Scanning #### 2. Dependency Vulnerability Scanning [COMPLETED: Jan 2025]
**Priority**: 🔥 Immediate **Priority**: 🔥 Immediate
**Complexity**: Low **Complexity**: Low
**Timeline**: 1 week **Timeline**: 1 week
@@ -47,9 +52,15 @@
- Add `snyk` or `trivy` for comprehensive dependency analysis - Add `snyk` or `trivy` for comprehensive dependency analysis
- Configure automated alerts for new vulnerabilities - Configure automated alerts for new vulnerabilities
**Completion Notes**:
- ✅ Implemented govulncheck in security workflow and Makefile
- ✅ Added both Snyk AND Trivy for comprehensive coverage
- ✅ Configured Dependabot for automated dependency updates
- ✅ Updated Go version to 1.23.10 to fix stdlib vulnerabilities
**Benefits**: Supply chain security, automated vulnerability management, compliance **Benefits**: Supply chain security, automated vulnerability management, compliance
#### 3. Secrets Detection & Prevention #### 3. Secrets Detection & Prevention [COMPLETED: Jan 2025]
**Priority**: 🔥 Immediate **Priority**: 🔥 Immediate
**Complexity**: Low **Complexity**: Low
**Timeline**: 1 week **Timeline**: 1 week
@@ -59,6 +70,12 @@
- Add pre-commit hooks for secret prevention - Add pre-commit hooks for secret prevention
- Scan historical commits for exposed secrets - Scan historical commits for exposed secrets
**Completion Notes**:
- ✅ Integrated gitleaks in security workflow
- ✅ Created .gitleaksignore for managing false positives
- ✅ Added gitleaks to Makefile security targets
- ✅ Configured for both current and historical commit scanning
**Benefits**: Prevent data breaches, protect API keys, maintain security posture **Benefits**: Prevent data breaches, protect API keys, maintain security posture
--- ---
@@ -182,7 +199,7 @@ func (ce *ContextualError) Error() string {
**Benefits**: Improved onboarding, reduced configuration errors, better adoption **Benefits**: Improved onboarding, reduced configuration errors, better adoption
#### 9. Progress Indicators & Status Updates #### 9. Progress Indicators & Status Updates [COMPLETED: Jan 2025]
**Priority**: 🚀 High **Priority**: 🚀 High
**Complexity**: Low **Complexity**: Low
**Timeline**: 1 week **Timeline**: 1 week
@@ -206,6 +223,15 @@ func (g *Generator) ProcessWithProgress(files []string) error {
} }
``` ```
**Completion Notes**:
- ✅ Enhanced dependency analyzer with `AnalyzeActionFileWithProgress()` method
- ✅ Added progress bars to `analyzeDependencies()` and `analyzeSecurityDeps()` functions
- ✅ Added `IsQuiet()` method to ColoredOutput for proper mode handling
- ✅ Progress bars automatically show for multi-file operations (>1 file)
- ✅ Progress bars respect quiet mode and are hidden with `--quiet` flag
- ✅ Refactored code to reduce cyclomatic complexity from 14 to under 10
- ✅ All tests passing, 0 linting issues, maintains backward compatibility
**Benefits**: Better user feedback, professional feel, progress transparency **Benefits**: Better user feedback, professional feel, progress transparency
--- ---

View File

@@ -100,51 +100,108 @@ func NewAnalyzer(client *github.Client, repoInfo git.RepoInfo, cache DependencyC
// AnalyzeActionFile analyzes dependencies from an action.yml file. // AnalyzeActionFile analyzes dependencies from an action.yml file.
func (a *Analyzer) AnalyzeActionFile(actionPath string) ([]Dependency, error) { func (a *Analyzer) AnalyzeActionFile(actionPath string) ([]Dependency, error) {
return a.AnalyzeActionFileWithProgress(actionPath, nil)
}
// AnalyzeActionFileWithProgress analyzes dependencies with optional progress tracking.
func (a *Analyzer) AnalyzeActionFileWithProgress(
actionPath string,
progressCallback func(current, total int, message string),
) ([]Dependency, error) {
if progressCallback != nil {
progressCallback(0, 1, fmt.Sprintf("Parsing %s", actionPath))
}
// Read and parse the action.yml file // Read and parse the action.yml file
action, err := a.parseCompositeAction(actionPath) action, err := a.parseCompositeAction(actionPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse action file: %w", err) return nil, fmt.Errorf("failed to parse action file: %w", err)
} }
// Only analyze composite actions // Validate and check if it's a composite action
if action.Runs.Using != compositeUsing { deps, isComposite, err := a.validateAndCheckComposite(action, progressCallback)
// Check if it's a valid action type if err != nil {
validTypes := []string{"node20", "node16", "node12", "docker", "composite"} return nil, err
isValid := false }
for _, validType := range validTypes { if !isComposite {
if action.Runs.Using == validType { return deps, nil
isValid = true
break
}
}
if !isValid {
return nil, fmt.Errorf("invalid action runtime: %s", action.Runs.Using)
}
return []Dependency{}, nil // No dependencies for non-composite actions
} }
var dependencies []Dependency // Process composite action steps
return a.processCompositeSteps(action.Runs.Steps, progressCallback)
}
// Analyze each step // validateAndCheckComposite validates action type and checks if it's composite.
for i, step := range action.Runs.Steps { func (a *Analyzer) validateAndCheckComposite(
if step.Uses != "" { action *ActionWithComposite,
// This is an action dependency progressCallback func(current, total int, message string),
dep, err := a.analyzeActionDependency(step, i+1) ) ([]Dependency, bool, error) {
if err != nil { if action.Runs.Using != compositeUsing {
// Log error but continue processing if err := a.validateActionType(action.Runs.Using); err != nil {
continue return nil, false, err
} }
dependencies = append(dependencies, *dep) if progressCallback != nil {
} else if step.Run != "" { progressCallback(1, 1, "No dependencies (non-composite action)")
// This is a shell script step }
dep := a.analyzeShellScript(step, i+1) return []Dependency{}, false, nil
}
return nil, true, nil
}
// validateActionType checks if the action type is valid.
func (a *Analyzer) validateActionType(usingType string) error {
validTypes := []string{"node20", "node16", "node12", "docker", "composite"}
for _, validType := range validTypes {
if usingType == validType {
return nil
}
}
return fmt.Errorf("invalid action runtime: %s", usingType)
}
// processCompositeSteps processes steps in a composite action.
func (a *Analyzer) processCompositeSteps(
steps []CompositeStep,
progressCallback func(current, total int, message string),
) ([]Dependency, error) {
var dependencies []Dependency
totalSteps := len(steps)
for i, step := range steps {
if progressCallback != nil {
progressCallback(i, totalSteps, fmt.Sprintf("Analyzing step %d/%d", i+1, totalSteps))
}
dep := a.processStep(step, i+1)
if dep != nil {
dependencies = append(dependencies, *dep) dependencies = append(dependencies, *dep)
} }
} }
if progressCallback != nil {
progressCallback(totalSteps, totalSteps, fmt.Sprintf("Found %d dependencies", len(dependencies)))
}
return dependencies, nil return dependencies, nil
} }
// processStep processes a single step and returns dependency if found.
func (a *Analyzer) processStep(step CompositeStep, stepNumber int) *Dependency {
if step.Uses != "" {
// This is an action dependency
dep, err := a.analyzeActionDependency(step, stepNumber)
if err != nil {
// 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
}
// parseCompositeAction is implemented in parser.go // parseCompositeAction is implemented in parser.go
// analyzeActionDependency analyzes a single action dependency. // analyzeActionDependency analyzes a single action dependency.

View File

@@ -21,6 +21,11 @@ func NewColoredOutput(quiet bool) *ColoredOutput {
} }
} }
// IsQuiet returns whether the output is in quiet mode.
func (co *ColoredOutput) IsQuiet() bool {
return co.Quiet
}
// Success prints a success message in green. // Success prints a success message in green.
func (co *ColoredOutput) Success(format string, args ...any) { func (co *ColoredOutput) Success(format string, args ...any) {
if co.Quiet { if co.Quiet {

65
main.go
View File

@@ -8,6 +8,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/schollz/progressbar/v3"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/ivuorinen/gh-action-readme/internal" "github.com/ivuorinen/gh-action-readme/internal"
@@ -510,10 +511,38 @@ func analyzeDependencies(output *internal.ColoredOutput, actionFiles []string, a
totalDeps := 0 totalDeps := 0
output.Bold("Dependencies found in action files:") output.Bold("Dependencies found in action files:")
for _, actionFile := range actionFiles { // Create progress bar for multiple files
output.Info("\n📄 %s", actionFile) var bar *progressbar.ProgressBar
totalDeps += analyzeActionFileDeps(output, actionFile, analyzer) if len(actionFiles) > 1 && !output.IsQuiet() {
bar = progressbar.NewOptions(len(actionFiles),
progressbar.OptionSetDescription("Analyzing dependencies"),
progressbar.OptionSetWidth(50),
progressbar.OptionShowCount(),
progressbar.OptionShowIts(),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "=",
SaucerHead: ">",
SaucerPadding: " ",
BarStart: "[",
BarEnd: "]",
}))
} }
for _, actionFile := range actionFiles {
if bar == nil {
output.Info("\n📄 %s", actionFile)
}
totalDeps += analyzeActionFileDeps(output, actionFile, analyzer)
if bar != nil {
_ = bar.Add(1)
}
}
if bar != nil {
fmt.Println()
}
return totalDeps return totalDeps
} }
@@ -586,9 +615,30 @@ func analyzeSecurityDeps(
} }
output.Bold("Security Analysis of GitHub Action Dependencies:") output.Bold("Security Analysis of GitHub Action Dependencies:")
// Create progress bar for multiple files
var bar *progressbar.ProgressBar
if len(actionFiles) > 1 && !output.IsQuiet() {
bar = progressbar.NewOptions(len(actionFiles),
progressbar.OptionSetDescription("Security analysis"),
progressbar.OptionSetWidth(50),
progressbar.OptionShowCount(),
progressbar.OptionShowIts(),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "=",
SaucerHead: ">",
SaucerPadding: " ",
BarStart: "[",
BarEnd: "]",
}))
}
for _, actionFile := range actionFiles { for _, actionFile := range actionFiles {
deps, err := analyzer.AnalyzeActionFile(actionFile) deps, err := analyzer.AnalyzeActionFile(actionFile)
if err != nil { if err != nil {
if bar != nil {
_ = bar.Add(1)
}
continue continue
} }
@@ -602,7 +652,16 @@ func analyzeSecurityDeps(
}{actionFile, dep}) }{actionFile, dep})
} }
} }
if bar != nil {
_ = bar.Add(1)
}
} }
if bar != nil {
fmt.Println()
}
return pinnedCount, floatingDeps return pinnedCount, floatingDeps
} }