diff --git a/docs/src/content/docs/status.mdx b/docs/src/content/docs/status.mdx index bfc7afc9d0..1d9611903d 100644 --- a/docs/src/content/docs/status.mdx +++ b/docs/src/content/docs/status.mdx @@ -29,9 +29,11 @@ Status of all agentic workflows. [Browse source files](https://github.com/github | [Daily News](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/daily-news.md) | copilot | [![Daily News](https://github.com/githubnext/gh-aw/actions/workflows/daily-news.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/daily-news.lock.yml) | `0 9 * * 1-5` | - | | [Daily Perf Improver](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/daily-perf-improver.md) | copilot | [![Daily Perf Improver](https://github.com/githubnext/gh-aw/actions/workflows/daily-perf-improver.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/daily-perf-improver.lock.yml) | `0 2 * * 1-5` | - | | [Daily Test Coverage Improver](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/daily-test-improver.md) | copilot | [![Daily Test Coverage Improver](https://github.com/githubnext/gh-aw/actions/workflows/daily-test-improver.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/daily-test-improver.lock.yml) | `0 2 * * 1-5` | - | +| [Dependabot Go Module Dependency Checker](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/dependabot-go-checker.md) | copilot | [![Dependabot Go Module 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) | copilot | [![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 Firewall](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/dev.firewall.md) | copilot | [![Dev Firewall](https://github.com/githubnext/gh-aw/actions/workflows/dev.firewall.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/dev.firewall.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` | - | | [Documentation Unbloat](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/unbloat-docs.md) | claude | [![Documentation Unbloat](https://github.com/githubnext/gh-aw/actions/workflows/unbloat-docs.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/unbloat-docs.lock.yml) | `0 22 * * *` | `/unbloat` | | [Duplicate Code Detector](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/duplicate-code-detector.md) | codex | [![Duplicate Code Detector](https://github.com/githubnext/gh-aw/actions/workflows/duplicate-code-detector.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/duplicate-code-detector.lock.yml) | `0 21 * * *` | - | @@ -49,6 +51,7 @@ Status of all agentic workflows. [Browse source files](https://github.com/github | [Mergefest](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/mergefest.md) | copilot | [![Mergefest](https://github.com/githubnext/gh-aw/actions/workflows/mergefest.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/mergefest.lock.yml) | - | `/mergefest` | | [Plan Command](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/plan.md) | copilot | [![Plan Command](https://github.com/githubnext/gh-aw/actions/workflows/plan.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/plan.lock.yml) | - | `/plan` | | [Poem Bot - A Creative Agentic Workflow](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/poem-bot.md) | copilot | [![Poem Bot - A Creative Agentic Workflow](https://github.com/githubnext/gh-aw/actions/workflows/poem-bot.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/poem-bot.lock.yml) | - | `/poem` | +| [PR Nitpick Reviewer 🔍](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/pr-nitpick-reviewer.md) | copilot | [![PR Nitpick Reviewer 🔍](https://github.com/githubnext/gh-aw/actions/workflows/pr-nitpick-reviewer.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/pr-nitpick-reviewer.lock.yml) | - | - | | [Python Data Visualization Generator](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/python-data-charts.md) | copilot | [![Python Data Visualization Generator](https://github.com/githubnext/gh-aw/actions/workflows/python-data-charts.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/python-data-charts.lock.yml) | - | - | | [Q](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/q.md) | copilot | [![Q](https://github.com/githubnext/gh-aw/actions/workflows/q.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/q.lock.yml) | - | `/q` | | [Repository Tree Map Generator](https://github.com/githubnext/gh-aw/blob/main/.github/workflows/repo-tree-map.md) | copilot | [![Repository Tree Map Generator](https://github.com/githubnext/gh-aw/actions/workflows/repo-tree-map.lock.yml/badge.svg)](https://github.com/githubnext/gh-aw/actions/workflows/repo-tree-map.lock.yml) | `0 15 * * 1` | - | diff --git a/pkg/cli/add_command.go b/pkg/cli/add_command.go index 5e9968d34d..326386a763 100644 --- a/pkg/cli/add_command.go +++ b/pkg/cli/add_command.go @@ -32,12 +32,15 @@ Examples: ` + constants.CLIExtensionPrefix + ` add githubnext/agentics/workflows/ci-doctor.md@main ` + constants.CLIExtensionPrefix + ` add https://github.com/githubnext/agentics/blob/main/workflows/ci-doctor.md ` + constants.CLIExtensionPrefix + ` add githubnext/agentics/ci-doctor --pr --force + ` + constants.CLIExtensionPrefix + ` add githubnext/agentics/* + ` + constants.CLIExtensionPrefix + ` add githubnext/agentics/*@v1.0.0 Workflow specifications: - Two parts: "owner/repo[@version]" (lists available workflows in the repository) - Three parts: "owner/repo/workflow-name[@version]" (implicitly looks in workflows/ directory) - Four+ parts: "owner/repo/workflows/workflow-name.md[@version]" (requires explicit .md extension) - GitHub URL: "https://github.com/owner/repo/blob/branch/path/to/workflow.md" + - Wildcard: "owner/repo/*[@version]" (adds all workflows from the repository) - Version can be tag, branch, or SHA The -n flag allows you to specify a custom name for the workflow file (only applies to the first workflow when adding multiple). @@ -192,6 +195,13 @@ func AddWorkflows(workflows []string, number int, verbose bool, engineOverride s } } + // Expand wildcards after installation + var err error + processedWorkflows, err = expandWildcardWorkflows(processedWorkflows, verbose) + if err != nil { + return err + } + // Handle PR creation workflow if createPR { addLog.Print("Creating workflow with PR") @@ -893,3 +903,41 @@ func addSourceToWorkflow(content, source string) (string, error) { // Use shared frontmatter logic that preserves formatting return addFieldToFrontmatter(content, "source", source) } + +// expandWildcardWorkflows expands wildcard workflow specifications into individual workflow specs. +// For each wildcard spec, it discovers all workflows in the installed package and replaces +// the wildcard with the discovered workflows. Non-wildcard specs are passed through unchanged. +func expandWildcardWorkflows(specs []*WorkflowSpec, verbose bool) ([]*WorkflowSpec, error) { + expandedWorkflows := []*WorkflowSpec{} + + for _, spec := range specs { + if spec.IsWildcard { + addLog.Printf("Expanding wildcard for repository: %s", spec.RepoSlug) + if verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Discovering workflows in %s...", spec.RepoSlug))) + } + + discovered, err := discoverWorkflowsInPackage(spec.RepoSlug, spec.Version, verbose) + if err != nil { + return nil, fmt.Errorf("failed to discover workflows in %s: %w", spec.RepoSlug, err) + } + + if len(discovered) == 0 { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("No workflows found in %s", spec.RepoSlug))) + } else { + if verbose { + fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Found %d workflow(s) in %s", len(discovered), spec.RepoSlug))) + } + expandedWorkflows = append(expandedWorkflows, discovered...) + } + } else { + expandedWorkflows = append(expandedWorkflows, spec) + } + } + + if len(expandedWorkflows) == 0 { + return nil, fmt.Errorf("no workflows to add after expansion") + } + + return expandedWorkflows, nil +} diff --git a/pkg/cli/add_wildcard_test.go b/pkg/cli/add_wildcard_test.go new file mode 100644 index 0000000000..efdc6949ed --- /dev/null +++ b/pkg/cli/add_wildcard_test.go @@ -0,0 +1,404 @@ +package cli + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// TestParseWorkflowSpecWithWildcard tests parsing workflow specs with wildcards +func TestParseWorkflowSpecWithWildcard(t *testing.T) { + tests := []struct { + name string + spec string + expectWildcard bool + expectError bool + expectedRepo string + expectedVer string + }{ + { + name: "wildcard_without_version", + spec: "githubnext/agentics/*", + expectWildcard: true, + expectError: false, + expectedRepo: "githubnext/agentics", + expectedVer: "", + }, + { + name: "wildcard_with_version", + spec: "githubnext/agentics/*@v1.0.0", + expectWildcard: true, + expectError: false, + expectedRepo: "githubnext/agentics", + expectedVer: "v1.0.0", + }, + { + name: "wildcard_with_branch", + spec: "owner/repo/*@main", + expectWildcard: true, + expectError: false, + expectedRepo: "owner/repo", + expectedVer: "main", + }, + { + name: "non_wildcard_spec", + spec: "githubnext/agentics/workflow-name", + expectWildcard: false, + expectError: false, + expectedRepo: "githubnext/agentics", + expectedVer: "", + }, + { + name: "invalid_spec_too_few_parts", + spec: "owner/*", + expectWildcard: false, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := parseWorkflowSpec(tt.spec) + + if tt.expectError { + if err == nil { + t.Errorf("parseWorkflowSpec() expected error for spec '%s', got nil", tt.spec) + } + return + } + + if err != nil { + t.Errorf("parseWorkflowSpec() unexpected error: %v", err) + return + } + + if result.IsWildcard != tt.expectWildcard { + t.Errorf("parseWorkflowSpec() IsWildcard = %v, expected %v", result.IsWildcard, tt.expectWildcard) + } + + if tt.expectWildcard { + if result.WorkflowPath != "*" { + t.Errorf("parseWorkflowSpec() WorkflowPath = %v, expected '*'", result.WorkflowPath) + } + if result.WorkflowName != "*" { + t.Errorf("parseWorkflowSpec() WorkflowName = %v, expected '*'", result.WorkflowName) + } + } + + if result.RepoSlug != tt.expectedRepo { + t.Errorf("parseWorkflowSpec() RepoSlug = %v, expected %v", result.RepoSlug, tt.expectedRepo) + } + + if result.Version != tt.expectedVer { + t.Errorf("parseWorkflowSpec() Version = %v, expected %v", result.Version, tt.expectedVer) + } + }) + } +} + +// TestDiscoverWorkflowsInPackage tests discovering workflows in an installed package +func TestDiscoverWorkflowsInPackage(t *testing.T) { + // Create a temporary packages directory structure + tempDir := t.TempDir() + + // Override packages directory for testing + oldHome := os.Getenv("HOME") + os.Setenv("HOME", tempDir) + defer os.Setenv("HOME", oldHome) + + // Create a mock package structure (use .aw/packages, not .gh-aw/packages) + packagePath := filepath.Join(tempDir, ".aw", "packages", "test-owner", "test-repo") + workflowsDir := filepath.Join(packagePath, "workflows") + if err := os.MkdirAll(workflowsDir, 0755); err != nil { + t.Fatalf("Failed to create test directories: %v", err) + } + + // Create some mock workflow files + workflows := []string{ + "workflow1.md", + "workflow2.md", + "nested/workflow3.md", + } + + for _, wf := range workflows { + filePath := filepath.Join(packagePath, wf) + dir := filepath.Dir(filePath) + if err := os.MkdirAll(dir, 0755); err != nil { + t.Fatalf("Failed to create directory %s: %v", dir, err) + } + if err := os.WriteFile(filePath, []byte("# Test Workflow"), 0644); err != nil { + t.Fatalf("Failed to create test workflow %s: %v", wf, err) + } + } + + // Test discovery + discovered, err := discoverWorkflowsInPackage("test-owner/test-repo", "", false) + if err != nil { + t.Fatalf("discoverWorkflowsInPackage() error = %v", err) + } + + if len(discovered) != len(workflows) { + t.Errorf("discoverWorkflowsInPackage() found %d workflows, expected %d", len(discovered), len(workflows)) + } + + // Verify discovered workflow paths + discoveredPaths := make(map[string]bool) + for _, spec := range discovered { + discoveredPaths[spec.WorkflowPath] = true + } + + for _, expectedPath := range workflows { + if !discoveredPaths[expectedPath] { + t.Errorf("Expected workflow %s not found in discovered workflows", expectedPath) + } + } + + // Verify all specs have correct repo info + for _, spec := range discovered { + if spec.RepoSlug != "test-owner/test-repo" { + t.Errorf("Workflow spec has incorrect RepoSlug: %s, expected test-owner/test-repo", spec.RepoSlug) + } + if spec.IsWildcard { + t.Errorf("Discovered workflow spec should not be marked as wildcard") + } + } +} + +// TestDiscoverWorkflowsInPackage_NotFound tests behavior when package is not found +func TestDiscoverWorkflowsInPackage_NotFound(t *testing.T) { + // Create a temporary packages directory + tempDir := t.TempDir() + + // Override packages directory for testing + oldHome := os.Getenv("HOME") + os.Setenv("HOME", tempDir) + defer os.Setenv("HOME", oldHome) + + // Try to discover workflows in a non-existent package + _, err := discoverWorkflowsInPackage("nonexistent/repo", "", false) + if err == nil { + t.Error("discoverWorkflowsInPackage() expected error for non-existent package, got nil") + } + + if !strings.Contains(err.Error(), "package not found") { + t.Errorf("discoverWorkflowsInPackage() error should mention 'package not found', got: %v", err) + } +} + +// TestDiscoverWorkflowsInPackage_EmptyPackage tests behavior with empty package +func TestDiscoverWorkflowsInPackage_EmptyPackage(t *testing.T) { + // Create a temporary packages directory + tempDir := t.TempDir() + + // Override packages directory for testing + oldHome := os.Getenv("HOME") + os.Setenv("HOME", tempDir) + defer os.Setenv("HOME", oldHome) + + // Create an empty package directory (use .aw/packages, not .gh-aw/packages) + packagePath := filepath.Join(tempDir, ".aw", "packages", "empty-owner", "empty-repo") + if err := os.MkdirAll(packagePath, 0755); err != nil { + t.Fatalf("Failed to create test directory: %v", err) + } + + // Test discovery + discovered, err := discoverWorkflowsInPackage("empty-owner/empty-repo", "", false) + if err != nil { + t.Fatalf("discoverWorkflowsInPackage() error = %v", err) + } + + if len(discovered) != 0 { + t.Errorf("discoverWorkflowsInPackage() found %d workflows in empty package, expected 0", len(discovered)) + } +} + +// TestExpandWildcardWorkflows tests expanding wildcard workflow specifications +func TestExpandWildcardWorkflows(t *testing.T) { + // Create a temporary packages directory structure + tempDir := t.TempDir() + + // Override packages directory for testing + oldHome := os.Getenv("HOME") + os.Setenv("HOME", tempDir) + defer os.Setenv("HOME", oldHome) + + // Create a mock package with workflows + packagePath := filepath.Join(tempDir, ".aw", "packages", "test-org", "test-repo") + workflowsDir := filepath.Join(packagePath, "workflows") + if err := os.MkdirAll(workflowsDir, 0755); err != nil { + t.Fatalf("Failed to create test directories: %v", err) + } + + // Create mock workflow files + workflows := []string{ + "workflows/workflow1.md", + "workflows/workflow2.md", + } + + for _, wf := range workflows { + filePath := filepath.Join(packagePath, wf) + if err := os.WriteFile(filePath, []byte("# Test Workflow"), 0644); err != nil { + t.Fatalf("Failed to create test workflow %s: %v", wf, err) + } + } + + tests := []struct { + name string + specs []*WorkflowSpec + expectedCount int + expectError bool + errorContains string + }{ + { + name: "expand_single_wildcard", + specs: []*WorkflowSpec{ + { + RepoSpec: RepoSpec{ + RepoSlug: "test-org/test-repo", + Version: "", + }, + WorkflowPath: "*", + WorkflowName: "*", + IsWildcard: true, + }, + }, + expectedCount: 2, + expectError: false, + }, + { + name: "mixed_wildcard_and_specific", + specs: []*WorkflowSpec{ + { + RepoSpec: RepoSpec{ + RepoSlug: "test-org/test-repo", + Version: "", + }, + WorkflowPath: "*", + WorkflowName: "*", + IsWildcard: true, + }, + { + RepoSpec: RepoSpec{ + RepoSlug: "other-org/other-repo", + Version: "", + }, + WorkflowPath: "workflows/specific.md", + WorkflowName: "specific", + IsWildcard: false, + }, + }, + expectedCount: 3, // 2 from wildcard + 1 specific + expectError: false, + }, + { + name: "no_wildcard_specs", + specs: []*WorkflowSpec{ + { + RepoSpec: RepoSpec{ + RepoSlug: "other-org/other-repo", + Version: "", + }, + WorkflowPath: "workflows/specific.md", + WorkflowName: "specific", + IsWildcard: false, + }, + }, + expectedCount: 1, + expectError: false, + }, + { + name: "empty_input", + specs: []*WorkflowSpec{}, + expectedCount: 0, + expectError: true, + errorContains: "no workflows to add after expansion", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := expandWildcardWorkflows(tt.specs, false) + + if tt.expectError { + if err == nil { + t.Errorf("expandWildcardWorkflows() expected error, got nil") + return + } + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("expandWildcardWorkflows() error should contain '%s', got: %v", tt.errorContains, err) + } + return + } + + if err != nil { + t.Errorf("expandWildcardWorkflows() unexpected error: %v", err) + return + } + + if len(result) != tt.expectedCount { + t.Errorf("expandWildcardWorkflows() returned %d workflows, expected %d", len(result), tt.expectedCount) + } + + // Verify no wildcard specs remain in result + for _, spec := range result { + if spec.IsWildcard { + t.Errorf("expandWildcardWorkflows() result contains wildcard spec: %v", spec) + } + } + }) + } +} + +// TestExpandWildcardWorkflows_ErrorHandling tests error cases for wildcard expansion +func TestExpandWildcardWorkflows_ErrorHandling(t *testing.T) { + // Create a temporary packages directory + tempDir := t.TempDir() + + // Override packages directory for testing + oldHome := os.Getenv("HOME") + os.Setenv("HOME", tempDir) + defer os.Setenv("HOME", oldHome) + + tests := []struct { + name string + specs []*WorkflowSpec + expectError bool + errorContains string + }{ + { + name: "nonexistent_package", + specs: []*WorkflowSpec{ + { + RepoSpec: RepoSpec{ + RepoSlug: "nonexistent/repo", + Version: "", + }, + WorkflowPath: "*", + WorkflowName: "*", + IsWildcard: true, + }, + }, + expectError: true, + errorContains: "failed to discover workflows", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := expandWildcardWorkflows(tt.specs, false) + + if tt.expectError { + if err == nil { + t.Errorf("expandWildcardWorkflows() expected error, got nil") + return + } + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("expandWildcardWorkflows() error should contain '%s', got: %v", tt.errorContains, err) + } + } else if err != nil { + t.Errorf("expandWildcardWorkflows() unexpected error: %v", err) + } + }) + } +} diff --git a/pkg/cli/packages.go b/pkg/cli/packages.go index 08e3333338..c1c5b19999 100644 --- a/pkg/cli/packages.go +++ b/pkg/cli/packages.go @@ -577,3 +577,70 @@ type IncludeDependency struct { TargetPath string // Relative path where it should be copied in .github/workflows IsOptional bool // Whether this is an optional include (@include?) } + +// discoverWorkflowsInPackage discovers all workflow files in an installed package +// Returns a list of WorkflowSpec for each discovered workflow +func discoverWorkflowsInPackage(repoSlug, version string, verbose bool) ([]*WorkflowSpec, error) { + packagesLog.Printf("Discovering workflows in package: %s (version: %s)", repoSlug, version) + + packagesDir, err := getPackagesDir() + if err != nil { + return nil, fmt.Errorf("failed to get packages directory: %w", err) + } + + packagePath := filepath.Join(packagesDir, repoSlug) + if _, err := os.Stat(packagePath); os.IsNotExist(err) { + return nil, fmt.Errorf("package not found: %s (try installing it first)", repoSlug) + } + + var workflows []*WorkflowSpec + + // Walk through the package directory and find all .md files + err = filepath.Walk(packagePath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Skip if not a markdown file + if info.IsDir() || !strings.HasSuffix(info.Name(), ".md") { + return nil + } + + // Skip common documentation markdown files + switch strings.ToLower(info.Name()) { + case "readme.md", "license.md", "contributing.md", "code_of_conduct.md", "security.md": + return nil + } + + // Get relative path from package root + relPath, err := filepath.Rel(packagePath, path) + if err != nil { + return fmt.Errorf("failed to get relative path: %w", err) + } + + // Create workflow spec + spec := &WorkflowSpec{ + RepoSpec: RepoSpec{ + RepoSlug: repoSlug, + Version: version, + }, + WorkflowPath: relPath, + WorkflowName: strings.TrimSuffix(filepath.Base(relPath), ".md"), + } + + workflows = append(workflows, spec) + + if verbose { + fmt.Fprintf(os.Stderr, "Discovered workflow: %s\n", spec.String()) + } + + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to walk package directory: %w", err) + } + + packagesLog.Printf("Discovered %d workflows in package %s", len(workflows), repoSlug) + return workflows, nil +} diff --git a/pkg/cli/spec.go b/pkg/cli/spec.go index 1b73efefe3..f9d3840e70 100644 --- a/pkg/cli/spec.go +++ b/pkg/cli/spec.go @@ -27,6 +27,7 @@ type WorkflowSpec struct { RepoSpec // embedded RepoSpec for Repo and Version fields WorkflowPath string // e.g., "workflows/workflow-name.md" WorkflowName string // e.g., "workflow-name" + IsWildcard bool // true if this is a wildcard spec (e.g., "owner/repo/*") } // String returns the canonical string representation of the workflow spec @@ -224,6 +225,19 @@ func parseWorkflowSpec(spec string) (*WorkflowSpec, error) { return nil, fmt.Errorf("invalid workflow specification: '%s/%s' does not look like a valid GitHub repository", owner, repo) } + // Check if this is a wildcard specification (owner/repo/*) + if workflowPath == "*" { + return &WorkflowSpec{ + RepoSpec: RepoSpec{ + RepoSlug: fmt.Sprintf("%s/%s", owner, repo), + Version: version, + }, + WorkflowPath: "*", + WorkflowName: "*", + IsWildcard: true, + }, nil + } + // Handle different cases based on the number of path parts if len(slashParts) == 3 && !strings.HasSuffix(workflowPath, ".md") { // Three-part spec: owner/repo/workflow-name