From a7eb5c5e48b25315292cc5370c06d2ba541d5c1c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Oct 2025 01:29:06 +0000 Subject: [PATCH 1/5] Initial plan From d21b18e47fa4a2b5e17ef80a2b657485e51e24ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Oct 2025 01:47:06 +0000 Subject: [PATCH 2/5] Add Python (pip) and Golang dependency manifest generation support Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/dependabot.go | 434 ++++++++++++++++++++++++++++--- pkg/workflow/dependabot_test.go | 444 +++++++++++++++++++++++++++++++- 2 files changed, 835 insertions(+), 43 deletions(-) diff --git a/pkg/workflow/dependabot.go b/pkg/workflow/dependabot.go index de11ec34f22..1638be6f3e6 100644 --- a/pkg/workflow/dependabot.go +++ b/pkg/workflow/dependabot.go @@ -46,48 +46,106 @@ type NpmDependency struct { Version string // semver range or specific version } -// GenerateDependabotManifests generates package.json and dependabot.yml if npm dependencies are found +// PipDependency represents a parsed pip package with version +type PipDependency struct { + Name string + Version string // version specifier (e.g., ==1.0.0, >=2.0.0) +} + +// GoDependency represents a parsed Go package +type GoDependency struct { + Path string // import path (e.g., github.com/user/repo) + Version string // version or pseudo-version +} + +// GenerateDependabotManifests generates manifest files and dependabot.yml for detected dependencies func (c *Compiler) GenerateDependabotManifests(workflowDataList []*WorkflowData, workflowDir string, forceOverwrite bool) error { dependabotLog.Print("Starting Dependabot manifest generation") - // Collect all npm dependencies from all workflows - allDeps := c.collectNpmDependencies(workflowDataList) - if len(allDeps) == 0 { - dependabotLog.Print("No npm dependencies found, skipping manifest generation") + // Track which ecosystems have dependencies + ecosystems := make(map[string]bool) + + // Collect npm dependencies + npmDeps := c.collectNpmDependencies(workflowDataList) + if len(npmDeps) > 0 { + ecosystems["npm"] = true + dependabotLog.Printf("Found %d unique npm dependencies", len(npmDeps)) if c.verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No npm dependencies detected in workflows, skipping Dependabot manifest generation")) + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Found %d npm dependencies in workflows", len(npmDeps)))) + } + + // Generate package.json + packageJSONPath := filepath.Join(workflowDir, "package.json") + if err := c.generatePackageJSON(packageJSONPath, npmDeps, forceOverwrite); err != nil { + if c.strictMode { + return fmt.Errorf("failed to generate package.json: %w", err) + } + c.IncrementWarningCount() + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to generate package.json: %v", err))) + } else { + // Generate package-lock.json + if err := c.generatePackageLock(workflowDir); err != nil { + if c.strictMode { + return fmt.Errorf("failed to generate package-lock.json: %w", err) + } + c.IncrementWarningCount() + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to generate package-lock.json: %v", err))) + } } - return nil } - dependabotLog.Printf("Found %d unique npm dependencies", len(allDeps)) - if c.verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Found %d npm dependencies in workflows", len(allDeps)))) + // Collect pip dependencies + pipDeps := c.collectPipDependencies(workflowDataList) + if len(pipDeps) > 0 { + ecosystems["pip"] = true + dependabotLog.Printf("Found %d unique pip dependencies", len(pipDeps)) + if c.verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Found %d pip dependencies in workflows", len(pipDeps)))) + } + + // Generate requirements.txt + requirementsPath := filepath.Join(workflowDir, "requirements.txt") + if err := c.generateRequirementsTxt(requirementsPath, pipDeps, forceOverwrite); err != nil { + if c.strictMode { + return fmt.Errorf("failed to generate requirements.txt: %w", err) + } + c.IncrementWarningCount() + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to generate requirements.txt: %v", err))) + } } - // Generate package.json - packageJSONPath := filepath.Join(workflowDir, "package.json") - if err := c.generatePackageJSON(packageJSONPath, allDeps, forceOverwrite); err != nil { - if c.strictMode { - return fmt.Errorf("failed to generate package.json: %w", err) + // Collect go dependencies + goDeps := c.collectGoDependencies(workflowDataList) + if len(goDeps) > 0 { + ecosystems["gomod"] = true + dependabotLog.Printf("Found %d unique go dependencies", len(goDeps)) + if c.verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Found %d go dependencies in workflows", len(goDeps)))) + } + + // Generate go.mod + goModPath := filepath.Join(workflowDir, "go.mod") + if err := c.generateGoMod(goModPath, goDeps, forceOverwrite); err != nil { + if c.strictMode { + return fmt.Errorf("failed to generate go.mod: %w", err) + } + c.IncrementWarningCount() + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to generate go.mod: %v", err))) } - c.IncrementWarningCount() - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to generate package.json: %v", err))) - return nil } - // Generate package-lock.json - if err := c.generatePackageLock(workflowDir); err != nil { - if c.strictMode { - return fmt.Errorf("failed to generate package-lock.json: %w", err) + // If no dependencies found at all, skip + if len(ecosystems) == 0 { + dependabotLog.Print("No dependencies found, skipping manifest generation") + if c.verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No dependencies detected in workflows, skipping Dependabot manifest generation")) } - c.IncrementWarningCount() - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to generate package-lock.json: %v", err))) + return nil } - // Generate dependabot.yml + // Generate dependabot.yml with all detected ecosystems dependabotPath := filepath.Join(filepath.Dir(workflowDir), "dependabot.yml") - if err := c.generateDependabotConfig(dependabotPath, forceOverwrite); err != nil { + if err := c.generateDependabotConfig(dependabotPath, ecosystems, forceOverwrite); err != nil { if c.strictMode { return fmt.Errorf("failed to generate dependabot.yml: %w", err) } @@ -283,7 +341,7 @@ func (c *Compiler) generatePackageLock(workflowDir string) error { } // generateDependabotConfig creates or updates .github/dependabot.yml -func (c *Compiler) generateDependabotConfig(path string, forceOverwrite bool) error { +func (c *Compiler) generateDependabotConfig(path string, ecosystems map[string]bool, forceOverwrite bool) error { dependabotLog.Printf("Generating dependabot.yml at %s", path) var config DependabotConfig @@ -317,23 +375,24 @@ func (c *Compiler) generateDependabotConfig(path string, forceOverwrite bool) er config = DependabotConfig{Version: 2} } - // Check if npm ecosystem already exists for .github/workflows - npmExists := false - for _, update := range config.Updates { - if update.PackageEcosystem == "npm" && update.Directory == "/.github/workflows" { - npmExists = true - break + // Add ecosystems that don't already exist for .github/workflows + for ecosystem := range ecosystems { + exists := false + for _, update := range config.Updates { + if update.PackageEcosystem == ecosystem && update.Directory == "/.github/workflows" { + exists = true + break + } } - } - // Add npm ecosystem if it doesn't exist - if !npmExists { - npmUpdate := DependabotUpdateEntry{ - PackageEcosystem: "npm", - Directory: "/.github/workflows", + if !exists { + entry := DependabotUpdateEntry{ + PackageEcosystem: ecosystem, + Directory: "/.github/workflows", + } + entry.Schedule.Interval = "weekly" + config.Updates = append(config.Updates, entry) } - npmUpdate.Schedule.Interval = "weekly" - config.Updates = append(config.Updates, npmUpdate) } // Write dependabot.yml @@ -358,3 +417,296 @@ func (c *Compiler) generateDependabotConfig(path string, forceOverwrite bool) er return nil } + +// collectPipDependencies collects all pip dependencies from workflow data +func (c *Compiler) collectPipDependencies(workflowDataList []*WorkflowData) []PipDependency { +dependabotLog.Print("Collecting pip dependencies from workflows") + +depMap := make(map[string]string) // package name -> version (last seen) + +for _, workflowData := range workflowDataList { +packages := extractPipPackages(workflowData) +for _, pkg := range packages { +dep := parsePipPackage(pkg) +depMap[dep.Name] = dep.Version +} +} + +// Convert map to sorted slice +var deps []PipDependency +for name, version := range depMap { +deps = append(deps, PipDependency{ +Name: name, +Version: version, +}) +} + +// Sort by name for deterministic output +sort.Slice(deps, func(i, j int) bool { +return deps[i].Name < deps[j].Name +}) + +dependabotLog.Printf("Collected %d unique pip dependencies", len(deps)) +return deps +} + +// parsePipPackage parses a pip package string like "requests==2.28.0" into name and version +func parsePipPackage(pkg string) PipDependency { +// Handle version specifiers (==, >=, <=, >, <, !=, ~=) +for _, sep := range []string{"==", ">=", "<=", "!=", "~=", ">", "<"} { +if idx := strings.Index(pkg, sep); idx > 0 { +return PipDependency{ +Name: pkg[:idx], +Version: pkg[idx:], // Include the separator +} +} +} + +// No version specified +return PipDependency{ +Name: pkg, +Version: "", +} +} + +// generateRequirementsTxt creates or updates requirements.txt with dependencies +func (c *Compiler) generateRequirementsTxt(path string, deps []PipDependency, forceOverwrite bool) error { +dependabotLog.Printf("Generating requirements.txt at %s", path) + +// Build requirements map for merging +reqMap := make(map[string]string) +for _, dep := range deps { +if dep.Version != "" { +reqMap[dep.Name] = dep.Version +} else { +reqMap[dep.Name] = "" +} +} + +// Check if requirements.txt already exists +if _, err := os.Stat(path); err == nil { +// File exists - merge dependencies +dependabotLog.Print("Existing requirements.txt found, merging dependencies") + +existingData, err := os.ReadFile(path) +if err != nil { +return fmt.Errorf("failed to read existing requirements.txt: %w", err) +} + +// Parse existing requirements +lines := strings.Split(string(existingData), "\n") +for _, line := range lines { +line = strings.TrimSpace(line) +if line == "" || strings.HasPrefix(line, "#") { +continue +} +dep := parsePipPackage(line) +// Only add if not already in our new deps +if _, exists := reqMap[dep.Name]; !exists { +if dep.Version != "" { +reqMap[dep.Name] = dep.Version +} else { +reqMap[dep.Name] = "" +} +} +} + +if c.verbose { +fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Merging with existing requirements.txt")) +} +} else { +dependabotLog.Print("Creating new requirements.txt") +} + +// Sort dependencies by name +var sortedNames []string +for name := range reqMap { +sortedNames = append(sortedNames, name) +} +sort.Strings(sortedNames) + +// Build requirements.txt content +var lines []string +for _, name := range sortedNames { +version := reqMap[name] +if version != "" { +lines = append(lines, name+version) +} else { +lines = append(lines, name) +} +} + +content := strings.Join(lines, "\n") + "\n" + +if err := os.WriteFile(path, []byte(content), 0644); err != nil { +return fmt.Errorf("failed to write requirements.txt: %w", err) +} + +dependabotLog.Printf("Successfully wrote requirements.txt with %d dependencies", len(reqMap)) +if c.verbose { +fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Generated requirements.txt with %d dependencies", len(reqMap)))) +} + +// Track the created file +if c.fileTracker != nil { +c.fileTracker.TrackCreated(path) +} + +return nil +} + +// collectGoDependencies collects all Go dependencies from workflow data +func (c *Compiler) collectGoDependencies(workflowDataList []*WorkflowData) []GoDependency { +dependabotLog.Print("Collecting Go dependencies from workflows") + +depMap := make(map[string]string) // package path -> version (last seen) + +for _, workflowData := range workflowDataList { +packages := extractGoPackages(workflowData) +for _, pkg := range packages { +dep := parseGoPackage(pkg) +depMap[dep.Path] = dep.Version +} +} + +// Convert map to sorted slice +var deps []GoDependency +for path, version := range depMap { +deps = append(deps, GoDependency{ +Path: path, +Version: version, +}) +} + +// Sort by path for deterministic output +sort.Slice(deps, func(i, j int) bool { +return deps[i].Path < deps[j].Path +}) + +dependabotLog.Printf("Collected %d unique Go dependencies", len(deps)) +return deps +} + +// parseGoPackage parses a Go package string like "github.com/user/repo@v1.2.3" into path and version +func parseGoPackage(pkg string) GoDependency { +// Handle version separator @ +if idx := strings.Index(pkg, "@"); idx > 0 { +return GoDependency{ +Path: pkg[:idx], +Version: pkg[idx+1:], +} +} + +// No version specified - will use latest +return GoDependency{ +Path: pkg, +Version: "latest", +} +} + +// extractGoPackages extracts Go package paths from workflow data +func extractGoPackages(workflowData *WorkflowData) []string { +return collectPackagesFromWorkflow(workflowData, extractGoFromCommands, "") +} + +// extractGoFromCommands extracts Go package paths from command strings +func extractGoFromCommands(commands string) []string { +var packages []string +lines := strings.Split(commands, "\n") + +for _, line := range lines { +// Look for "go install " or "go get " patterns +words := strings.Fields(line) +for i, word := range words { +if word == "go" && i+1 < len(words) { +cmd := words[i+1] +if cmd == "install" || cmd == "get" { +// Find the package path +for j := i + 2; j < len(words); j++ { +pkg := words[j] +pkg = strings.TrimRight(pkg, "&|;") +// Skip flags (start with - or --) +if !strings.HasPrefix(pkg, "-") { +packages = append(packages, pkg) +break +} +} +} +} +} +} + +return packages +} + +// generateGoMod creates or updates go.mod with dependencies +func (c *Compiler) generateGoMod(path string, deps []GoDependency, forceOverwrite bool) error { +dependabotLog.Printf("Generating go.mod at %s", path) + +// Build module content +var lines []string + +// Check if go.mod already exists +if _, err := os.Stat(path); err == nil { +// File exists - read and preserve module declaration +dependabotLog.Print("Existing go.mod found, merging dependencies") + +existingData, err := os.ReadFile(path) +if err != nil { +return fmt.Errorf("failed to read existing go.mod: %w", err) +} + +existingLines := strings.Split(string(existingData), "\n") +// Keep module declaration and go version +for _, line := range existingLines { +trimmed := strings.TrimSpace(line) +if strings.HasPrefix(trimmed, "module ") || strings.HasPrefix(trimmed, "go ") { +lines = append(lines, line) +} +} + +if c.verbose { +fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Merging with existing go.mod")) +} +} else { +// New go.mod +dependabotLog.Print("Creating new go.mod") +lines = append(lines, "module github.com/githubnext/gh-aw-workflows-deps") +lines = append(lines, "") +lines = append(lines, "go 1.21") +} + +// Add require section if we have dependencies +if len(deps) > 0 { +lines = append(lines, "") +lines = append(lines, "require (") +for _, dep := range deps { +version := dep.Version +if version == "latest" || version == "" { +// For latest, we need to use a placeholder or skip +// Dependabot will update to actual versions +version = "v0.0.0" +} +lines = append(lines, fmt.Sprintf("\t%s %s", dep.Path, version)) +} +lines = append(lines, ")") +} + +content := strings.Join(lines, "\n") + "\n" + +if err := os.WriteFile(path, []byte(content), 0644); err != nil { +return fmt.Errorf("failed to write go.mod: %w", err) +} + +dependabotLog.Printf("Successfully wrote go.mod with %d dependencies", len(deps)) +if c.verbose { +fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Generated go.mod with %d dependencies", len(deps)))) +} + +// Track the created file +if c.fileTracker != nil { +c.fileTracker.TrackCreated(path) +} + +return nil +} diff --git a/pkg/workflow/dependabot_test.go b/pkg/workflow/dependabot_test.go index d265cfbe2c7..e62e90b81f7 100644 --- a/pkg/workflow/dependabot_test.go +++ b/pkg/workflow/dependabot_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "os" "path/filepath" + "strings" "testing" "github.com/goccy/go-yaml" @@ -259,8 +260,10 @@ func TestGenerateDependabotConfig(t *testing.T) { tempDir := t.TempDir() dependabotPath := filepath.Join(tempDir, "dependabot.yml") + ecosystems := map[string]bool{"npm": true} + // Test creating new dependabot.yml - err := compiler.generateDependabotConfig(dependabotPath, false) + err := compiler.generateDependabotConfig(dependabotPath, ecosystems, false) if err != nil { t.Fatalf("failed to generate dependabot.yml: %v", err) } @@ -320,8 +323,10 @@ func TestGenerateDependabotConfig_PreserveExisting(t *testing.T) { existingData, _ := yaml.Marshal(&existingConfig) os.WriteFile(dependabotPath, existingData, 0644) + ecosystems := map[string]bool{"npm": true} + // Try to generate without force - should preserve - err := compiler.generateDependabotConfig(dependabotPath, false) + err := compiler.generateDependabotConfig(dependabotPath, ecosystems, false) if err != nil { t.Fatalf("failed to check existing dependabot.yml: %v", err) } @@ -421,3 +426,438 @@ func TestGenerateDependabotManifests_StrictMode(t *testing.T) { } } } + +// Tests for Python (pip) support + +func TestParsePipPackage(t *testing.T) { +tests := []struct { +name string +pkg string +expectedName string +expectedVersion string +}{ +{ +name: "package with == version", +pkg: "requests==2.28.0", +expectedName: "requests", +expectedVersion: "==2.28.0", +}, +{ +name: "package with >= version", +pkg: "django>=3.2.0", +expectedName: "django", +expectedVersion: ">=3.2.0", +}, +{ +name: "package with ~= version", +pkg: "flask~=2.0.0", +expectedName: "flask", +expectedVersion: "~=2.0.0", +}, +{ +name: "package without version", +pkg: "numpy", +expectedName: "numpy", +expectedVersion: "", +}, +{ +name: "package with != version", +pkg: "pytest!=7.0.0", +expectedName: "pytest", +expectedVersion: "!=7.0.0", +}, +} + +for _, tt := range tests { +t.Run(tt.name, func(t *testing.T) { +dep := parsePipPackage(tt.pkg) +if dep.Name != tt.expectedName { +t.Errorf("expected name %q, got %q", tt.expectedName, dep.Name) +} +if dep.Version != tt.expectedVersion { +t.Errorf("expected version %q, got %q", tt.expectedVersion, dep.Version) +} +}) +} +} + +func TestCollectPipDependencies(t *testing.T) { +compiler := NewCompiler(false, "", "test") + +tests := []struct { +name string +workflows []*WorkflowData +expectedDeps []PipDependency +}{ +{ +name: "single workflow with pip dependencies", +workflows: []*WorkflowData{ +{ +CustomSteps: "pip install requests==2.28.0", +}, +}, +expectedDeps: []PipDependency{ +{Name: "requests", Version: "==2.28.0"}, +}, +}, +{ +name: "multiple workflows with different dependencies", +workflows: []*WorkflowData{ +{ +CustomSteps: "pip install requests==2.28.0", +}, +{ +CustomSteps: "pip3 install django>=3.2.0", +}, +}, +expectedDeps: []PipDependency{ +{Name: "django", Version: ">=3.2.0"}, +{Name: "requests", Version: "==2.28.0"}, +}, +}, +{ +name: "duplicate dependencies use last version", +workflows: []*WorkflowData{ +{ +CustomSteps: "pip install requests==2.27.0", +}, +{ +CustomSteps: "pip install requests==2.28.0", +}, +}, +expectedDeps: []PipDependency{ +{Name: "requests", Version: "==2.28.0"}, +}, +}, +{ +name: "no pip dependencies", +workflows: []*WorkflowData{}, +expectedDeps: []PipDependency{}, +}, +} + +for _, tt := range tests { +t.Run(tt.name, func(t *testing.T) { +deps := compiler.collectPipDependencies(tt.workflows) +if len(deps) != len(tt.expectedDeps) { +t.Errorf("expected %d dependencies, got %d", len(tt.expectedDeps), len(deps)) +} +for i, dep := range deps { +if i >= len(tt.expectedDeps) { +break +} +expected := tt.expectedDeps[i] +if dep.Name != expected.Name { +t.Errorf("dependency %d: expected name %q, got %q", i, expected.Name, dep.Name) +} +if dep.Version != expected.Version { +t.Errorf("dependency %d: expected version %q, got %q", i, expected.Version, dep.Version) +} +} +}) +} +} + +func TestGenerateRequirementsTxt(t *testing.T) { +compiler := NewCompiler(false, "", "test") +tempDir := t.TempDir() +requirementsPath := filepath.Join(tempDir, "requirements.txt") + +deps := []PipDependency{ +{Name: "requests", Version: "==2.28.0"}, +{Name: "django", Version: ">=3.2.0"}, +} + +// Test creating new requirements.txt +err := compiler.generateRequirementsTxt(requirementsPath, deps, false) +if err != nil { +t.Fatalf("failed to generate requirements.txt: %v", err) +} + +// Verify file was created +if _, err := os.Stat(requirementsPath); os.IsNotExist(err) { +t.Fatal("requirements.txt was not created") +} + +// Read and verify content +data, err := os.ReadFile(requirementsPath) +if err != nil { +t.Fatalf("failed to read requirements.txt: %v", err) +} + +content := string(data) +if !strings.Contains(content, "django>=3.2.0") { +t.Error("requirements.txt should contain django>=3.2.0") +} +if !strings.Contains(content, "requests==2.28.0") { +t.Error("requirements.txt should contain requests==2.28.0") +} +} + +// Tests for Golang support + +func TestParseGoPackage(t *testing.T) { +tests := []struct { +name string +pkg string +expectedPath string +expectedVersion string +}{ +{ +name: "package with version", +pkg: "github.com/user/repo@v1.2.3", +expectedPath: "github.com/user/repo", +expectedVersion: "v1.2.3", +}, +{ +name: "package without version", +pkg: "github.com/user/repo", +expectedPath: "github.com/user/repo", +expectedVersion: "latest", +}, +{ +name: "package with pseudo-version", +pkg: "golang.org/x/tools@v0.1.12-0.20220713141851-7464d2807d88", +expectedPath: "golang.org/x/tools", +expectedVersion: "v0.1.12-0.20220713141851-7464d2807d88", +}, +} + +for _, tt := range tests { +t.Run(tt.name, func(t *testing.T) { +dep := parseGoPackage(tt.pkg) +if dep.Path != tt.expectedPath { +t.Errorf("expected path %q, got %q", tt.expectedPath, dep.Path) +} +if dep.Version != tt.expectedVersion { +t.Errorf("expected version %q, got %q", tt.expectedVersion, dep.Version) +} +}) +} +} + +func TestCollectGoDependencies(t *testing.T) { +compiler := NewCompiler(false, "", "test") + +tests := []struct { +name string +workflows []*WorkflowData +expectedDeps []GoDependency +}{ +{ +name: "single workflow with go install", +workflows: []*WorkflowData{ +{ +CustomSteps: "go install github.com/user/tool@v1.0.0", +}, +}, +expectedDeps: []GoDependency{ +{Path: "github.com/user/tool", Version: "v1.0.0"}, +}, +}, +{ +name: "multiple workflows with different dependencies", +workflows: []*WorkflowData{ +{ +CustomSteps: "go install github.com/user/tool@v1.0.0", +}, +{ +CustomSteps: "go get golang.org/x/tools@latest", +}, +}, +expectedDeps: []GoDependency{ +{Path: "github.com/user/tool", Version: "v1.0.0"}, +{Path: "golang.org/x/tools", Version: "latest"}, +}, +}, +{ +name: "duplicate dependencies use last version", +workflows: []*WorkflowData{ +{ +CustomSteps: "go install github.com/user/tool@v1.0.0", +}, +{ +CustomSteps: "go install github.com/user/tool@v2.0.0", +}, +}, +expectedDeps: []GoDependency{ +{Path: "github.com/user/tool", Version: "v2.0.0"}, +}, +}, +{ +name: "no go dependencies", +workflows: []*WorkflowData{}, +expectedDeps: []GoDependency{}, +}, +} + +for _, tt := range tests { +t.Run(tt.name, func(t *testing.T) { +deps := compiler.collectGoDependencies(tt.workflows) +if len(deps) != len(tt.expectedDeps) { +t.Errorf("expected %d dependencies, got %d", len(tt.expectedDeps), len(deps)) +} +for i, dep := range deps { +if i >= len(tt.expectedDeps) { +break +} +expected := tt.expectedDeps[i] +if dep.Path != expected.Path { +t.Errorf("dependency %d: expected path %q, got %q", i, expected.Path, dep.Path) +} +if dep.Version != expected.Version { +t.Errorf("dependency %d: expected version %q, got %q", i, expected.Version, dep.Version) +} +} +}) +} +} + +func TestGenerateGoMod(t *testing.T) { +compiler := NewCompiler(false, "", "test") +tempDir := t.TempDir() +goModPath := filepath.Join(tempDir, "go.mod") + +deps := []GoDependency{ +{Path: "github.com/user/tool", Version: "v1.0.0"}, +{Path: "golang.org/x/tools", Version: "v0.1.0"}, +} + +// Test creating new go.mod +err := compiler.generateGoMod(goModPath, deps, false) +if err != nil { +t.Fatalf("failed to generate go.mod: %v", err) +} + +// Verify file was created +if _, err := os.Stat(goModPath); os.IsNotExist(err) { +t.Fatal("go.mod was not created") +} + +// Read and verify content +data, err := os.ReadFile(goModPath) +if err != nil { +t.Fatalf("failed to read go.mod: %v", err) +} + +content := string(data) +if !strings.Contains(content, "module github.com/githubnext/gh-aw-workflows-deps") { +t.Error("go.mod should contain module declaration") +} +if !strings.Contains(content, "require (") { +t.Error("go.mod should contain require section") +} +if !strings.Contains(content, "github.com/user/tool v1.0.0") { +t.Error("go.mod should contain github.com/user/tool v1.0.0") +} +if !strings.Contains(content, "golang.org/x/tools v0.1.0") { +t.Error("go.mod should contain golang.org/x/tools v0.1.0") +} +} + +// Tests for multi-ecosystem support + +func TestGenerateDependabotConfig_MultipleEcosystems(t *testing.T) { +compiler := NewCompiler(false, "", "test") +tempDir := t.TempDir() +dependabotPath := filepath.Join(tempDir, "dependabot.yml") + +ecosystems := map[string]bool{ +"npm": true, +"pip": true, +"gomod": true, +} + +// Test creating new dependabot.yml with multiple ecosystems +err := compiler.generateDependabotConfig(dependabotPath, ecosystems, false) +if err != nil { +t.Fatalf("failed to generate dependabot.yml: %v", err) +} + +// Verify file was created +if _, err := os.Stat(dependabotPath); os.IsNotExist(err) { +t.Fatal("dependabot.yml was not created") +} + +// Read and verify content +data, err := os.ReadFile(dependabotPath) +if err != nil { +t.Fatalf("failed to read dependabot.yml: %v", err) +} + +var config DependabotConfig +if err := yaml.Unmarshal(data, &config); err != nil { +t.Fatalf("failed to parse dependabot.yml: %v", err) +} + +// Verify structure +if config.Version != 2 { +t.Errorf("expected version 2, got %d", config.Version) +} +if len(config.Updates) != 3 { +t.Fatalf("expected 3 update entries, got %d", len(config.Updates)) +} + +// Check that all ecosystems are present +ecosystemsFound := make(map[string]bool) +for _, update := range config.Updates { +ecosystemsFound[update.PackageEcosystem] = true +if update.Directory != "/.github/workflows" { +t.Errorf("expected directory '/.github/workflows', got %q", update.Directory) +} +if update.Schedule.Interval != "weekly" { +t.Errorf("expected interval 'weekly', got %q", update.Schedule.Interval) +} +} + +for ecosystem := range ecosystems { +if !ecosystemsFound[ecosystem] { +t.Errorf("ecosystem %q not found in dependabot.yml", ecosystem) +} +} +} + +func TestGenerateDependabotManifests_AllEcosystems(t *testing.T) { +compiler := NewCompiler(true, "", "test") +tempDir := t.TempDir() +workflowDir := filepath.Join(tempDir, ".github", "workflows") +os.MkdirAll(workflowDir, 0755) + +// Workflow with npm, pip, and go dependencies +workflows := []*WorkflowData{ +{ +CustomSteps: ` +npx @playwright/mcp@latest +pip install requests==2.28.0 +go install github.com/user/tool@v1.0.0 +`, +}, +} + +// This will skip npm install (no npm in test env), but should generate manifest files +_ = compiler.GenerateDependabotManifests(workflows, workflowDir, false) + +// Check that package.json was created +packageJSONPath := filepath.Join(workflowDir, "package.json") +if _, err := os.Stat(packageJSONPath); os.IsNotExist(err) { +t.Error("package.json should be created") +} + +// Check that requirements.txt was created +requirementsPath := filepath.Join(workflowDir, "requirements.txt") +if _, err := os.Stat(requirementsPath); os.IsNotExist(err) { +t.Error("requirements.txt should be created") +} + +// Check that go.mod was created +goModPath := filepath.Join(workflowDir, "go.mod") +if _, err := os.Stat(goModPath); os.IsNotExist(err) { +t.Error("go.mod should be created") +} + +// Check dependabot.yml +dependabotPath := filepath.Join(tempDir, ".github", "dependabot.yml") +if _, err := os.Stat(dependabotPath); os.IsNotExist(err) { +t.Error("dependabot.yml should be created") +} +} From 578e95e1db691be6482d4365642fed6577f6d986 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Oct 2025 01:59:08 +0000 Subject: [PATCH 3/5] Update CLI help text for multi-ecosystem dependency manifest support Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- cmd/gh-aw/main.go | 14 +- pkg/workflow/dependabot.go | 454 +++++++++---------- pkg/workflow/dependabot_test.go | 750 ++++++++++++++++---------------- 3 files changed, 609 insertions(+), 609 deletions(-) diff --git a/cmd/gh-aw/main.go b/cmd/gh-aw/main.go index eb466677cc6..55d1b0dda1a 100644 --- a/cmd/gh-aw/main.go +++ b/cmd/gh-aw/main.go @@ -124,11 +124,11 @@ var compileCmd = &cobra.Command{ If no files are specified, all markdown files in .github/workflows will be compiled. -The --dependabot flag generates npm package manifests when npm dependencies are detected: - - Creates package.json with all npm packages used in workflows - - Runs npm install --package-lock-only to generate package-lock.json - - Creates .github/dependabot.yml for automatic dependency updates - - Requires npm to be installed and available in PATH +The --dependabot flag generates dependency manifests when dependencies are detected: + - For npm: Creates package.json and package-lock.json (requires npm in PATH) + - For Python: Creates requirements.txt for pip packages + - For Go: Creates go.mod for go install/get packages + - Creates .github/dependabot.yml with all detected ecosystems - Use --force to overwrite existing dependabot.yml - Cannot be used with specific workflow files or custom --workflows-dir - Only processes workflows in the default .github/workflows directory @@ -141,7 +141,7 @@ Examples: ` + constants.CLIExtensionPrefix + ` compile --workflows-dir custom/workflows # Compile from custom directory ` + constants.CLIExtensionPrefix + ` compile --watch ci-doctor # Watch and auto-compile ` + constants.CLIExtensionPrefix + ` compile --trial --logical-repo owner/repo # Compile for trial mode - ` + constants.CLIExtensionPrefix + ` compile --dependabot # Generate Dependabot manifests for npm dependencies + ` + constants.CLIExtensionPrefix + ` compile --dependabot # Generate Dependabot manifests ` + constants.CLIExtensionPrefix + ` compile --dependabot --force # Force overwrite existing dependabot.yml`, Run: func(cmd *cobra.Command, args []string) { engineOverride, _ := cmd.Flags().GetString("engine") @@ -280,7 +280,7 @@ func init() { compileCmd.Flags().Bool("strict", false, "Enable strict mode: require timeout, refuse write permissions, require network configuration") compileCmd.Flags().Bool("trial", false, "Enable trial mode compilation (modifies workflows for trial execution)") compileCmd.Flags().String("logical-repo", "", "Repository to simulate workflow execution against (for trial mode)") - compileCmd.Flags().Bool("dependabot", false, "Generate npm package manifest/lockfile and Dependabot config when npm dependencies are detected") + compileCmd.Flags().Bool("dependabot", false, "Generate dependency manifests (package.json, requirements.txt, go.mod) and Dependabot config when dependencies are detected") compileCmd.Flags().Bool("force", false, "Force overwrite of existing files (e.g., dependabot.yml)") rootCmd.AddCommand(compileCmd) diff --git a/pkg/workflow/dependabot.go b/pkg/workflow/dependabot.go index 1638be6f3e6..1aed0f777f7 100644 --- a/pkg/workflow/dependabot.go +++ b/pkg/workflow/dependabot.go @@ -420,293 +420,293 @@ func (c *Compiler) generateDependabotConfig(path string, ecosystems map[string]b // collectPipDependencies collects all pip dependencies from workflow data func (c *Compiler) collectPipDependencies(workflowDataList []*WorkflowData) []PipDependency { -dependabotLog.Print("Collecting pip dependencies from workflows") + dependabotLog.Print("Collecting pip dependencies from workflows") -depMap := make(map[string]string) // package name -> version (last seen) + depMap := make(map[string]string) // package name -> version (last seen) -for _, workflowData := range workflowDataList { -packages := extractPipPackages(workflowData) -for _, pkg := range packages { -dep := parsePipPackage(pkg) -depMap[dep.Name] = dep.Version -} -} + for _, workflowData := range workflowDataList { + packages := extractPipPackages(workflowData) + for _, pkg := range packages { + dep := parsePipPackage(pkg) + depMap[dep.Name] = dep.Version + } + } -// Convert map to sorted slice -var deps []PipDependency -for name, version := range depMap { -deps = append(deps, PipDependency{ -Name: name, -Version: version, -}) -} + // Convert map to sorted slice + var deps []PipDependency + for name, version := range depMap { + deps = append(deps, PipDependency{ + Name: name, + Version: version, + }) + } -// Sort by name for deterministic output -sort.Slice(deps, func(i, j int) bool { -return deps[i].Name < deps[j].Name -}) + // Sort by name for deterministic output + sort.Slice(deps, func(i, j int) bool { + return deps[i].Name < deps[j].Name + }) -dependabotLog.Printf("Collected %d unique pip dependencies", len(deps)) -return deps + dependabotLog.Printf("Collected %d unique pip dependencies", len(deps)) + return deps } // parsePipPackage parses a pip package string like "requests==2.28.0" into name and version func parsePipPackage(pkg string) PipDependency { -// Handle version specifiers (==, >=, <=, >, <, !=, ~=) -for _, sep := range []string{"==", ">=", "<=", "!=", "~=", ">", "<"} { -if idx := strings.Index(pkg, sep); idx > 0 { -return PipDependency{ -Name: pkg[:idx], -Version: pkg[idx:], // Include the separator -} -} -} + // Handle version specifiers (==, >=, <=, >, <, !=, ~=) + for _, sep := range []string{"==", ">=", "<=", "!=", "~=", ">", "<"} { + if idx := strings.Index(pkg, sep); idx > 0 { + return PipDependency{ + Name: pkg[:idx], + Version: pkg[idx:], // Include the separator + } + } + } -// No version specified -return PipDependency{ -Name: pkg, -Version: "", -} + // No version specified + return PipDependency{ + Name: pkg, + Version: "", + } } // generateRequirementsTxt creates or updates requirements.txt with dependencies func (c *Compiler) generateRequirementsTxt(path string, deps []PipDependency, forceOverwrite bool) error { -dependabotLog.Printf("Generating requirements.txt at %s", path) - -// Build requirements map for merging -reqMap := make(map[string]string) -for _, dep := range deps { -if dep.Version != "" { -reqMap[dep.Name] = dep.Version -} else { -reqMap[dep.Name] = "" -} -} + dependabotLog.Printf("Generating requirements.txt at %s", path) -// Check if requirements.txt already exists -if _, err := os.Stat(path); err == nil { -// File exists - merge dependencies -dependabotLog.Print("Existing requirements.txt found, merging dependencies") + // Build requirements map for merging + reqMap := make(map[string]string) + for _, dep := range deps { + if dep.Version != "" { + reqMap[dep.Name] = dep.Version + } else { + reqMap[dep.Name] = "" + } + } -existingData, err := os.ReadFile(path) -if err != nil { -return fmt.Errorf("failed to read existing requirements.txt: %w", err) -} + // Check if requirements.txt already exists + if _, err := os.Stat(path); err == nil { + // File exists - merge dependencies + dependabotLog.Print("Existing requirements.txt found, merging dependencies") -// Parse existing requirements -lines := strings.Split(string(existingData), "\n") -for _, line := range lines { -line = strings.TrimSpace(line) -if line == "" || strings.HasPrefix(line, "#") { -continue -} -dep := parsePipPackage(line) -// Only add if not already in our new deps -if _, exists := reqMap[dep.Name]; !exists { -if dep.Version != "" { -reqMap[dep.Name] = dep.Version -} else { -reqMap[dep.Name] = "" -} -} -} + existingData, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read existing requirements.txt: %w", err) + } -if c.verbose { -fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Merging with existing requirements.txt")) -} -} else { -dependabotLog.Print("Creating new requirements.txt") -} + // Parse existing requirements + lines := strings.Split(string(existingData), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + dep := parsePipPackage(line) + // Only add if not already in our new deps + if _, exists := reqMap[dep.Name]; !exists { + if dep.Version != "" { + reqMap[dep.Name] = dep.Version + } else { + reqMap[dep.Name] = "" + } + } + } -// Sort dependencies by name -var sortedNames []string -for name := range reqMap { -sortedNames = append(sortedNames, name) -} -sort.Strings(sortedNames) - -// Build requirements.txt content -var lines []string -for _, name := range sortedNames { -version := reqMap[name] -if version != "" { -lines = append(lines, name+version) -} else { -lines = append(lines, name) -} -} + if c.verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Merging with existing requirements.txt")) + } + } else { + dependabotLog.Print("Creating new requirements.txt") + } -content := strings.Join(lines, "\n") + "\n" + // Sort dependencies by name + var sortedNames []string + for name := range reqMap { + sortedNames = append(sortedNames, name) + } + sort.Strings(sortedNames) + + // Build requirements.txt content + var lines []string + for _, name := range sortedNames { + version := reqMap[name] + if version != "" { + lines = append(lines, name+version) + } else { + lines = append(lines, name) + } + } -if err := os.WriteFile(path, []byte(content), 0644); err != nil { -return fmt.Errorf("failed to write requirements.txt: %w", err) -} + content := strings.Join(lines, "\n") + "\n" -dependabotLog.Printf("Successfully wrote requirements.txt with %d dependencies", len(reqMap)) -if c.verbose { -fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Generated requirements.txt with %d dependencies", len(reqMap)))) -} + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + return fmt.Errorf("failed to write requirements.txt: %w", err) + } -// Track the created file -if c.fileTracker != nil { -c.fileTracker.TrackCreated(path) -} + dependabotLog.Printf("Successfully wrote requirements.txt with %d dependencies", len(reqMap)) + if c.verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Generated requirements.txt with %d dependencies", len(reqMap)))) + } -return nil + // Track the created file + if c.fileTracker != nil { + c.fileTracker.TrackCreated(path) + } + + return nil } // collectGoDependencies collects all Go dependencies from workflow data func (c *Compiler) collectGoDependencies(workflowDataList []*WorkflowData) []GoDependency { -dependabotLog.Print("Collecting Go dependencies from workflows") + dependabotLog.Print("Collecting Go dependencies from workflows") -depMap := make(map[string]string) // package path -> version (last seen) + depMap := make(map[string]string) // package path -> version (last seen) -for _, workflowData := range workflowDataList { -packages := extractGoPackages(workflowData) -for _, pkg := range packages { -dep := parseGoPackage(pkg) -depMap[dep.Path] = dep.Version -} -} + for _, workflowData := range workflowDataList { + packages := extractGoPackages(workflowData) + for _, pkg := range packages { + dep := parseGoPackage(pkg) + depMap[dep.Path] = dep.Version + } + } -// Convert map to sorted slice -var deps []GoDependency -for path, version := range depMap { -deps = append(deps, GoDependency{ -Path: path, -Version: version, -}) -} + // Convert map to sorted slice + var deps []GoDependency + for path, version := range depMap { + deps = append(deps, GoDependency{ + Path: path, + Version: version, + }) + } -// Sort by path for deterministic output -sort.Slice(deps, func(i, j int) bool { -return deps[i].Path < deps[j].Path -}) + // Sort by path for deterministic output + sort.Slice(deps, func(i, j int) bool { + return deps[i].Path < deps[j].Path + }) -dependabotLog.Printf("Collected %d unique Go dependencies", len(deps)) -return deps + dependabotLog.Printf("Collected %d unique Go dependencies", len(deps)) + return deps } // parseGoPackage parses a Go package string like "github.com/user/repo@v1.2.3" into path and version func parseGoPackage(pkg string) GoDependency { -// Handle version separator @ -if idx := strings.Index(pkg, "@"); idx > 0 { -return GoDependency{ -Path: pkg[:idx], -Version: pkg[idx+1:], -} -} + // Handle version separator @ + if idx := strings.Index(pkg, "@"); idx > 0 { + return GoDependency{ + Path: pkg[:idx], + Version: pkg[idx+1:], + } + } -// No version specified - will use latest -return GoDependency{ -Path: pkg, -Version: "latest", -} + // No version specified - will use latest + return GoDependency{ + Path: pkg, + Version: "latest", + } } // extractGoPackages extracts Go package paths from workflow data func extractGoPackages(workflowData *WorkflowData) []string { -return collectPackagesFromWorkflow(workflowData, extractGoFromCommands, "") + return collectPackagesFromWorkflow(workflowData, extractGoFromCommands, "") } // extractGoFromCommands extracts Go package paths from command strings func extractGoFromCommands(commands string) []string { -var packages []string -lines := strings.Split(commands, "\n") - -for _, line := range lines { -// Look for "go install " or "go get " patterns -words := strings.Fields(line) -for i, word := range words { -if word == "go" && i+1 < len(words) { -cmd := words[i+1] -if cmd == "install" || cmd == "get" { -// Find the package path -for j := i + 2; j < len(words); j++ { -pkg := words[j] -pkg = strings.TrimRight(pkg, "&|;") -// Skip flags (start with - or --) -if !strings.HasPrefix(pkg, "-") { -packages = append(packages, pkg) -break -} -} -} -} -} -} + var packages []string + lines := strings.Split(commands, "\n") + + for _, line := range lines { + // Look for "go install " or "go get " patterns + words := strings.Fields(line) + for i, word := range words { + if word == "go" && i+1 < len(words) { + cmd := words[i+1] + if cmd == "install" || cmd == "get" { + // Find the package path + for j := i + 2; j < len(words); j++ { + pkg := words[j] + pkg = strings.TrimRight(pkg, "&|;") + // Skip flags (start with - or --) + if !strings.HasPrefix(pkg, "-") { + packages = append(packages, pkg) + break + } + } + } + } + } + } -return packages + return packages } // generateGoMod creates or updates go.mod with dependencies func (c *Compiler) generateGoMod(path string, deps []GoDependency, forceOverwrite bool) error { -dependabotLog.Printf("Generating go.mod at %s", path) + dependabotLog.Printf("Generating go.mod at %s", path) -// Build module content -var lines []string + // Build module content + var lines []string -// Check if go.mod already exists -if _, err := os.Stat(path); err == nil { -// File exists - read and preserve module declaration -dependabotLog.Print("Existing go.mod found, merging dependencies") + // Check if go.mod already exists + if _, err := os.Stat(path); err == nil { + // File exists - read and preserve module declaration + dependabotLog.Print("Existing go.mod found, merging dependencies") -existingData, err := os.ReadFile(path) -if err != nil { -return fmt.Errorf("failed to read existing go.mod: %w", err) -} + existingData, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read existing go.mod: %w", err) + } -existingLines := strings.Split(string(existingData), "\n") -// Keep module declaration and go version -for _, line := range existingLines { -trimmed := strings.TrimSpace(line) -if strings.HasPrefix(trimmed, "module ") || strings.HasPrefix(trimmed, "go ") { -lines = append(lines, line) -} -} + existingLines := strings.Split(string(existingData), "\n") + // Keep module declaration and go version + for _, line := range existingLines { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "module ") || strings.HasPrefix(trimmed, "go ") { + lines = append(lines, line) + } + } -if c.verbose { -fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Merging with existing go.mod")) -} -} else { -// New go.mod -dependabotLog.Print("Creating new go.mod") -lines = append(lines, "module github.com/githubnext/gh-aw-workflows-deps") -lines = append(lines, "") -lines = append(lines, "go 1.21") -} + if c.verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Merging with existing go.mod")) + } + } else { + // New go.mod + dependabotLog.Print("Creating new go.mod") + lines = append(lines, "module github.com/githubnext/gh-aw-workflows-deps") + lines = append(lines, "") + lines = append(lines, "go 1.21") + } -// Add require section if we have dependencies -if len(deps) > 0 { -lines = append(lines, "") -lines = append(lines, "require (") -for _, dep := range deps { -version := dep.Version -if version == "latest" || version == "" { -// For latest, we need to use a placeholder or skip -// Dependabot will update to actual versions -version = "v0.0.0" -} -lines = append(lines, fmt.Sprintf("\t%s %s", dep.Path, version)) -} -lines = append(lines, ")") -} + // Add require section if we have dependencies + if len(deps) > 0 { + lines = append(lines, "") + lines = append(lines, "require (") + for _, dep := range deps { + version := dep.Version + if version == "latest" || version == "" { + // For latest, we need to use a placeholder or skip + // Dependabot will update to actual versions + version = "v0.0.0" + } + lines = append(lines, fmt.Sprintf("\t%s %s", dep.Path, version)) + } + lines = append(lines, ")") + } -content := strings.Join(lines, "\n") + "\n" + content := strings.Join(lines, "\n") + "\n" -if err := os.WriteFile(path, []byte(content), 0644); err != nil { -return fmt.Errorf("failed to write go.mod: %w", err) -} + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + return fmt.Errorf("failed to write go.mod: %w", err) + } -dependabotLog.Printf("Successfully wrote go.mod with %d dependencies", len(deps)) -if c.verbose { -fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Generated go.mod with %d dependencies", len(deps)))) -} + dependabotLog.Printf("Successfully wrote go.mod with %d dependencies", len(deps)) + if c.verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Generated go.mod with %d dependencies", len(deps)))) + } -// Track the created file -if c.fileTracker != nil { -c.fileTracker.TrackCreated(path) -} + // Track the created file + if c.fileTracker != nil { + c.fileTracker.TrackCreated(path) + } -return nil + return nil } diff --git a/pkg/workflow/dependabot_test.go b/pkg/workflow/dependabot_test.go index e62e90b81f7..77613009d19 100644 --- a/pkg/workflow/dependabot_test.go +++ b/pkg/workflow/dependabot_test.go @@ -430,434 +430,434 @@ func TestGenerateDependabotManifests_StrictMode(t *testing.T) { // Tests for Python (pip) support func TestParsePipPackage(t *testing.T) { -tests := []struct { -name string -pkg string -expectedName string -expectedVersion string -}{ -{ -name: "package with == version", -pkg: "requests==2.28.0", -expectedName: "requests", -expectedVersion: "==2.28.0", -}, -{ -name: "package with >= version", -pkg: "django>=3.2.0", -expectedName: "django", -expectedVersion: ">=3.2.0", -}, -{ -name: "package with ~= version", -pkg: "flask~=2.0.0", -expectedName: "flask", -expectedVersion: "~=2.0.0", -}, -{ -name: "package without version", -pkg: "numpy", -expectedName: "numpy", -expectedVersion: "", -}, -{ -name: "package with != version", -pkg: "pytest!=7.0.0", -expectedName: "pytest", -expectedVersion: "!=7.0.0", -}, -} + tests := []struct { + name string + pkg string + expectedName string + expectedVersion string + }{ + { + name: "package with == version", + pkg: "requests==2.28.0", + expectedName: "requests", + expectedVersion: "==2.28.0", + }, + { + name: "package with >= version", + pkg: "django>=3.2.0", + expectedName: "django", + expectedVersion: ">=3.2.0", + }, + { + name: "package with ~= version", + pkg: "flask~=2.0.0", + expectedName: "flask", + expectedVersion: "~=2.0.0", + }, + { + name: "package without version", + pkg: "numpy", + expectedName: "numpy", + expectedVersion: "", + }, + { + name: "package with != version", + pkg: "pytest!=7.0.0", + expectedName: "pytest", + expectedVersion: "!=7.0.0", + }, + } -for _, tt := range tests { -t.Run(tt.name, func(t *testing.T) { -dep := parsePipPackage(tt.pkg) -if dep.Name != tt.expectedName { -t.Errorf("expected name %q, got %q", tt.expectedName, dep.Name) -} -if dep.Version != tt.expectedVersion { -t.Errorf("expected version %q, got %q", tt.expectedVersion, dep.Version) -} -}) -} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dep := parsePipPackage(tt.pkg) + if dep.Name != tt.expectedName { + t.Errorf("expected name %q, got %q", tt.expectedName, dep.Name) + } + if dep.Version != tt.expectedVersion { + t.Errorf("expected version %q, got %q", tt.expectedVersion, dep.Version) + } + }) + } } func TestCollectPipDependencies(t *testing.T) { -compiler := NewCompiler(false, "", "test") - -tests := []struct { -name string -workflows []*WorkflowData -expectedDeps []PipDependency -}{ -{ -name: "single workflow with pip dependencies", -workflows: []*WorkflowData{ -{ -CustomSteps: "pip install requests==2.28.0", -}, -}, -expectedDeps: []PipDependency{ -{Name: "requests", Version: "==2.28.0"}, -}, -}, -{ -name: "multiple workflows with different dependencies", -workflows: []*WorkflowData{ -{ -CustomSteps: "pip install requests==2.28.0", -}, -{ -CustomSteps: "pip3 install django>=3.2.0", -}, -}, -expectedDeps: []PipDependency{ -{Name: "django", Version: ">=3.2.0"}, -{Name: "requests", Version: "==2.28.0"}, -}, -}, -{ -name: "duplicate dependencies use last version", -workflows: []*WorkflowData{ -{ -CustomSteps: "pip install requests==2.27.0", -}, -{ -CustomSteps: "pip install requests==2.28.0", -}, -}, -expectedDeps: []PipDependency{ -{Name: "requests", Version: "==2.28.0"}, -}, -}, -{ -name: "no pip dependencies", -workflows: []*WorkflowData{}, -expectedDeps: []PipDependency{}, -}, -} + compiler := NewCompiler(false, "", "test") -for _, tt := range tests { -t.Run(tt.name, func(t *testing.T) { -deps := compiler.collectPipDependencies(tt.workflows) -if len(deps) != len(tt.expectedDeps) { -t.Errorf("expected %d dependencies, got %d", len(tt.expectedDeps), len(deps)) -} -for i, dep := range deps { -if i >= len(tt.expectedDeps) { -break -} -expected := tt.expectedDeps[i] -if dep.Name != expected.Name { -t.Errorf("dependency %d: expected name %q, got %q", i, expected.Name, dep.Name) -} -if dep.Version != expected.Version { -t.Errorf("dependency %d: expected version %q, got %q", i, expected.Version, dep.Version) -} -} -}) -} + tests := []struct { + name string + workflows []*WorkflowData + expectedDeps []PipDependency + }{ + { + name: "single workflow with pip dependencies", + workflows: []*WorkflowData{ + { + CustomSteps: "pip install requests==2.28.0", + }, + }, + expectedDeps: []PipDependency{ + {Name: "requests", Version: "==2.28.0"}, + }, + }, + { + name: "multiple workflows with different dependencies", + workflows: []*WorkflowData{ + { + CustomSteps: "pip install requests==2.28.0", + }, + { + CustomSteps: "pip3 install django>=3.2.0", + }, + }, + expectedDeps: []PipDependency{ + {Name: "django", Version: ">=3.2.0"}, + {Name: "requests", Version: "==2.28.0"}, + }, + }, + { + name: "duplicate dependencies use last version", + workflows: []*WorkflowData{ + { + CustomSteps: "pip install requests==2.27.0", + }, + { + CustomSteps: "pip install requests==2.28.0", + }, + }, + expectedDeps: []PipDependency{ + {Name: "requests", Version: "==2.28.0"}, + }, + }, + { + name: "no pip dependencies", + workflows: []*WorkflowData{}, + expectedDeps: []PipDependency{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + deps := compiler.collectPipDependencies(tt.workflows) + if len(deps) != len(tt.expectedDeps) { + t.Errorf("expected %d dependencies, got %d", len(tt.expectedDeps), len(deps)) + } + for i, dep := range deps { + if i >= len(tt.expectedDeps) { + break + } + expected := tt.expectedDeps[i] + if dep.Name != expected.Name { + t.Errorf("dependency %d: expected name %q, got %q", i, expected.Name, dep.Name) + } + if dep.Version != expected.Version { + t.Errorf("dependency %d: expected version %q, got %q", i, expected.Version, dep.Version) + } + } + }) + } } func TestGenerateRequirementsTxt(t *testing.T) { -compiler := NewCompiler(false, "", "test") -tempDir := t.TempDir() -requirementsPath := filepath.Join(tempDir, "requirements.txt") + compiler := NewCompiler(false, "", "test") + tempDir := t.TempDir() + requirementsPath := filepath.Join(tempDir, "requirements.txt") -deps := []PipDependency{ -{Name: "requests", Version: "==2.28.0"}, -{Name: "django", Version: ">=3.2.0"}, -} + deps := []PipDependency{ + {Name: "requests", Version: "==2.28.0"}, + {Name: "django", Version: ">=3.2.0"}, + } -// Test creating new requirements.txt -err := compiler.generateRequirementsTxt(requirementsPath, deps, false) -if err != nil { -t.Fatalf("failed to generate requirements.txt: %v", err) -} + // Test creating new requirements.txt + err := compiler.generateRequirementsTxt(requirementsPath, deps, false) + if err != nil { + t.Fatalf("failed to generate requirements.txt: %v", err) + } -// Verify file was created -if _, err := os.Stat(requirementsPath); os.IsNotExist(err) { -t.Fatal("requirements.txt was not created") -} + // Verify file was created + if _, err := os.Stat(requirementsPath); os.IsNotExist(err) { + t.Fatal("requirements.txt was not created") + } -// Read and verify content -data, err := os.ReadFile(requirementsPath) -if err != nil { -t.Fatalf("failed to read requirements.txt: %v", err) -} + // Read and verify content + data, err := os.ReadFile(requirementsPath) + if err != nil { + t.Fatalf("failed to read requirements.txt: %v", err) + } -content := string(data) -if !strings.Contains(content, "django>=3.2.0") { -t.Error("requirements.txt should contain django>=3.2.0") -} -if !strings.Contains(content, "requests==2.28.0") { -t.Error("requirements.txt should contain requests==2.28.0") -} + content := string(data) + if !strings.Contains(content, "django>=3.2.0") { + t.Error("requirements.txt should contain django>=3.2.0") + } + if !strings.Contains(content, "requests==2.28.0") { + t.Error("requirements.txt should contain requests==2.28.0") + } } // Tests for Golang support func TestParseGoPackage(t *testing.T) { -tests := []struct { -name string -pkg string -expectedPath string -expectedVersion string -}{ -{ -name: "package with version", -pkg: "github.com/user/repo@v1.2.3", -expectedPath: "github.com/user/repo", -expectedVersion: "v1.2.3", -}, -{ -name: "package without version", -pkg: "github.com/user/repo", -expectedPath: "github.com/user/repo", -expectedVersion: "latest", -}, -{ -name: "package with pseudo-version", -pkg: "golang.org/x/tools@v0.1.12-0.20220713141851-7464d2807d88", -expectedPath: "golang.org/x/tools", -expectedVersion: "v0.1.12-0.20220713141851-7464d2807d88", -}, -} + tests := []struct { + name string + pkg string + expectedPath string + expectedVersion string + }{ + { + name: "package with version", + pkg: "github.com/user/repo@v1.2.3", + expectedPath: "github.com/user/repo", + expectedVersion: "v1.2.3", + }, + { + name: "package without version", + pkg: "github.com/user/repo", + expectedPath: "github.com/user/repo", + expectedVersion: "latest", + }, + { + name: "package with pseudo-version", + pkg: "golang.org/x/tools@v0.1.12-0.20220713141851-7464d2807d88", + expectedPath: "golang.org/x/tools", + expectedVersion: "v0.1.12-0.20220713141851-7464d2807d88", + }, + } -for _, tt := range tests { -t.Run(tt.name, func(t *testing.T) { -dep := parseGoPackage(tt.pkg) -if dep.Path != tt.expectedPath { -t.Errorf("expected path %q, got %q", tt.expectedPath, dep.Path) -} -if dep.Version != tt.expectedVersion { -t.Errorf("expected version %q, got %q", tt.expectedVersion, dep.Version) -} -}) -} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dep := parseGoPackage(tt.pkg) + if dep.Path != tt.expectedPath { + t.Errorf("expected path %q, got %q", tt.expectedPath, dep.Path) + } + if dep.Version != tt.expectedVersion { + t.Errorf("expected version %q, got %q", tt.expectedVersion, dep.Version) + } + }) + } } func TestCollectGoDependencies(t *testing.T) { -compiler := NewCompiler(false, "", "test") - -tests := []struct { -name string -workflows []*WorkflowData -expectedDeps []GoDependency -}{ -{ -name: "single workflow with go install", -workflows: []*WorkflowData{ -{ -CustomSteps: "go install github.com/user/tool@v1.0.0", -}, -}, -expectedDeps: []GoDependency{ -{Path: "github.com/user/tool", Version: "v1.0.0"}, -}, -}, -{ -name: "multiple workflows with different dependencies", -workflows: []*WorkflowData{ -{ -CustomSteps: "go install github.com/user/tool@v1.0.0", -}, -{ -CustomSteps: "go get golang.org/x/tools@latest", -}, -}, -expectedDeps: []GoDependency{ -{Path: "github.com/user/tool", Version: "v1.0.0"}, -{Path: "golang.org/x/tools", Version: "latest"}, -}, -}, -{ -name: "duplicate dependencies use last version", -workflows: []*WorkflowData{ -{ -CustomSteps: "go install github.com/user/tool@v1.0.0", -}, -{ -CustomSteps: "go install github.com/user/tool@v2.0.0", -}, -}, -expectedDeps: []GoDependency{ -{Path: "github.com/user/tool", Version: "v2.0.0"}, -}, -}, -{ -name: "no go dependencies", -workflows: []*WorkflowData{}, -expectedDeps: []GoDependency{}, -}, -} + compiler := NewCompiler(false, "", "test") -for _, tt := range tests { -t.Run(tt.name, func(t *testing.T) { -deps := compiler.collectGoDependencies(tt.workflows) -if len(deps) != len(tt.expectedDeps) { -t.Errorf("expected %d dependencies, got %d", len(tt.expectedDeps), len(deps)) -} -for i, dep := range deps { -if i >= len(tt.expectedDeps) { -break -} -expected := tt.expectedDeps[i] -if dep.Path != expected.Path { -t.Errorf("dependency %d: expected path %q, got %q", i, expected.Path, dep.Path) -} -if dep.Version != expected.Version { -t.Errorf("dependency %d: expected version %q, got %q", i, expected.Version, dep.Version) -} -} -}) -} + tests := []struct { + name string + workflows []*WorkflowData + expectedDeps []GoDependency + }{ + { + name: "single workflow with go install", + workflows: []*WorkflowData{ + { + CustomSteps: "go install github.com/user/tool@v1.0.0", + }, + }, + expectedDeps: []GoDependency{ + {Path: "github.com/user/tool", Version: "v1.0.0"}, + }, + }, + { + name: "multiple workflows with different dependencies", + workflows: []*WorkflowData{ + { + CustomSteps: "go install github.com/user/tool@v1.0.0", + }, + { + CustomSteps: "go get golang.org/x/tools@latest", + }, + }, + expectedDeps: []GoDependency{ + {Path: "github.com/user/tool", Version: "v1.0.0"}, + {Path: "golang.org/x/tools", Version: "latest"}, + }, + }, + { + name: "duplicate dependencies use last version", + workflows: []*WorkflowData{ + { + CustomSteps: "go install github.com/user/tool@v1.0.0", + }, + { + CustomSteps: "go install github.com/user/tool@v2.0.0", + }, + }, + expectedDeps: []GoDependency{ + {Path: "github.com/user/tool", Version: "v2.0.0"}, + }, + }, + { + name: "no go dependencies", + workflows: []*WorkflowData{}, + expectedDeps: []GoDependency{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + deps := compiler.collectGoDependencies(tt.workflows) + if len(deps) != len(tt.expectedDeps) { + t.Errorf("expected %d dependencies, got %d", len(tt.expectedDeps), len(deps)) + } + for i, dep := range deps { + if i >= len(tt.expectedDeps) { + break + } + expected := tt.expectedDeps[i] + if dep.Path != expected.Path { + t.Errorf("dependency %d: expected path %q, got %q", i, expected.Path, dep.Path) + } + if dep.Version != expected.Version { + t.Errorf("dependency %d: expected version %q, got %q", i, expected.Version, dep.Version) + } + } + }) + } } func TestGenerateGoMod(t *testing.T) { -compiler := NewCompiler(false, "", "test") -tempDir := t.TempDir() -goModPath := filepath.Join(tempDir, "go.mod") + compiler := NewCompiler(false, "", "test") + tempDir := t.TempDir() + goModPath := filepath.Join(tempDir, "go.mod") -deps := []GoDependency{ -{Path: "github.com/user/tool", Version: "v1.0.0"}, -{Path: "golang.org/x/tools", Version: "v0.1.0"}, -} + deps := []GoDependency{ + {Path: "github.com/user/tool", Version: "v1.0.0"}, + {Path: "golang.org/x/tools", Version: "v0.1.0"}, + } -// Test creating new go.mod -err := compiler.generateGoMod(goModPath, deps, false) -if err != nil { -t.Fatalf("failed to generate go.mod: %v", err) -} + // Test creating new go.mod + err := compiler.generateGoMod(goModPath, deps, false) + if err != nil { + t.Fatalf("failed to generate go.mod: %v", err) + } -// Verify file was created -if _, err := os.Stat(goModPath); os.IsNotExist(err) { -t.Fatal("go.mod was not created") -} + // Verify file was created + if _, err := os.Stat(goModPath); os.IsNotExist(err) { + t.Fatal("go.mod was not created") + } -// Read and verify content -data, err := os.ReadFile(goModPath) -if err != nil { -t.Fatalf("failed to read go.mod: %v", err) -} + // Read and verify content + data, err := os.ReadFile(goModPath) + if err != nil { + t.Fatalf("failed to read go.mod: %v", err) + } -content := string(data) -if !strings.Contains(content, "module github.com/githubnext/gh-aw-workflows-deps") { -t.Error("go.mod should contain module declaration") -} -if !strings.Contains(content, "require (") { -t.Error("go.mod should contain require section") -} -if !strings.Contains(content, "github.com/user/tool v1.0.0") { -t.Error("go.mod should contain github.com/user/tool v1.0.0") -} -if !strings.Contains(content, "golang.org/x/tools v0.1.0") { -t.Error("go.mod should contain golang.org/x/tools v0.1.0") -} + content := string(data) + if !strings.Contains(content, "module github.com/githubnext/gh-aw-workflows-deps") { + t.Error("go.mod should contain module declaration") + } + if !strings.Contains(content, "require (") { + t.Error("go.mod should contain require section") + } + if !strings.Contains(content, "github.com/user/tool v1.0.0") { + t.Error("go.mod should contain github.com/user/tool v1.0.0") + } + if !strings.Contains(content, "golang.org/x/tools v0.1.0") { + t.Error("go.mod should contain golang.org/x/tools v0.1.0") + } } // Tests for multi-ecosystem support func TestGenerateDependabotConfig_MultipleEcosystems(t *testing.T) { -compiler := NewCompiler(false, "", "test") -tempDir := t.TempDir() -dependabotPath := filepath.Join(tempDir, "dependabot.yml") - -ecosystems := map[string]bool{ -"npm": true, -"pip": true, -"gomod": true, -} + compiler := NewCompiler(false, "", "test") + tempDir := t.TempDir() + dependabotPath := filepath.Join(tempDir, "dependabot.yml") -// Test creating new dependabot.yml with multiple ecosystems -err := compiler.generateDependabotConfig(dependabotPath, ecosystems, false) -if err != nil { -t.Fatalf("failed to generate dependabot.yml: %v", err) -} + ecosystems := map[string]bool{ + "npm": true, + "pip": true, + "gomod": true, + } -// Verify file was created -if _, err := os.Stat(dependabotPath); os.IsNotExist(err) { -t.Fatal("dependabot.yml was not created") -} + // Test creating new dependabot.yml with multiple ecosystems + err := compiler.generateDependabotConfig(dependabotPath, ecosystems, false) + if err != nil { + t.Fatalf("failed to generate dependabot.yml: %v", err) + } -// Read and verify content -data, err := os.ReadFile(dependabotPath) -if err != nil { -t.Fatalf("failed to read dependabot.yml: %v", err) -} + // Verify file was created + if _, err := os.Stat(dependabotPath); os.IsNotExist(err) { + t.Fatal("dependabot.yml was not created") + } -var config DependabotConfig -if err := yaml.Unmarshal(data, &config); err != nil { -t.Fatalf("failed to parse dependabot.yml: %v", err) -} + // Read and verify content + data, err := os.ReadFile(dependabotPath) + if err != nil { + t.Fatalf("failed to read dependabot.yml: %v", err) + } -// Verify structure -if config.Version != 2 { -t.Errorf("expected version 2, got %d", config.Version) -} -if len(config.Updates) != 3 { -t.Fatalf("expected 3 update entries, got %d", len(config.Updates)) -} + var config DependabotConfig + if err := yaml.Unmarshal(data, &config); err != nil { + t.Fatalf("failed to parse dependabot.yml: %v", err) + } -// Check that all ecosystems are present -ecosystemsFound := make(map[string]bool) -for _, update := range config.Updates { -ecosystemsFound[update.PackageEcosystem] = true -if update.Directory != "/.github/workflows" { -t.Errorf("expected directory '/.github/workflows', got %q", update.Directory) -} -if update.Schedule.Interval != "weekly" { -t.Errorf("expected interval 'weekly', got %q", update.Schedule.Interval) -} -} + // Verify structure + if config.Version != 2 { + t.Errorf("expected version 2, got %d", config.Version) + } + if len(config.Updates) != 3 { + t.Fatalf("expected 3 update entries, got %d", len(config.Updates)) + } -for ecosystem := range ecosystems { -if !ecosystemsFound[ecosystem] { -t.Errorf("ecosystem %q not found in dependabot.yml", ecosystem) -} -} + // Check that all ecosystems are present + ecosystemsFound := make(map[string]bool) + for _, update := range config.Updates { + ecosystemsFound[update.PackageEcosystem] = true + if update.Directory != "/.github/workflows" { + t.Errorf("expected directory '/.github/workflows', got %q", update.Directory) + } + if update.Schedule.Interval != "weekly" { + t.Errorf("expected interval 'weekly', got %q", update.Schedule.Interval) + } + } + + for ecosystem := range ecosystems { + if !ecosystemsFound[ecosystem] { + t.Errorf("ecosystem %q not found in dependabot.yml", ecosystem) + } + } } func TestGenerateDependabotManifests_AllEcosystems(t *testing.T) { -compiler := NewCompiler(true, "", "test") -tempDir := t.TempDir() -workflowDir := filepath.Join(tempDir, ".github", "workflows") -os.MkdirAll(workflowDir, 0755) - -// Workflow with npm, pip, and go dependencies -workflows := []*WorkflowData{ -{ -CustomSteps: ` + compiler := NewCompiler(true, "", "test") + tempDir := t.TempDir() + workflowDir := filepath.Join(tempDir, ".github", "workflows") + os.MkdirAll(workflowDir, 0755) + + // Workflow with npm, pip, and go dependencies + workflows := []*WorkflowData{ + { + CustomSteps: ` npx @playwright/mcp@latest pip install requests==2.28.0 go install github.com/user/tool@v1.0.0 `, -}, -} + }, + } -// This will skip npm install (no npm in test env), but should generate manifest files -_ = compiler.GenerateDependabotManifests(workflows, workflowDir, false) + // This will skip npm install (no npm in test env), but should generate manifest files + _ = compiler.GenerateDependabotManifests(workflows, workflowDir, false) -// Check that package.json was created -packageJSONPath := filepath.Join(workflowDir, "package.json") -if _, err := os.Stat(packageJSONPath); os.IsNotExist(err) { -t.Error("package.json should be created") -} + // Check that package.json was created + packageJSONPath := filepath.Join(workflowDir, "package.json") + if _, err := os.Stat(packageJSONPath); os.IsNotExist(err) { + t.Error("package.json should be created") + } -// Check that requirements.txt was created -requirementsPath := filepath.Join(workflowDir, "requirements.txt") -if _, err := os.Stat(requirementsPath); os.IsNotExist(err) { -t.Error("requirements.txt should be created") -} + // Check that requirements.txt was created + requirementsPath := filepath.Join(workflowDir, "requirements.txt") + if _, err := os.Stat(requirementsPath); os.IsNotExist(err) { + t.Error("requirements.txt should be created") + } -// Check that go.mod was created -goModPath := filepath.Join(workflowDir, "go.mod") -if _, err := os.Stat(goModPath); os.IsNotExist(err) { -t.Error("go.mod should be created") -} + // Check that go.mod was created + goModPath := filepath.Join(workflowDir, "go.mod") + if _, err := os.Stat(goModPath); os.IsNotExist(err) { + t.Error("go.mod should be created") + } -// Check dependabot.yml -dependabotPath := filepath.Join(tempDir, ".github", "dependabot.yml") -if _, err := os.Stat(dependabotPath); os.IsNotExist(err) { -t.Error("dependabot.yml should be created") -} + // Check dependabot.yml + dependabotPath := filepath.Join(tempDir, ".github", "dependabot.yml") + if _, err := os.Stat(dependabotPath); os.IsNotExist(err) { + t.Error("dependabot.yml should be created") + } } From b8abe1074e6dac2343afbc29bb2ea10fc1f6951a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 25 Oct 2025 03:38:43 +0000 Subject: [PATCH 4/5] skip-ci: Add changeset for pip and golang dependabot support --- .changeset/minor-add-pip-golang-dependabot.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/minor-add-pip-golang-dependabot.md diff --git a/.changeset/minor-add-pip-golang-dependabot.md b/.changeset/minor-add-pip-golang-dependabot.md new file mode 100644 index 00000000000..65a9c31e051 --- /dev/null +++ b/.changeset/minor-add-pip-golang-dependabot.md @@ -0,0 +1,5 @@ +--- +"gh-aw": minor +--- + +Add pip and golang dependency manifest generation support From 2e24fae84f6d73b048d86f67a97516c4facec9e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Oct 2025 03:54:10 +0000 Subject: [PATCH 5/5] Fix build errors and improve go.mod generation - Fixed syntax error in generateDependabotConfig from merge conflict - Removed duplicate/orphaned code that referenced non-existent npmUpdate variable - Improved go.mod generation to skip dependencies without explicit versions instead of using v0.0.0 placeholder - Added better documentation for version handling in Go modules All tests passing, build working, lint/format clean. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/dependabot.go | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/pkg/workflow/dependabot.go b/pkg/workflow/dependabot.go index 5aa8b53aad6..5c433e3100e 100644 --- a/pkg/workflow/dependabot.go +++ b/pkg/workflow/dependabot.go @@ -384,18 +384,6 @@ func (c *Compiler) generateDependabotConfig(path string, ecosystems map[string]b entry.Schedule.Interval = "weekly" config.Updates = append(config.Updates, entry) } - npmUpdate.Schedule.Interval = "weekly" - config.Updates = append(config.Updates, npmUpdate) - - dependabotLog.Print("Added npm ecosystem entry to dependabot.yml") - if c.verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Added npm ecosystem to dependabot.yml")) - } - } else { - dependabotLog.Print("npm ecosystem already exists in dependabot.yml") - if c.verbose { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("npm ecosystem already configured in dependabot.yml")) - } } // Write dependabot.yml @@ -686,9 +674,11 @@ func (c *Compiler) generateGoMod(path string, deps []GoDependency, forceOverwrit for _, dep := range deps { version := dep.Version if version == "latest" || version == "" { - // For latest, we need to use a placeholder or skip - // Dependabot will update to actual versions - version = "v0.0.0" + // Skip dependencies without explicit versions - they should be added manually + // or resolved using 'go get' or 'go mod tidy'. Using v0.0.0 as a placeholder + // can cause issues with Go module resolution. + dependabotLog.Printf("Skipping %s: no version specified (use 'go get %s@latest' to resolve)", dep.Path, dep.Path) + continue } lines = append(lines, fmt.Sprintf("\t%s %s", dep.Path, version)) }