mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-02-25 12:53:43 +00:00
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:
36
TODO.md
36
TODO.md
@@ -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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
65
main.go
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user