diff --git a/docs/src/content/docs/labs.mdx b/docs/src/content/docs/labs.mdx index a99dc0ba8b7..7393cf8caf4 100644 --- a/docs/src/content/docs/labs.mdx +++ b/docs/src/content/docs/labs.mdx @@ -21,7 +21,7 @@ These are experimental agentic workflows used by the GitHub Next team to learn, | [Changeset Generator](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/changeset.md) | codex | [![Changeset Generator](https://github.com/githubnext/gh-aw/actions/workflows/changeset.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/changeset.lock.yml) | - | - | | [CI Failure Doctor](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/ci-doctor.md) | copilot | [![CI Failure Doctor](https://github.com/githubnext/gh-aw/actions/workflows/ci-doctor.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/ci-doctor.lock.yml) | - | - | | [CLI Consistency Checker](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/cli-consistency-checker.md) | copilot | [![CLI Consistency Checker](https://github.com/githubnext/gh-aw/actions/workflows/cli-consistency-checker.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/cli-consistency-checker.lock.yml) | `0 13 * * 1-5` | - | -| [CLI Version Checker](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/cli-version-checker.md) | copilot | [![CLI Version Checker](https://github.com/githubnext/gh-aw/actions/workflows/cli-version-checker.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/cli-version-checker.lock.yml) | `0 15 * * *` | - | +| [CLI Version Checker](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/cli-version-checker.md) | claude | [![CLI Version Checker](https://github.com/githubnext/gh-aw/actions/workflows/cli-version-checker.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/cli-version-checker.lock.yml) | `0 15 * * *` | - | | [Close Outdated Discussions](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/close-old-discussions.md) | codex | [![Close Outdated Discussions](https://github.com/githubnext/gh-aw/actions/workflows/close-old-discussions.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/close-old-discussions.lock.yml) | `0 14 * * 1-5` | - | | [Commit Changes Analyzer](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/commit-changes-analyzer.md) | claude | [![Commit Changes Analyzer](https://github.com/githubnext/gh-aw/actions/workflows/commit-changes-analyzer.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/commit-changes-analyzer.lock.yml) | - | - | | [Copilot Agent PR Analysis](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/copilot-agent-analysis.md) | claude | [![Copilot Agent PR Analysis](https://github.com/githubnext/gh-aw/actions/workflows/copilot-agent-analysis.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/copilot-agent-analysis.lock.yml) | `0 18 * * *` | - | @@ -41,7 +41,7 @@ These are experimental agentic workflows used by the GitHub Next team to learn, | [Daily Team Status](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/daily-team-status.md) | copilot | [![Daily Team Status](https://github.com/githubnext/gh-aw/actions/workflows/daily-team-status.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/daily-team-status.lock.yml) | - | - | | [DeepReport - Intelligence Gathering Agent](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/deep-report.md) | codex | [![DeepReport - Intelligence Gathering Agent](https://github.com/githubnext/gh-aw/actions/workflows/deep-report.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/deep-report.lock.yml) | `0 15 * * 1-5` | - | | [Dependabot Dependency Checker](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/dependabot-go-checker.md) | copilot | [![Dependabot Dependency Checker](https://github.com/githubnext/gh-aw/actions/workflows/dependabot-go-checker.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/dependabot-go-checker.lock.yml) | `0 9 * * 1,3,5` | - | -| [Dev](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/dev.md) | codex | [![Dev](https://github.com/githubnext/gh-aw/actions/workflows/dev.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/dev.lock.yml) | - | - | +| [Dev](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/dev.md) | claude | [![Dev](https://github.com/githubnext/gh-aw/actions/workflows/dev.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/dev.lock.yml) | - | - | | [Dev Hawk](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/dev-hawk.md) | copilot | [![Dev Hawk](https://github.com/githubnext/gh-aw/actions/workflows/dev-hawk.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/dev-hawk.lock.yml) | - | - | | [Developer Documentation Consolidator](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/developer-docs-consolidator.md) | claude | [![Developer Documentation Consolidator](https://github.com/githubnext/gh-aw/actions/workflows/developer-docs-consolidator.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/developer-docs-consolidator.lock.yml) | `17 3 * * *` | - | | [Dictation Prompt Generator](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/dictation-prompt.md) | copilot | [![Dictation Prompt Generator](https://github.com/githubnext/gh-aw/actions/workflows/dictation-prompt.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/dictation-prompt.lock.yml) | `0 6 * * 0` | - | @@ -84,7 +84,8 @@ These are experimental agentic workflows used by the GitHub Next team to learn, | [Semantic Function Refactoring](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/semantic-function-refactor.md) | claude | [![Semantic Function Refactoring](https://github.com/githubnext/gh-aw/actions/workflows/semantic-function-refactor.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/semantic-function-refactor.lock.yml) | `0 8 * * *` | - | | [Smoke Claude](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/smoke-claude.md) | claude | [![Smoke Claude](https://github.com/githubnext/gh-aw/actions/workflows/smoke-claude.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/smoke-claude.lock.yml) | `0 0,6,12,18 * * *` | - | | [Smoke Codex](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/smoke-codex.md) | codex | [![Smoke Codex](https://github.com/githubnext/gh-aw/actions/workflows/smoke-codex.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/smoke-codex.lock.yml) | `0 0,6,12,18 * * *` | - | -| [Smoke Copilot](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/smoke-copilot.md) | copilot | [![Smoke Copilot](https://github.com/githubnext/gh-aw/actions/workflows/smoke-copilot.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/smoke-copilot.lock.yml) | `0 0,6,12,18 * * *` | - | +| [Smoke Copilot](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/smoke-copilot-playwright.md) | copilot | [![Smoke Copilot](https://github.com/githubnext/gh-aw/actions/workflows/smoke-copilot-playwright.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/smoke-copilot-playwright.lock.yml) | `0 0,6,12,18 * * *` | - | +| [Smoke Copilot](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/smoke-copilot.md) | copilot | [![Smoke Copilot](https://github.com/githubnext/gh-aw/actions/workflows/smoke-copilot.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/smoke-copilot.lock.yml) | `0 0,7,13,19 * * *` | - | | [Smoke Copilot No Firewall](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/smoke-copilot-no-firewall.md) | copilot | [![Smoke Copilot No Firewall](https://github.com/githubnext/gh-aw/actions/workflows/smoke-copilot-no-firewall.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/smoke-copilot-no-firewall.lock.yml) | `0 0,6,12,18 * * *` | - | | [Smoke Detector - Smoke Test Failure Investigator](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/smoke-detector.md) | claude | [![Smoke Detector - Smoke Test Failure Investigator](https://github.com/githubnext/gh-aw/actions/workflows/smoke-detector.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/smoke-detector.lock.yml) | - | - | | [Smoke SRT](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/smoke-srt.md) | copilot | [![Smoke SRT](https://github.com/githubnext/gh-aw/actions/workflows/smoke-srt.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/smoke-srt.lock.yml) | - | - | diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index 985db81ad70..3e2e628074f 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -1107,10 +1107,14 @@ tools: # functions) github: null - # Option 2: Simple GitHub tool configuration (enables all GitHub API functions) + # Option 2: Boolean to explicitly enable (true) or disable (false) the GitHub MCP + # server. When set to false, the GitHub MCP server is not mounted. + github: true + + # Option 3: Simple GitHub tool configuration (enables all GitHub API functions) github: "example-value" - # Option 3: GitHub tools object configuration with restricted function access + # Option 4: GitHub tools object configuration with restricted function access github: # List of allowed GitHub API functions (e.g., 'create_issue', 'update_issue', # 'add_comment') @@ -1508,6 +1512,14 @@ safe-outputs: # (optional) target-repo: "example-value" + # List of additional repositories in format 'owner/repo' that issues can be + # created in. When specified, the agent can use a 'repo' field in the output to + # specify which repository to create the issue in. The target repository (current + # or target-repo) is always implicitly allowed. + # (optional) + allowed-repos: [] + # Array of strings + # GitHub token to use for this specific output type. Overrides global github-token # if specified. # (optional) @@ -1602,6 +1614,14 @@ safe-outputs: # (optional) target-repo: "example-value" + # List of additional repositories in format 'owner/repo' that discussions can be + # created in. When specified, the agent can use a 'repo' field in the output to + # specify which repository to create the discussion in. The target repository + # (current or target-repo) is always implicitly allowed. + # (optional) + allowed-repos: [] + # Array of strings + # GitHub token to use for this specific output type. Overrides global github-token # if specified. # (optional) diff --git a/pkg/workflow/dependabot.go b/pkg/workflow/dependabot.go index dc1f6a0b406..3dc85ca15c7 100644 --- a/pkg/workflow/dependabot.go +++ b/pkg/workflow/dependabot.go @@ -602,25 +602,12 @@ func extractGoPackages(workflowData *WorkflowData) []string { // extractGoFromCommands extracts Go package paths from command strings func extractGoFromCommands(commands string) []string { - var packages []string - - // Extract "go install " pattern - installExtractor := PackageExtractor{ - CommandNames: []string{"go"}, - RequiredSubcommand: "install", - TrimSuffixes: "&|;", - } - packages = append(packages, installExtractor.ExtractPackages(commands)...) - - // Extract "go get " pattern - getExtractor := PackageExtractor{ - CommandNames: []string{"go"}, - RequiredSubcommand: "get", - TrimSuffixes: "&|;", + extractor := PackageExtractor{ + CommandNames: []string{"go"}, + RequiredSubcommands: []string{"install", "get"}, + TrimSuffixes: "&|;", } - packages = append(packages, getExtractor.ExtractPackages(commands)...) - - return packages + return extractor.ExtractPackages(commands) } // generateGoMod creates or updates go.mod with dependencies diff --git a/pkg/workflow/dependabot_test.go b/pkg/workflow/dependabot_test.go index d34531d8706..3fa8b12d863 100644 --- a/pkg/workflow/dependabot_test.go +++ b/pkg/workflow/dependabot_test.go @@ -863,3 +863,91 @@ go install github.com/user/tool@v1.0.0 t.Error("dependabot.yml should be created") } } + +// Tests for extractGoFromCommands function + +func TestExtractGoFromCommands(t *testing.T) { + tests := []struct { + name string + commands string + want []string + }{ + { + name: "simple go install", + commands: "go install github.com/user/tool@v1.0.0", + want: []string{"github.com/user/tool@v1.0.0"}, + }, + { + name: "go get", + commands: "go get golang.org/x/tools@latest", + want: []string{"golang.org/x/tools@latest"}, + }, + { + name: "mixed go install and go get", + commands: `go install github.com/user/tool@v1.0.0 +go get golang.org/x/lint@latest`, + want: []string{"github.com/user/tool@v1.0.0", "golang.org/x/lint@latest"}, + }, + { + name: "go install with flags", + commands: "go install -v github.com/user/tool", + want: []string{"github.com/user/tool"}, + }, + { + name: "go without install or get", + commands: "go build main.go", + want: nil, + }, + { + name: "go mod command (not extracted)", + commands: "go mod tidy", + want: nil, + }, + { + name: "empty command", + commands: "", + want: nil, + }, + { + name: "go get with flags", + commands: "go get -u github.com/user/tool@latest", + want: []string{"github.com/user/tool@latest"}, + }, + { + name: "multiple go install commands", + commands: `go install github.com/tool1/pkg@v1.0.0 +go install github.com/tool2/pkg@v2.0.0`, + want: []string{"github.com/tool1/pkg@v1.0.0", "github.com/tool2/pkg@v2.0.0"}, + }, + { + name: "go install with trailing semicolon", + commands: "go install github.com/user/tool@v1.0.0;", + want: []string{"github.com/user/tool@v1.0.0"}, + }, + { + name: "go get with trailing ampersand", + commands: "go get github.com/user/tool@latest&", + want: []string{"github.com/user/tool@latest"}, + }, + { + name: "go install and go get on same line", + commands: "go install github.com/tool1/pkg@v1.0.0 && go get github.com/tool2/pkg@latest", + want: []string{"github.com/tool1/pkg@v1.0.0", "github.com/tool2/pkg@latest"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := extractGoFromCommands(tt.commands) + if len(got) != len(tt.want) { + t.Errorf("extractGoFromCommands() = %v, want %v", got, tt.want) + return + } + for i, v := range got { + if v != tt.want[i] { + t.Errorf("extractGoFromCommands()[%d] = %v, want %v", i, v, tt.want[i]) + } + } + }) + } +} diff --git a/pkg/workflow/package_extraction.go b/pkg/workflow/package_extraction.go index 6eeb5ae8d6f..334b231cfd1 100644 --- a/pkg/workflow/package_extraction.go +++ b/pkg/workflow/package_extraction.go @@ -50,14 +50,14 @@ // packages := extractor.ExtractPackages("pip install requests==2.28.0") // // Returns: []string{"requests==2.28.0"} // -// Go: +// Go (with multiple subcommands): // -// installExtractor := PackageExtractor{ -// CommandNames: []string{"go"}, -// RequiredSubcommand: "install", -// TrimSuffixes: "&|;", +// extractor := PackageExtractor{ +// CommandNames: []string{"go"}, +// RequiredSubcommands: []string{"install", "get"}, +// TrimSuffixes: "&|;", // } -// packages := installExtractor.ExtractPackages("go install github.com/user/tool@v1.0.0") +// packages := extractor.ExtractPackages("go install github.com/user/tool@v1.0.0") // // Returns: []string{"github.com/user/tool@v1.0.0"} // // Python (uv): @@ -151,8 +151,24 @@ type PackageExtractor struct { // - "install" for pip (pip install ) // - "get" for go (go get ) // - "" for npx (npx ) + // + // Deprecated: Use RequiredSubcommands for multiple subcommand support. + // This field is maintained for backward compatibility. RequiredSubcommand string + // RequiredSubcommands is a list of subcommands that can follow the command name + // before the package name appears. If any of these subcommands is found, the + // package name following it will be extracted. Set to empty slice if the package + // name comes directly after the command. + // + // This field takes precedence over RequiredSubcommand if both are set. + // + // Examples: + // - ["install"] for pip (pip install ) + // - ["install", "get"] for go (go install or go get ) + // - [] for npx (npx ) + RequiredSubcommands []string + // TrimSuffixes is a string of characters to trim from the end of package names. // This is useful for removing shell operators that may appear after package names // in command strings. @@ -218,9 +234,10 @@ type PackageExtractor struct { // packages := extractor.ExtractPackages("npx @playwright/mcp@latest") // // Returns: []string{"@playwright/mcp@latest"} func (pe *PackageExtractor) ExtractPackages(commands string) []string { + subcommands := pe.getRequiredSubcommands() if pkgLog.Enabled() { - pkgLog.Printf("Extracting packages from commands using %v (subcommand: %q)", - pe.CommandNames, pe.RequiredSubcommand) + pkgLog.Printf("Extracting packages from commands using %v (subcommands: %v)", + pe.CommandNames, subcommands) } var packages []string @@ -234,9 +251,9 @@ func (pe *PackageExtractor) ExtractPackages(commands string) []string { continue } - // If we have a required subcommand, find it first - if pe.RequiredSubcommand != "" { - pkg := pe.extractWithSubcommand(words, i) + // If we have required subcommands, find any of them first + if len(subcommands) > 0 { + pkg := pe.extractWithSubcommands(words, i, subcommands) if pkg != "" { pkgLog.Printf("Extracted package with subcommand: %s", pkg) packages = append(packages, pkg) @@ -256,6 +273,18 @@ func (pe *PackageExtractor) ExtractPackages(commands string) []string { return packages } +// getRequiredSubcommands returns the list of required subcommands. +// It prefers RequiredSubcommands if set, otherwise falls back to RequiredSubcommand. +func (pe *PackageExtractor) getRequiredSubcommands() []string { + if len(pe.RequiredSubcommands) > 0 { + return pe.RequiredSubcommands + } + if pe.RequiredSubcommand != "" { + return []string{pe.RequiredSubcommand} + } + return nil +} + // isCommandName checks if the given word matches any of the configured command names func (pe *PackageExtractor) isCommandName(word string) bool { for _, cmdName := range pe.CommandNames { @@ -266,14 +295,16 @@ func (pe *PackageExtractor) isCommandName(word string) bool { return false } -// extractWithSubcommand extracts a package name when a required subcommand must be present -// (e.g., "pip install ") -func (pe *PackageExtractor) extractWithSubcommand(words []string, commandIndex int) string { - // Look for the required subcommand after the command name +// extractWithSubcommands extracts a package name when any of the required subcommands must be present +// (e.g., "pip install " or "go get ") +func (pe *PackageExtractor) extractWithSubcommands(words []string, commandIndex int, subcommands []string) string { + // Look for any of the required subcommands after the command name for j := commandIndex + 1; j < len(words); j++ { - if words[j] == pe.RequiredSubcommand { - // Found the subcommand - now find the package name - return pe.findPackageName(words, j+1) + for _, subcommand := range subcommands { + if words[j] == subcommand { + // Found a matching subcommand - now find the package name + return pe.findPackageName(words, j+1) + } } } return "" diff --git a/pkg/workflow/package_extraction_test.go b/pkg/workflow/package_extraction_test.go index f41763654ea..39d591bd2e1 100644 --- a/pkg/workflow/package_extraction_test.go +++ b/pkg/workflow/package_extraction_test.go @@ -205,6 +205,131 @@ func TestPackageExtractor_ExtractPackages_GoPattern(t *testing.T) { } } +func TestPackageExtractor_ExtractPackages_MultipleSubcommands(t *testing.T) { + // Test extraction with multiple required subcommands (e.g., "go install" and "go get") + extractor := PackageExtractor{ + CommandNames: []string{"go"}, + RequiredSubcommands: []string{"install", "get"}, + TrimSuffixes: "&|;", + } + + tests := []struct { + name string + commands string + want []string + }{ + { + name: "go install with multiple subcommands", + commands: "go install github.com/user/tool@v1.0.0", + want: []string{"github.com/user/tool@v1.0.0"}, + }, + { + name: "go get with multiple subcommands", + commands: "go get golang.org/x/tools@latest", + want: []string{"golang.org/x/tools@latest"}, + }, + { + name: "mixed go install and go get", + commands: `go install github.com/user/tool@v1.0.0 +go get golang.org/x/lint@latest`, + want: []string{"github.com/user/tool@v1.0.0", "golang.org/x/lint@latest"}, + }, + { + name: "go install with flags", + commands: "go install -v github.com/user/tool", + want: []string{"github.com/user/tool"}, + }, + { + name: "go without install or get", + commands: "go build main.go", + want: nil, + }, + { + name: "go mod command (not extracted)", + commands: "go mod tidy", + want: nil, + }, + { + name: "empty command", + commands: "", + want: nil, + }, + { + name: "go get with flags", + commands: "go get -u github.com/user/tool@latest", + want: []string{"github.com/user/tool@latest"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := extractor.ExtractPackages(tt.commands) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ExtractPackages() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPackageExtractor_getRequiredSubcommands(t *testing.T) { + tests := []struct { + name string + extractor PackageExtractor + want []string + }{ + { + name: "only RequiredSubcommand set", + extractor: PackageExtractor{ + RequiredSubcommand: "install", + }, + want: []string{"install"}, + }, + { + name: "only RequiredSubcommands set", + extractor: PackageExtractor{ + RequiredSubcommands: []string{"install", "get"}, + }, + want: []string{"install", "get"}, + }, + { + name: "both fields set - RequiredSubcommands takes precedence", + extractor: PackageExtractor{ + RequiredSubcommand: "deprecated", + RequiredSubcommands: []string{"install", "get"}, + }, + want: []string{"install", "get"}, + }, + { + name: "neither field set", + extractor: PackageExtractor{}, + want: nil, + }, + { + name: "empty RequiredSubcommand", + extractor: PackageExtractor{ + RequiredSubcommand: "", + }, + want: nil, + }, + { + name: "empty RequiredSubcommands slice", + extractor: PackageExtractor{ + RequiredSubcommands: []string{}, + }, + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.extractor.getRequiredSubcommands() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getRequiredSubcommands() = %v, want %v", got, tt.want) + } + }) + } +} + func TestPackageExtractor_isCommandName(t *testing.T) { extractor := PackageExtractor{ CommandNames: []string{"pip", "pip3"},