From 7199a4a44487f6da78521901531a43018fb70183 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:24:00 +0000 Subject: [PATCH 1/2] Initial plan From cd38e68dca8e71d02747988ee34697eb5c009f0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 23:00:03 +0000 Subject: [PATCH 2/2] Fix CLI consistency: add --json to run, --repo to audit, --engine to new, --dir to list Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- cmd/gh-aw/main.go | 20 ++++++++++-- pkg/cli/audit.go | 15 ++++++++- pkg/cli/commands.go | 14 +++++--- pkg/cli/commands_test.go | 2 +- pkg/cli/list_workflows_command.go | 12 ++++++- pkg/cli/run_workflow_execution.go | 53 +++++++++++++++++++++++++++++++ 6 files changed, 105 insertions(+), 11 deletions(-) diff --git a/cmd/gh-aw/main.go b/cmd/gh-aw/main.go index 1ce23131789..9ac47884696 100644 --- a/cmd/gh-aw/main.go +++ b/cmd/gh-aw/main.go @@ -122,12 +122,20 @@ Examples: ` + 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`, + ` + string(constants.CLIExtensionPrefix) + ` new my-workflow --force # Overwrite if exists + ` + string(constants.CLIExtensionPrefix) + ` new my-workflow --engine copilot # Create template with specific engine`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { forceFlag, _ := cmd.Flags().GetBool("force") verbose, _ := cmd.Flags().GetBool("verbose") interactiveFlag, _ := cmd.Flags().GetBool("interactive") + engineOverride, _ := cmd.Flags().GetString("engine") + + if engineOverride != "" { + if err := validateEngine(engineOverride); err != nil { + return err + } + } // If no arguments provided or interactive flag is set, use interactive mode if len(args) == 0 || interactiveFlag { @@ -147,7 +155,7 @@ Examples: // Template mode with workflow name workflowName := args[0] - return cli.NewWorkflow(workflowName, verbose, forceFlag) + return cli.NewWorkflow(workflowName, verbose, forceFlag, engineOverride) }, } @@ -360,7 +368,8 @@ Examples: gh aw run daily-perf-improver --auto-merge-prs # Auto-merge any PRs created during execution gh aw run daily-perf-improver -F name=value -F env=prod # Pass workflow inputs gh aw run daily-perf-improver --push # Commit and push workflow files before running - gh aw run daily-perf-improver --dry-run # Validate without actually running`, + gh aw run daily-perf-improver --dry-run # Validate without actually running + gh aw run daily-perf-improver --json # Output results in JSON format`, Args: cobra.ArbitraryArgs, RunE: func(cmd *cobra.Command, args []string) error { repeatCount, _ := cmd.Flags().GetInt("repeat") @@ -372,6 +381,7 @@ Examples: inputs, _ := cmd.Flags().GetStringArray("raw-field") push, _ := cmd.Flags().GetBool("push") dryRun, _ := cmd.Flags().GetBool("dry-run") + jsonOutput, _ := cmd.Flags().GetBool("json") if err := validateEngine(engineOverride); err != nil { return err @@ -409,6 +419,7 @@ Examples: Inputs: inputs, Verbose: verboseFlag, DryRun: dryRun, + JSON: jsonOutput, }) }, } @@ -623,6 +634,8 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all // Add flags to new command newCmd.Flags().BoolP("force", "f", false, "Overwrite existing files without confirmation") newCmd.Flags().BoolP("interactive", "i", false, "Launch interactive workflow creation wizard") + newCmd.Flags().StringP("engine", "e", "", "Override AI engine (claude, codex, copilot, custom)") + cli.RegisterEngineFlagCompletion(newCmd) // Add AI flag to compile and add commands compileCmd.Flags().StringP("engine", "e", "", "Override AI engine (claude, codex, copilot, custom)") @@ -681,6 +694,7 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all runCmd.Flags().StringArrayP("raw-field", "F", []string{}, "Add a string parameter in key=value format (can be used multiple times)") runCmd.Flags().Bool("push", false, "Commit and push workflow files (including transitive imports) before running") runCmd.Flags().Bool("dry-run", false, "Validate workflow without actually triggering execution on GitHub Actions") + runCmd.Flags().BoolP("json", "j", false, "Output results in JSON format") // Register completions for run command runCmd.ValidArgsFunction = cli.CompleteWorkflowNames cli.RegisterEngineFlagCompletion(runCmd) diff --git a/pkg/cli/audit.go b/pkg/cli/audit.go index f0b8ed277c0..bb294869785 100644 --- a/pkg/cli/audit.go +++ b/pkg/cli/audit.go @@ -59,7 +59,8 @@ Examples: ` + string(constants.CLIExtensionPrefix) + ` audit https://github.example.com/owner/repo/actions/runs/1234567890 # Audit from GitHub Enterprise ` + string(constants.CLIExtensionPrefix) + ` audit 1234567890 -o ./audit-reports # Custom output directory ` + string(constants.CLIExtensionPrefix) + ` audit 1234567890 -v # Verbose output - ` + string(constants.CLIExtensionPrefix) + ` audit 1234567890 --parse # Parse agent logs and firewall logs, generating log.md and firewall.md`, + ` + string(constants.CLIExtensionPrefix) + ` audit 1234567890 --parse # Parse agent logs and firewall logs, generating log.md and firewall.md + ` + string(constants.CLIExtensionPrefix) + ` audit 1234567890 --repo owner/repo # Audit run from a specific repository`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { runIDOrURL := args[0] @@ -75,6 +76,17 @@ Examples: verbose, _ := cmd.Flags().GetBool("verbose") jsonOutput, _ := cmd.Flags().GetBool("json") parse, _ := cmd.Flags().GetBool("parse") + repoFlag, _ := cmd.Flags().GetString("repo") + + // If --repo is provided and owner/repo were not parsed from a URL, apply them + if repoFlag != "" && components.Owner == "" { + parts := strings.SplitN(repoFlag, "/", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return fmt.Errorf("invalid repository format '%s': expected 'owner/repo'", repoFlag) + } + components.Owner = parts[0] + components.Repo = parts[1] + } return AuditWorkflowRun( cmd.Context(), @@ -95,6 +107,7 @@ Examples: // Add flags to audit command addOutputFlag(cmd, defaultLogsOutputDir) addJSONFlag(cmd) + addRepoFlag(cmd) cmd.Flags().Bool("parse", false, "Run JavaScript parsers on agent logs and firewall logs, writing Markdown to log.md and firewall.md") // Register completions for audit command diff --git a/pkg/cli/commands.go b/pkg/cli/commands.go index c6b2f13fa44..21f97b2edb7 100644 --- a/pkg/cli/commands.go +++ b/pkg/cli/commands.go @@ -226,8 +226,8 @@ func resolveWorkflowFileInDir(fileOrWorkflowName string, verbose bool, workflowD } // NewWorkflow creates a new workflow markdown file with template content -func NewWorkflow(workflowName string, verbose bool, force bool) error { - commandsLog.Printf("Creating new workflow: name=%s, force=%v", workflowName, force) +func NewWorkflow(workflowName string, verbose bool, force bool, engine string) error { + commandsLog.Printf("Creating new workflow: name=%s, force=%v, engine=%s", workflowName, force, engine) // Normalize the workflow name by removing .md extension if present // This ensures consistent behavior whether user provides "my-workflow" or "my-workflow.md" @@ -277,7 +277,7 @@ func NewWorkflow(workflowName string, verbose bool, force bool) error { } // Create the template content - template := createWorkflowTemplate(workflowName) + template := createWorkflowTemplate(workflowName, engine) // Write the template to file with restrictive permissions (owner-only) if err := os.WriteFile(destFile, []byte(template), 0600); err != nil { @@ -291,7 +291,11 @@ func NewWorkflow(workflowName string, verbose bool, force bool) error { } // createWorkflowTemplate generates a concise workflow template with essential options -func createWorkflowTemplate(workflowName string) string { +func createWorkflowTemplate(workflowName string, engine string) string { + engineLine := "" + if engine != "" { + engineLine = "\n# AI engine to use for this workflow\nengine: " + engine + "\n" + } return `--- # Trigger - when should this workflow run? on: @@ -313,7 +317,7 @@ permissions: contents: read issues: read pull-requests: read - +` + engineLine + ` # Tools - GitHub API access via toolsets (context, repos, issues, pull_requests) # tools: # github: diff --git a/pkg/cli/commands_test.go b/pkg/cli/commands_test.go index f1d1e3174d1..6c529b490dd 100644 --- a/pkg/cli/commands_test.go +++ b/pkg/cli/commands_test.go @@ -468,7 +468,7 @@ func TestNewWorkflow(t *testing.T) { } // Run the function - err := NewWorkflow(test.workflowName, false, test.force) + err := NewWorkflow(test.workflowName, false, test.force, "") // Check error expectation if test.expectedError && err == nil { diff --git a/pkg/cli/list_workflows_command.go b/pkg/cli/list_workflows_command.go index 428c68cb522..e5684796bcd 100644 --- a/pkg/cli/list_workflows_command.go +++ b/pkg/cli/list_workflows_command.go @@ -41,6 +41,7 @@ Examples: ` + string(constants.CLIExtensionPrefix) + ` list # List all workflows in current repo ` + string(constants.CLIExtensionPrefix) + ` list --repo github/gh-aw # List workflows from github/gh-aw repo ` + string(constants.CLIExtensionPrefix) + ` list --repo org/repo --path workflows # List from custom path + ` + string(constants.CLIExtensionPrefix) + ` list --dir custom/workflows # List from custom local directory ` + string(constants.CLIExtensionPrefix) + ` list ci- # List workflows with 'ci-' in name ` + string(constants.CLIExtensionPrefix) + ` list --repo github/gh-aw ci- # List workflows from github/gh-aw with 'ci-' in name ` + string(constants.CLIExtensionPrefix) + ` list --json # Output in JSON format @@ -53,9 +54,16 @@ Examples: repo, _ := cmd.Flags().GetString("repo") path, _ := cmd.Flags().GetString("path") + dir, _ := cmd.Flags().GetString("dir") verbose, _ := cmd.Flags().GetBool("verbose") jsonFlag, _ := cmd.Flags().GetBool("json") labelFilter, _ := cmd.Flags().GetString("label") + + // --dir overrides the local workflow directory when no remote repo is specified. + // When --repo is set, --path is used for the remote repository path instead. + if dir != "" && repo == "" { + path = dir + } return RunListWorkflows(repo, path, pattern, verbose, jsonFlag, labelFilter) }, } @@ -64,9 +72,11 @@ Examples: addJSONFlag(cmd) cmd.Flags().String("label", "", "Filter workflows by label") cmd.Flags().String("path", ".github/workflows", "Path to workflows directory in the repository") + cmd.Flags().StringP("dir", "d", "", "Workflow directory (default: .github/workflows)") // Register completions for list command cmd.ValidArgsFunction = CompleteWorkflowNames + RegisterDirFlagCompletion(cmd, "dir") return cmd } @@ -94,7 +104,7 @@ func RunListWorkflows(repo, path, pattern string, verbose bool, jsonOutput bool, fmt.Fprintf(os.Stderr, "Filtering by pattern: %s\n", pattern) } } - mdFiles, err = getMarkdownWorkflowFiles("") + mdFiles, err = getMarkdownWorkflowFiles(path) } if err != nil { diff --git a/pkg/cli/run_workflow_execution.go b/pkg/cli/run_workflow_execution.go index 5c2eeb42e8f..36329e51550 100644 --- a/pkg/cli/run_workflow_execution.go +++ b/pkg/cli/run_workflow_execution.go @@ -2,6 +2,7 @@ package cli import ( "context" + "encoding/json" "errors" "fmt" "os" @@ -34,6 +35,17 @@ type RunOptions struct { Inputs []string // Workflow inputs in key=value format Verbose bool // Enable verbose output DryRun bool // Validate without actually triggering + JSON bool // Output results in JSON format +} + +// WorkflowRunResult contains the result of a single workflow run trigger for JSON output +type WorkflowRunResult struct { + Workflow string `json:"workflow"` + LockFile string `json:"lock_file"` + Status string `json:"status"` // "triggered", "dry_run", "error" + RunID int64 `json:"run_id,omitempty"` + RunURL string `json:"run_url,omitempty"` + Error string `json:"error,omitempty"` } // RunWorkflowOnGitHub runs an agentic workflow on GitHub Actions @@ -555,6 +567,47 @@ func RunWorkflowsOnGitHub(ctx context.Context, workflowNames []string, opts RunO fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Successfully triggered %d workflow(s)", len(workflowNames)))) return nil } + + // When JSON output is requested, wrap runAllWorkflows to emit a JSON summary + if opts.JSON { + runAllWorkflowsInner := runAllWorkflows + runAllWorkflows = func() error { + // Build per-workflow results + var results []WorkflowRunResult + for _, workflowName := range workflowNames { + normalizedID := normalizeWorkflowID(workflowName) + lockFileName := normalizedID + ".lock.yml" + status := "triggered" + if opts.DryRun { + status = "dry_run" + } + results = append(results, WorkflowRunResult{ + Workflow: normalizedID, + LockFile: lockFileName, + Status: status, + }) + } + + // Execute the actual runs (text output still goes to stderr) + runErr := runAllWorkflowsInner() + if runErr != nil { + // Mark all as error when we can't distinguish which failed + for i := range results { + results[i].Status = "error" + results[i].Error = runErr.Error() + } + } + + // Output JSON to stdout + jsonBytes, err := json.MarshalIndent(results, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Println(string(jsonBytes)) + return runErr + } + } + // Execute workflows with optional repeat functionality return ExecuteWithRepeat(RepeatOptions{ RepeatCount: opts.RepeatCount,