Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
},
}

Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand Down Expand Up @@ -409,6 +419,7 @@ Examples:
Inputs: inputs,
Verbose: verboseFlag,
DryRun: dryRun,
JSON: jsonOutput,
})
},
}
Expand Down Expand Up @@ -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)")
Expand Down Expand Up @@ -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)
Expand Down
15 changes: 14 additions & 1 deletion pkg/cli/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
}
Comment on lines +82 to +89
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--repo parsing uses strings.SplitN(repoFlag, "/", 2), which will silently mis-handle values containing more than one / (e.g., host/owner/repo becomes owner=host, repo=owner/repo). Since the flag is meant to be owner/repo here, validate using an exact 2-part split (or a shared slug parser) so invalid inputs error instead of producing incorrect owner/repo values.

Copilot uses AI. Check for mistakes.

return AuditWorkflowRun(
cmd.Context(),
Expand All @@ -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
Expand Down
14 changes: 9 additions & 5 deletions pkg/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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:
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 11 additions & 1 deletion pkg/cli/list_workflows_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
},
}
Expand All @@ -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)")

Comment on lines 74 to 76
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The --dir flag help says (default: .github/workflows), but the flag default is actually an empty string and the default behavior comes from --path. Consider updating the description to clarify that --dir is an override for local listing when --repo is not set (and that the default local directory is still .github/workflows).

Copilot uses AI. Check for mistakes.
// Register completions for list command
cmd.ValidArgsFunction = CompleteWorkflowNames
RegisterDirFlagCompletion(cmd, "dir")

return cmd
}
Expand Down Expand Up @@ -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 {
Expand Down
53 changes: 53 additions & 0 deletions pkg/cli/run_workflow_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading