diff --git a/cmd/gh-aw/help_usage_examples_order_test.go b/cmd/gh-aw/help_usage_examples_order_test.go new file mode 100644 index 00000000000..ba25dae334b --- /dev/null +++ b/cmd/gh-aw/help_usage_examples_order_test.go @@ -0,0 +1,35 @@ +//go:build !integration + +package main + +import ( + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestCommandsUseExampleFieldForHelpOrdering(t *testing.T) { + testCases := []struct { + name string + cmd *cobra.Command + }{ + {name: "new", cmd: newCmd}, + {name: "remove", cmd: removeCmd}, + {name: "enable", cmd: enableCmd}, + {name: "disable", cmd: disableCmd}, + {name: "compile", cmd: compileCmd}, + {name: "run", cmd: runCmd}, + {name: "version", cmd: versionCmd}, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + assert.NotEmpty(t, tt.cmd.Example, "command should define examples via Example field") + assert.NotContains(t, tt.cmd.Long, "\nExamples:\n", "command long help should not embed an Examples section") + assert.Contains(t, tt.cmd.Example, "gh aw ", "command examples should contain gh aw invocations") + assert.False(t, strings.HasPrefix(tt.cmd.Example, "Examples:"), "command Example field should contain example lines only") + }) + } +} diff --git a/cmd/gh-aw/main.go b/cmd/gh-aw/main.go index 6e4625c467f..1fe823f5f11 100644 --- a/cmd/gh-aw/main.go +++ b/cmd/gh-aw/main.go @@ -121,10 +121,8 @@ When called with a workflow name, creates a template file with comprehensive exa - Tools configuration (github, claude, MCPs) - All frontmatter options with explanations -` + cli.WorkflowIDExplanation + ` - -Examples: - ` + string(constants.CLIExtensionPrefix) + ` new # Interactive mode +` + cli.WorkflowIDExplanation, + Example: ` ` + string(constants.CLIExtensionPrefix) + ` new # Interactive mode ` + string(constants.CLIExtensionPrefix) + ` new my-workflow # Create template file ` + string(constants.CLIExtensionPrefix) + ` new my-workflow.md # Same as above (.md extension stripped) ` + string(constants.CLIExtensionPrefix) + ` new my-workflow --force # Overwrite if exists @@ -173,10 +171,8 @@ The workflow-id is the basename of the Markdown file without the .md extension. You can provide a substring to match multiple workflows, or a specific workflow-id. By default, this command also removes orphaned include files that are no longer referenced -by any workflow. Use --keep-orphans to skip this cleanup. - -Examples: - ` + string(constants.CLIExtensionPrefix) + ` remove my-workflow # Remove specific workflow +by any workflow. Use --keep-orphans to skip this cleanup.`, + Example: ` ` + string(constants.CLIExtensionPrefix) + ` remove my-workflow # Remove specific workflow ` + string(constants.CLIExtensionPrefix) + ` remove test- # Remove all workflows containing 'test-' in name ` + string(constants.CLIExtensionPrefix) + ` remove old- --keep-orphans # Remove workflows but keep orphaned includes ` + string(constants.CLIExtensionPrefix) + ` remove my-workflow --dir .github/workflows/shared # Remove from custom directory`, @@ -196,10 +192,8 @@ var enableCmd = &cobra.Command{ Short: "Enable agentic workflows", Long: `Enable one or more workflows by ID, or all workflows if no IDs are provided. -` + cli.WorkflowIDExplanation + ` - -Examples: - ` + string(constants.CLIExtensionPrefix) + ` enable # Enable all workflows +` + cli.WorkflowIDExplanation, + Example: ` ` + string(constants.CLIExtensionPrefix) + ` enable # Enable all workflows ` + string(constants.CLIExtensionPrefix) + ` enable ci-doctor # Enable specific workflow ` + string(constants.CLIExtensionPrefix) + ` enable ci-doctor.md # Enable specific workflow (alternative format) ` + string(constants.CLIExtensionPrefix) + ` enable ci-doctor daily # Enable multiple workflows @@ -217,10 +211,8 @@ var disableCmd = &cobra.Command{ Any in-progress runs will be cancelled before disabling. -` + cli.WorkflowIDExplanation + ` - -Examples: - ` + string(constants.CLIExtensionPrefix) + ` disable # Disable all workflows +` + cli.WorkflowIDExplanation, + Example: ` ` + string(constants.CLIExtensionPrefix) + ` disable # Disable all workflows ` + string(constants.CLIExtensionPrefix) + ` disable ci-doctor # Disable specific workflow ` + string(constants.CLIExtensionPrefix) + ` disable ci-doctor.md # Disable specific workflow (alternative format) ` + string(constants.CLIExtensionPrefix) + ` disable ci-doctor daily # Disable multiple workflows @@ -275,9 +267,8 @@ Three flags govern this. --gh-aw-ref is mutually exclusive with the other two; Branch and tag names are resolved via the GitHub API. Cannot be combined with --action-tag or --action-mode. Use this when E2E-testing compiled workflows against a specific gh-aw revision. - -Examples: - ` + string(constants.CLIExtensionPrefix) + ` compile # Compile all Markdown files +`, + Example: ` ` + string(constants.CLIExtensionPrefix) + ` compile # Compile all Markdown files ` + string(constants.CLIExtensionPrefix) + ` compile ci-doctor # Compile a specific workflow ` + string(constants.CLIExtensionPrefix) + ` compile ci-doctor daily-plan # Compile multiple workflows ` + string(constants.CLIExtensionPrefix) + ` compile workflow.md # Compile by file path @@ -428,10 +419,8 @@ The workflows must have been compiled into GitHub Actions YAML files. This command only works with workflows that have workflow_dispatch triggers. -` + cli.WorkflowIDExplanation + ` - -Examples: - gh aw run # Interactive mode +` + cli.WorkflowIDExplanation, + Example: ` gh aw run # Interactive mode gh aw run daily-perf-improver gh aw run daily-perf-improver.md # Alternative format gh aw run daily-perf-improver --ref main # Run on specific branch @@ -502,9 +491,8 @@ var versionCmd = &cobra.Command{ Use: "version", Short: "Print the current version", Long: `Show the installed version of the gh aw extension. - -Examples: - ` + string(constants.CLIExtensionPrefix) + ` version # Print the current version`, +`, + Example: ` ` + string(constants.CLIExtensionPrefix) + ` version # Print the current version`, RunE: func(cmd *cobra.Command, args []string) error { fmt.Fprintf(os.Stderr, "%s version %s\n", string(constants.CLIExtensionPrefix), version) return nil diff --git a/docs/src/content/docs/setup/cli.md b/docs/src/content/docs/setup/cli.md index 93aa598ba4f..1c9cb88aa58 100644 --- a/docs/src/content/docs/setup/cli.md +++ b/docs/src/content/docs/setup/cli.md @@ -168,7 +168,7 @@ Add workflows from The Agentics collection or other repositories to `.github/wor ```bash wrap gh aw add githubnext/agentics/ci-doctor # Add single workflow gh aw add githubnext/agentics/ci-doctor@v1.0.0 # Add specific version -gh aw add githubnext/agentics/ci-doctor --dir shared # Organize in subdirectory +gh aw add githubnext/agentics/ci-doctor --dir .github/workflows/shared # Organize in subdirectory gh aw add githubnext/agentics/ci-doctor --create-pull-request # Create PR instead of commit gh aw add https://example.com/workflows/my-workflow.md # Arbitrary HTTPS URL (markdown) gh aw add https://example.com/workflows/my-workflow.json # Arbitrary HTTPS URL (JSON workflow definition) diff --git a/pkg/cli/lint_command.go b/pkg/cli/lint_command.go index 1a138c40858..1cc84d50acc 100644 --- a/pkg/cli/lint_command.go +++ b/pkg/cli/lint_command.go @@ -72,7 +72,7 @@ By default, shellcheck and pyflakes integrations are disabled for generated run }, } - cmd.Flags().StringP("dir", "d", constants.GetWorkflowDir(), "Directory to scan for *.lock.yml files when no arguments are provided") + cmd.Flags().StringP("dir", "d", "", fmt.Sprintf("Workflow directory (default: %s)", constants.GetWorkflowDir())) cmd.Flags().Bool("shellcheck", false, "Enable shellcheck integration in actionlint") cmd.Flags().Bool("pyflakes", false, "Enable pyflakes integration in actionlint") @@ -84,6 +84,9 @@ By default, shellcheck and pyflakes integrations are disabled for generated run func resolveLockFilesForLint(inputs []string, workflowDir string) ([]string, error) { candidates := inputs if len(candidates) == 0 { + if workflowDir == "" { + workflowDir = constants.GetWorkflowDir() + } candidates = []string{workflowDir} } diff --git a/pkg/cli/lint_command_test.go b/pkg/cli/lint_command_test.go index 90c2c36208d..3cccc1b6f4d 100644 --- a/pkg/cli/lint_command_test.go +++ b/pkg/cli/lint_command_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "testing" + "github.com/github/gh-aw/pkg/constants" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -17,7 +18,10 @@ func TestNewLintCommand(t *testing.T) { require.NotNil(t, cmd, "NewLintCommand should return a non-nil command") assert.Equal(t, "lint", cmd.Name(), "Command name should be 'lint'") require.NotNil(t, cmd.Flags().Lookup("dir"), "lint command should have a --dir flag") - assert.Equal(t, "d", cmd.Flags().Lookup("dir").Shorthand, "--dir should have -d shorthand") + dirFlag := cmd.Flags().Lookup("dir") + assert.Equal(t, "d", dirFlag.Shorthand, "--dir should have -d shorthand") + assert.Empty(t, dirFlag.DefValue, "--dir default should be empty so help text controls default formatting") + assert.Equal(t, "Workflow directory (default: "+constants.GetWorkflowDir()+")", dirFlag.Usage, "--dir help text should match standard workflow-dir phrasing") require.NotNil(t, cmd.Flags().Lookup("shellcheck"), "lint command should have a --shellcheck flag") require.NotNil(t, cmd.Flags().Lookup("pyflakes"), "lint command should have a --pyflakes flag") assert.Contains(t, defaultGhAwActionlintIgnorePatterns, `unknown permission scope "copilot-requests"`, @@ -50,6 +54,19 @@ func TestResolveLockFilesForLint(t *testing.T) { assert.Equal(t, []string{lockA, lockB}, files, "should return sorted .lock.yml files only") }) + t.Run("uses default workflow dir when dir is empty", func(t *testing.T) { + cwd := t.TempDir() + t.Chdir(cwd) + defaultWorkflowDir := filepath.Join(cwd, ".github", "workflows") + require.NoError(t, os.MkdirAll(defaultWorkflowDir, 0o755), "should create default workflow directory") + defaultLock := filepath.Join(defaultWorkflowDir, "default.lock.yml") + require.NoError(t, os.WriteFile(defaultLock, []byte("name: default"), 0o644), "should create lock file in default workflow directory") + + files, err := resolveLockFilesForLint(nil, "") + require.NoError(t, err, "should resolve lock files from default workflow dir when --dir is omitted") + assert.Equal(t, []string{defaultLock}, files, "should lint lock files from .github/workflows by default") + }) + t.Run("accepts explicit lock file path", func(t *testing.T) { files, err := resolveLockFilesForLint([]string{lockB}, tempDir) require.NoError(t, err, "should accept explicit lock file")