Skip to content

Commit f2c3abd

Browse files
authored
Implement multi-progress tracking and enhance security checks (#54)
* Implement multi-progress tracking with progress spinners - Added `ProgressSpinner` and `MultiProgress` types for managing concurrent progress bars. - Introduced task status constants: Pending, Running, Success, Failed, and Skipped. - Implemented methods for starting, completing, failing, and skipping tasks. - Added functionality to increment progress and track bytes written. - Created rendering logic for displaying progress bars in the terminal. - Implemented tests for all new functionality, including status transitions and concurrent access. - Added helper functions for formatting output and managing terminal display. * feat: enhance dependency checks and add vulnerability scanning tools * fix: adjust race detector usage in CI for macOS compatibility
1 parent b7d991d commit f2c3abd

File tree

18 files changed

+2232
-121
lines changed

18 files changed

+2232
-121
lines changed

.github/workflows/ci.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,15 @@ jobs:
4040
- name: Install golangci-lint
4141
run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
4242

43+
- name: Install staticcheck
44+
run: go install honnef.co/go/tools/cmd/staticcheck@latest
45+
4346
- name: Install gosec
4447
run: go install github.com/securego/gosec/v2/cmd/gosec@latest
4548

49+
- name: Install govulncheck
50+
run: go install golang.org/x/vuln/cmd/govulncheck@latest
51+
4652
- name: Run preflight checks
4753
run: mage preflight
4854

@@ -106,7 +112,12 @@ jobs:
106112
shell: bash
107113
run: |
108114
mkdir -p ../coverage
109-
go test -short -v -race -coverprofile=../coverage/coverage.out ./...
115+
# Race detector not supported on macOS without additional setup
116+
if [ "${{ matrix.os }}" = "macos-latest" ]; then
117+
go test -short -v -coverprofile=../coverage/coverage.out ./...
118+
else
119+
go test -short -v -race -coverprofile=../coverage/coverage.out ./...
120+
fi
110121
111122
- name: Upload coverage to Codecov
112123
if: github.repository == 'jongio/azd-app'

.github/workflows/release.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,15 @@ jobs:
4545
- name: Install golangci-lint
4646
run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
4747

48+
- name: Install staticcheck
49+
run: go install honnef.co/go/tools/cmd/staticcheck@latest
50+
4851
- name: Install gosec
4952
run: go install github.com/securego/gosec/v2/cmd/gosec@latest
5053

54+
- name: Install govulncheck
55+
run: go install golang.org/x/vuln/cmd/govulncheck@latest
56+
5157
- name: Run preflight checks
5258
run: mage preflight
5359

@@ -111,7 +117,12 @@ jobs:
111117
shell: bash
112118
run: |
113119
mkdir -p ../coverage
114-
go test -short -v -race -coverprofile=../coverage/coverage.out ./...
120+
# Race detector not supported on macOS without additional setup
121+
if [ "${{ matrix.os }}" = "macos-latest" ]; then
122+
go test -short -v -coverprofile=../coverage/coverage.out ./...
123+
else
124+
go test -short -v -race -coverprofile=../coverage/coverage.out ./...
125+
fi
115126
116127
lint:
117128
name: Lint

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,4 @@ cli2/jongio.azd.app/internal/cmd/prompt.go
112112
cli2/jongio.azd.app/internal/cmd/root.go
113113
cli2/jongio.azd.app/internal/cmd/version.go
114114
azd-app.sln
115+
cli/output-coverage
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Parallel Dependency Installation Output Specification
2+
3+
## Overview
4+
When installing dependencies for multiple projects concurrently, each project should display its own progress indicator and final status on separate, persistent lines.
5+
6+
## Requirements
7+
8+
### During Installation
9+
1. **Concurrent Display**: Each project displays on its own line with an animated progress indicator
10+
2. **Non-Overlapping Output**: Multiple projects install concurrently with separate, non-interfering progress displays
11+
3. **Real-time Activity**: Progress shows live activity (spinning/counting) while npm/pip/dotnet runs
12+
4. **Activity Feedback**: As the underlying process writes output, the progress bar updates to show work is happening
13+
14+
### After Completion
15+
1. **Persistent Status Lines**: Each project shows a final status line that remains visible (doesn't disappear)
16+
2. **Success Format**: `✓ <project-name> (<package-manager>)` displayed in green
17+
3. **Failure Format**: `✗ <project-name> (<package-manager>)` displayed in red
18+
4. **Complete Summary**: All final status lines remain visible so user can see the complete summary of what succeeded/failed
19+
5. **Overall Summary**: After all projects complete, show total count: `✓ Installed N project(s)` or error summary
20+
21+
## Visual Layout
22+
23+
### Expected Output Flow
24+
```
25+
Installing Dependencies
26+
27+
web (npm) (15/-, 8 it/s) [1s] ← animated progress bar
28+
api (pip) (12/-, 6 it/s) [1s] ← animated progress bar
29+
30+
[after completion - progress bars cleared and replaced with:]
31+
32+
✓ web (npm) ← persistent success line
33+
✓ api (pip) ← persistent success line
34+
35+
✓ Installed 2 project(s) ← overall summary
36+
```
37+
38+
### Error Case
39+
```
40+
Installing Dependencies
41+
42+
web (npm) (23/-, 9 it/s) [2s] ← animated progress bar
43+
api (pip) (8/-, 4 it/s) [1s] ← animated progress bar
44+
45+
[after completion:]
46+
47+
✓ web (npm) ← success
48+
✗ api (pip) ← failure
49+
50+
✗ Failed to install 1 project(s) ← error summary
51+
• api: failed to install requirements: ...
52+
```
53+
54+
## Implementation Details
55+
56+
### Progress Display
57+
- **Type**: Indeterminate spinner bar (unknown total)
58+
- **Updates**: Incremented on each write from npm/pip/dotnet process
59+
- **Cleanup**: On completion, clear the progress bar line before printing status
60+
61+
### Output Flow
62+
1. Create progress bar for each project with project name + package manager as description
63+
2. Route process stdout/stderr to update progress on each write
64+
3. When installation completes:
65+
- Stop the progress bar
66+
- Clear the progress bar line (overwrite with spaces + carriage return)
67+
- Print final status: `✓ <name>` or `✗ <name>` on a fresh line
68+
4. After all projects complete, print overall summary
69+
70+
### Concurrency Handling
71+
- Each project runs concurrently
72+
- All projects must complete before showing summary
73+
- Thread-safe handling of concurrent progress updates
74+
- Progress display library handles concurrent rendering to terminal
75+
76+
### Status Messages
77+
- **Success**: Green checkmark + project identifier
78+
- **Failure**: Red X + project identifier + error details in summary
79+
- **Summary**: Total count of successful/failed installations
80+
81+
## Edge Cases
82+
83+
### No Projects Found
84+
Show message: "No projects found"
85+
86+
### All Skipped (Already Installed)
87+
Still show per-project status but indicate they were skipped
88+
89+
### Mixed Success/Failure
90+
Show all individual statuses, then error summary listing only failures
91+
92+
### JSON Output Mode
93+
Suppress all progress bars and status lines, output only JSON result at end

cli/magefile.go

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,74 @@ func Vet() error {
284284
return nil
285285
}
286286

287+
// Staticcheck runs staticcheck for advanced static analysis.
288+
func Staticcheck() error {
289+
fmt.Println("Running staticcheck...")
290+
if err := sh.RunV("staticcheck", "./..."); err != nil {
291+
fmt.Println("⚠️ staticcheck found issues. Ensure staticcheck is installed:")
292+
fmt.Println(" go install honnef.co/go/tools/cmd/staticcheck@latest")
293+
return err
294+
}
295+
fmt.Println("✅ staticcheck passed!")
296+
return nil
297+
}
298+
299+
// ModTidy ensures go.mod and go.sum are tidy.
300+
func ModTidy() error {
301+
fmt.Println("Running go mod tidy...")
302+
if err := sh.RunV("go", "mod", "tidy"); err != nil {
303+
return fmt.Errorf("go mod tidy failed: %w", err)
304+
}
305+
306+
// Check if there are any changes
307+
if err := sh.RunV("git", "diff", "--exit-code", "go.mod", "go.sum"); err != nil {
308+
return fmt.Errorf("go.mod or go.sum has uncommitted changes after running go mod tidy - please review and commit these changes")
309+
}
310+
311+
fmt.Println("✅ go mod tidy passed!")
312+
return nil
313+
}
314+
315+
// ModVerify verifies dependencies have expected content.
316+
func ModVerify() error {
317+
fmt.Println("Running go mod verify...")
318+
if err := sh.RunV("go", "mod", "verify"); err != nil {
319+
return fmt.Errorf("go mod verify failed: %w", err)
320+
}
321+
fmt.Println("✅ go mod verify passed!")
322+
return nil
323+
}
324+
325+
// Vulncheck runs govulncheck to check for known vulnerabilities.
326+
func Vulncheck() error {
327+
fmt.Println("Running govulncheck...")
328+
if err := sh.RunV("govulncheck", "./..."); err != nil {
329+
fmt.Println("⚠️ govulncheck found vulnerabilities. Ensure govulncheck is installed:")
330+
fmt.Println(" go install golang.org/x/vuln/cmd/govulncheck@latest")
331+
return err
332+
}
333+
fmt.Println("✅ No known vulnerabilities found!")
334+
return nil
335+
}
336+
337+
// runVulncheck runs govulncheck if available, otherwise skips.
338+
func runVulncheck() error {
339+
fmt.Println("Checking for known vulnerabilities...")
340+
// Check if govulncheck is installed
341+
if _, err := exec.LookPath("govulncheck"); err != nil {
342+
fmt.Println("⚠️ govulncheck not installed - skipping vulnerability check")
343+
fmt.Println(" Install with: go install golang.org/x/vuln/cmd/govulncheck@latest")
344+
return nil // Don't fail preflight if not installed
345+
}
346+
347+
if err := sh.RunV("govulncheck", "./..."); err != nil {
348+
fmt.Println("⚠️ Known vulnerabilities found!")
349+
return err
350+
}
351+
fmt.Println("✅ No known vulnerabilities found!")
352+
return nil
353+
}
354+
287355
// Clean removes build artifacts and coverage reports.
288356
func Clean() error {
289357
fmt.Println("Cleaning build artifacts...")
@@ -369,12 +437,16 @@ func Preflight() error {
369437
fn func() error
370438
}{
371439
{"Formatting code", Fmt},
440+
{"Verifying go.mod consistency", ModVerify},
441+
{"Tidying go.mod and go.sum", ModTidy},
372442
{"Building and linting dashboard", DashboardBuild},
373443
{"Running dashboard tests", DashboardTest},
374444
{"Building Go binary", Build},
375445
{"Running go vet", Vet},
446+
{"Running staticcheck", Staticcheck},
376447
{"Running standard linting", Lint},
377448
{"Running quick security scan", runQuickSecurity},
449+
{"Checking for known vulnerabilities", runVulncheck},
378450
{"Running all tests with coverage", TestCoverage},
379451
}
380452

@@ -387,7 +459,8 @@ func Preflight() error {
387459
}
388460

389461
fmt.Println("✅ All preflight checks passed!")
390-
fmt.Println("💡 Tip: Run 'mage security' for a full security scan (~4 minutes)")
462+
fmt.Println("💡 Tips:")
463+
fmt.Println(" • Run 'mage security' for a full security scan (~4 minutes)")
391464
fmt.Println("🎉 Ready to ship!")
392465
return nil
393466
}

cli/src/cmd/app/commands/core.go

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,8 @@ func (di *DependencyInstaller) installProject(projectType, dir, manager string,
360360
func executeDeps() error {
361361
if !output.IsJSON() {
362362
output.Newline()
363-
output.Section("🔍", "Installing dependencies")
363+
output.Section("📦", "Installing Dependencies")
364+
output.Newline()
364365
}
365366

366367
// Determine search root
@@ -372,37 +373,89 @@ func executeDeps() error {
372373
return err
373374
}
374375

375-
// Install dependencies
376-
installer := NewDependencyInstaller(searchRoot)
377-
results, err := installer.InstallAll()
376+
// Detect all projects
377+
nodeProjects, err := detector.FindNodeProjects(searchRoot)
378378
if err != nil {
379-
return err
379+
if output.IsJSON() {
380+
return output.PrintJSON(DepsResult{Error: fmt.Sprintf("failed to detect Node.js projects: %v", err)})
381+
}
382+
return fmt.Errorf("failed to detect Node.js projects: %w", err)
380383
}
384+
pythonProjects, err := detector.FindPythonProjects(searchRoot)
385+
if err != nil {
386+
if output.IsJSON() {
387+
return output.PrintJSON(DepsResult{Error: fmt.Sprintf("failed to detect Python projects: %v", err)})
388+
}
389+
return fmt.Errorf("failed to detect Python projects: %w", err)
390+
}
391+
dotnetProjects, err := detector.FindDotnetProjects(searchRoot)
392+
if err != nil {
393+
if output.IsJSON() {
394+
return output.PrintJSON(DepsResult{Error: fmt.Sprintf("failed to detect .NET projects: %v", err)})
395+
}
396+
return fmt.Errorf("failed to detect .NET projects: %w", err)
397+
}
398+
399+
totalProjects := len(nodeProjects) + len(pythonProjects) + len(dotnetProjects)
381400

382401
// Handle no projects case
383-
if len(results) == 0 {
402+
if totalProjects == 0 {
384403
if output.IsJSON() {
385404
return output.PrintJSON(DepsResult{
386405
Success: true,
387406
Projects: []InstallResult{},
388407
Message: msgNoProjectsDetected,
389408
})
390409
}
391-
output.Info(msgNoProjectsDetected + " - skipping dependency installation")
410+
output.Info(msgNoProjectsDetected)
392411
return nil
393412
}
394413

395-
// Output results
396-
if output.IsJSON() {
397-
allSuccess := checkAllSuccess(results)
398-
return output.PrintJSON(DepsResult{
399-
Success: allSuccess,
400-
Projects: results,
401-
})
414+
// Use parallel installer for concurrent installation with progress bars
415+
if !output.IsJSON() {
416+
parallelInstaller := installer.NewParallelInstaller()
417+
parallelInstaller.Verbose = depsVerbose
418+
419+
// Add all projects to the parallel installer
420+
for _, project := range nodeProjects {
421+
parallelInstaller.AddNodeProject(project)
422+
}
423+
for _, project := range pythonProjects {
424+
parallelInstaller.AddPythonProject(project)
425+
}
426+
for _, project := range dotnetProjects {
427+
parallelInstaller.AddDotnetProject(project)
428+
}
429+
430+
// Run all installations in parallel
431+
if err := parallelInstaller.Run(); err != nil {
432+
return err
433+
}
434+
435+
// Check for failures
436+
if parallelInstaller.HasFailures() {
437+
failedProjects := parallelInstaller.FailedProjects()
438+
if len(failedProjects) > 0 {
439+
return fmt.Errorf("failed to install %d of %d projects: %v", len(failedProjects), parallelInstaller.TotalProjects(), failedProjects)
440+
}
441+
return fmt.Errorf("some installations failed")
442+
}
443+
444+
return nil
402445
}
403446

404-
output.Success("Dependencies installed successfully!")
405-
return nil
447+
// JSON mode: use sequential installer
448+
depInstaller := NewDependencyInstaller(searchRoot)
449+
results, err := depInstaller.InstallAll()
450+
if err != nil {
451+
return err
452+
}
453+
454+
allSuccess := checkAllSuccess(results)
455+
return output.PrintJSON(DepsResult{
456+
Success: allSuccess,
457+
Projects: results,
458+
})
406459
}
407460

408461
// getSearchRoot determines the search root for finding projects.

0 commit comments

Comments
 (0)