Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f05b279
Initial plan
Copilot Jan 5, 2026
5c89bfd
Add --push flag to run command with transitive imports support
Copilot Jan 5, 2026
4fe52fc
Complete implementation and testing of --push flag
Copilot Jan 5, 2026
30608fb
Add auto-recompilation when lock file is outdated
Copilot Jan 5, 2026
97c4093
Remove accidentally committed test file
Copilot Jan 5, 2026
7fb10cd
Add user confirmation prompt before commit and push
Copilot Jan 5, 2026
bd74add
Replace manual prompt with Bubble Tea confirmation dialog
Copilot Jan 5, 2026
cb1afbd
Move confirmation dialog to console package helper
Copilot Jan 5, 2026
78eedf8
Add check for staged files before creating commit
Copilot Jan 5, 2026
889d12b
Move staged files check after staging workflow files
Copilot Jan 5, 2026
4fa877e
Add warnings for missing/outdated lock files when not using --push
Copilot Jan 5, 2026
e7fadd8
Add extensive logging and fix slice allocation/sorting in run_push.go
Copilot Jan 5, 2026
4503843
Update warning messages to suggest using --push instead of compile
Copilot Jan 5, 2026
a466a5c
Refactor isAccessibleMode to own file and remove duplicates
Copilot Jan 5, 2026
5895b64
Add branch verification check for --push with --ref flag
Copilot Jan 5, 2026
b795625
Move branch validation to after changes detection
Copilot Jan 5, 2026
c1486ff
Merge branch 'main' into copilot/add-push-flag-to-run-command
pelikhan Jan 5, 2026
622d740
Run gofmt to fix formatting issues
Copilot Jan 5, 2026
dbdf37b
Fix all linting issues: testifylint, unconvert, and unused vars
Copilot Jan 5, 2026
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
7 changes: 5 additions & 2 deletions cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,8 @@ Examples:
gh aw run daily-perf-improver --repeat 3 # Run 3 times total
gh aw run daily-perf-improver --enable-if-needed # Enable if disabled, run, then restore state
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 -f name=value -f env=prod # Pass workflow inputs
gh aw run daily-perf-improver --push # Commit and push workflow files before running`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
repeatCount, _ := cmd.Flags().GetInt("repeat")
Expand All @@ -316,12 +317,13 @@ Examples:
autoMergePRs, _ := cmd.Flags().GetBool("auto-merge-prs")
pushSecrets, _ := cmd.Flags().GetBool("use-local-secrets")
inputs, _ := cmd.Flags().GetStringArray("raw-field")
push, _ := cmd.Flags().GetBool("push")

if err := validateEngine(engineOverride); err != nil {
return err
}

return cli.RunWorkflowsOnGitHub(cmd.Context(), args, repeatCount, enable, engineOverride, repoOverride, refOverride, autoMergePRs, pushSecrets, inputs, verboseFlag)
return cli.RunWorkflowsOnGitHub(cmd.Context(), args, repeatCount, enable, engineOverride, repoOverride, refOverride, autoMergePRs, pushSecrets, push, inputs, verboseFlag)
},
}

Expand Down Expand Up @@ -512,6 +514,7 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all
runCmd.Flags().Bool("auto-merge-prs", false, "Auto-merge any pull requests created during the workflow execution")
runCmd.Flags().Bool("use-local-secrets", false, "Use local environment API key secrets for workflow execution (pushes and cleans up secrets in repository)")
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")
// Register completions for run command
runCmd.ValidArgsFunction = cli.CompleteWorkflowNames
cli.RegisterEngineFlagCompletion(runCmd)
Expand Down
20 changes: 10 additions & 10 deletions pkg/cli/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,39 +337,39 @@ func TestDisableWorkflowsFailureScenarios(t *testing.T) {

func TestRunWorkflowOnGitHub(t *testing.T) {
// Test with empty workflow name
err := RunWorkflowOnGitHub(context.Background(), "", false, "", "", "", false, false, false, []string{}, false)
err := RunWorkflowOnGitHub(context.Background(), "", false, "", "", "", false, false, false, false, []string{}, false)
if err == nil {
t.Error("RunWorkflowOnGitHub should return error for empty workflow name")
}

// Test with nonexistent workflow (this will fail but gracefully)
err = RunWorkflowOnGitHub(context.Background(), "nonexistent-workflow", false, "", "", "", false, false, false, []string{}, false)
err = RunWorkflowOnGitHub(context.Background(), "nonexistent-workflow", false, "", "", "", false, false, false, false, []string{}, false)
if err == nil {
t.Error("RunWorkflowOnGitHub should return error for non-existent workflow")
}
}

func TestRunWorkflowsOnGitHub(t *testing.T) {
// Test with empty workflow list
err := RunWorkflowsOnGitHub(context.Background(), []string{}, 0, false, "", "", "", false, false, []string{}, false)
err := RunWorkflowsOnGitHub(context.Background(), []string{}, 0, false, "", "", "", false, false, false, []string{}, false)
if err == nil {
t.Error("RunWorkflowsOnGitHub should return error for empty workflow list")
}

// Test with workflow list containing empty name
err = RunWorkflowsOnGitHub(context.Background(), []string{"valid-workflow", ""}, 0, false, "", "", "", false, false, []string{}, false)
err = RunWorkflowsOnGitHub(context.Background(), []string{"valid-workflow", ""}, 0, false, "", "", "", false, false, false, []string{}, false)
if err == nil {
t.Error("RunWorkflowsOnGitHub should return error for workflow list containing empty name")
}

// Test with nonexistent workflows (this will fail but gracefully)
err = RunWorkflowsOnGitHub(context.Background(), []string{"nonexistent-workflow1", "nonexistent-workflow2"}, 0, false, "", "", "", false, false, []string{}, false)
err = RunWorkflowsOnGitHub(context.Background(), []string{"nonexistent-workflow1", "nonexistent-workflow2"}, 0, false, "", "", "", false, false, false, []string{}, false)
if err == nil {
t.Error("RunWorkflowsOnGitHub should return error for non-existent workflows")
}

// Test with negative repeat seconds (should work as 0)
err = RunWorkflowsOnGitHub(context.Background(), []string{"nonexistent-workflow"}, -1, false, "", "", "", false, false, []string{}, false)
err = RunWorkflowsOnGitHub(context.Background(), []string{"nonexistent-workflow"}, -1, false, "", "", "", false, false, false, []string{}, false)
if err == nil {
t.Error("RunWorkflowsOnGitHub should return error for non-existent workflow regardless of repeat value")
}
Expand Down Expand Up @@ -427,10 +427,10 @@ Test workflow for command existence.`
{func() error { return EnableWorkflows("nonexistent") }, true, "EnableWorkflows"}, // Should now error when no workflows found to enable
{func() error { return DisableWorkflows("nonexistent") }, true, "DisableWorkflows"}, // Should now also error when no workflows found to disable
{func() error {
return RunWorkflowOnGitHub(context.Background(), "", false, "", "", "", false, false, false, []string{}, false)
return RunWorkflowOnGitHub(context.Background(), "", false, "", "", "", false, false, false, false, []string{}, false)
}, true, "RunWorkflowOnGitHub"}, // Should error with empty workflow name
{func() error {
return RunWorkflowsOnGitHub(context.Background(), []string{}, 0, false, "", "", "", false, false, []string{}, false)
return RunWorkflowsOnGitHub(context.Background(), []string{}, 0, false, "", "", "", false, false, false, []string{}, false)
}, true, "RunWorkflowsOnGitHub"}, // Should error with empty workflow list
}

Expand Down Expand Up @@ -1078,13 +1078,13 @@ func TestCalculateTimeRemaining(t *testing.T) {

func TestRunWorkflowOnGitHubWithEnable(t *testing.T) {
// Test with enable flag enabled (should not error for basic validation)
err := RunWorkflowOnGitHub(context.Background(), "nonexistent-workflow", true, "", "", "", false, false, false, []string{}, false)
err := RunWorkflowOnGitHub(context.Background(), "nonexistent-workflow", true, "", "", "", false, false, false, false, []string{}, false)
if err == nil {
t.Error("RunWorkflowOnGitHub should return error for non-existent workflow even with enable flag")
}

// Test with empty workflow name and enable flag
err = RunWorkflowOnGitHub(context.Background(), "", true, "", "", "", false, false, false, []string{}, false)
err = RunWorkflowOnGitHub(context.Background(), "", true, "", "", "", false, false, false, false, []string{}, false)
if err == nil {
t.Error("RunWorkflowOnGitHub should return error for empty workflow name regardless of enable flag")
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/cli/context_cancellation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestRunWorkflowOnGitHubWithCancellation(t *testing.T) {
cancel()

// Try to run a workflow with a cancelled context
err := RunWorkflowOnGitHub(ctx, "test-workflow", false, "", "", "", false, false, false, []string{}, false)
err := RunWorkflowOnGitHub(ctx, "test-workflow", false, "", "", "", false, false, false, false, []string{}, false)

// Should return context.Canceled error
assert.ErrorIs(t, err, context.Canceled, "Should return context.Canceled error when context is cancelled")
Expand All @@ -28,7 +28,7 @@ func TestRunWorkflowsOnGitHubWithCancellation(t *testing.T) {
cancel()

// Try to run workflows with a cancelled context
err := RunWorkflowsOnGitHub(ctx, []string{"test-workflow"}, 0, false, "", "", "", false, false, []string{}, false)
err := RunWorkflowsOnGitHub(ctx, []string{"test-workflow"}, 0, false, "", "", "", false, false, false, []string{}, false)

// Should return context.Canceled error
assert.ErrorIs(t, err, context.Canceled, "Should return context.Canceled error when context is cancelled")
Expand Down Expand Up @@ -96,7 +96,7 @@ func TestRunWorkflowsOnGitHubCancellationDuringExecution(t *testing.T) {
// Try to run multiple workflows that would take a long time
// This should fail validation before timeout, but if it gets past validation,
// it should respect the context cancellation
err := RunWorkflowsOnGitHub(ctx, []string{"nonexistent-workflow-1", "nonexistent-workflow-2"}, 0, false, "", "", "", false, false, []string{}, false)
err := RunWorkflowsOnGitHub(ctx, []string{"nonexistent-workflow-1", "nonexistent-workflow-2"}, 0, false, "", "", "", false, false, false, []string{}, false)

// Should return an error (either validation error or context error)
assert.Error(t, err, "Should return an error")
Expand Down
13 changes: 3 additions & 10 deletions pkg/cli/interactive.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,6 @@ var commonWorkflowNames = []string{
"documentation-check",
}

// isAccessibleMode detects if accessibility mode should be enabled based on environment variables
func isAccessibleMode() bool {
return os.Getenv("ACCESSIBLE") != "" ||
os.Getenv("TERM") == "dumb" ||
os.Getenv("NO_COLOR") != ""
}

// InteractiveWorkflowBuilder collects user input to build an agentic workflow
type InteractiveWorkflowBuilder struct {
WorkflowName string
Expand Down Expand Up @@ -102,7 +95,7 @@ func (b *InteractiveWorkflowBuilder) promptForWorkflowName() error {
Value(&b.WorkflowName).
Validate(ValidateWorkflowName),
),
).WithAccessible(isAccessibleMode())
).WithAccessible(console.IsAccessibleMode())

return form.Run()
}
Expand Down Expand Up @@ -225,7 +218,7 @@ func (b *InteractiveWorkflowBuilder) promptForConfiguration() error {
).
Title("Instructions").
Description("Describe what you want this workflow to accomplish"),
).WithAccessible(isAccessibleMode())
).WithAccessible(console.IsAccessibleMode())

if err := form.Run(); err != nil {
return err
Expand Down Expand Up @@ -268,7 +261,7 @@ func (b *InteractiveWorkflowBuilder) generateWorkflow(force bool) error {
Negative("No, cancel").
Value(&overwrite),
),
).WithAccessible(isAccessibleMode())
).WithAccessible(console.IsAccessibleMode())

if err := confirmForm.Run(); err != nil {
return fmt.Errorf("confirmation failed: %w", err)
Expand Down
6 changes: 4 additions & 2 deletions pkg/cli/interactive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"os"
"strings"
"testing"

"github.com/githubnext/gh-aw/pkg/console"
)

func TestValidateWorkflowName_Integration(t *testing.T) {
Expand Down Expand Up @@ -225,7 +227,7 @@ func TestIsAccessibleMode(t *testing.T) {
os.Unsetenv("NO_COLOR")
}

result := isAccessibleMode()
result := console.IsAccessibleMode()

// Restore original values
if origAccessible != "" {
Expand All @@ -245,7 +247,7 @@ func TestIsAccessibleMode(t *testing.T) {
}

if result != tt.expected {
t.Errorf("isAccessibleMode() with ACCESSIBLE=%q TERM=%q NO_COLOR=%q = %v, want %v",
t.Errorf("console.IsAccessibleMode() with ACCESSIBLE=%q TERM=%q NO_COLOR=%q = %v, want %v",
tt.accessible, tt.term, tt.noColor, result, tt.expected)
}
})
Expand Down
48 changes: 44 additions & 4 deletions pkg/cli/run_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import (
var runLog = logger.New("cli:run_command")

// RunWorkflowOnGitHub runs an agentic workflow on GitHub Actions
func RunWorkflowOnGitHub(ctx context.Context, workflowIdOrName string, enable bool, engineOverride string, repoOverride string, refOverride string, autoMergePRs bool, pushSecrets bool, waitForCompletion bool, inputs []string, verbose bool) error {
runLog.Printf("Starting workflow run: workflow=%s, enable=%v, engineOverride=%s, repo=%s, ref=%s, wait=%v, inputs=%v", workflowIdOrName, enable, engineOverride, repoOverride, refOverride, waitForCompletion, inputs)
func RunWorkflowOnGitHub(ctx context.Context, workflowIdOrName string, enable bool, engineOverride string, repoOverride string, refOverride string, autoMergePRs bool, pushSecrets bool, push bool, waitForCompletion bool, inputs []string, verbose bool) error {
runLog.Printf("Starting workflow run: workflow=%s, enable=%v, engineOverride=%s, repo=%s, ref=%s, push=%v, wait=%v, inputs=%v", workflowIdOrName, enable, engineOverride, repoOverride, refOverride, push, waitForCompletion, inputs)

// Check context cancellation at the start
select {
Expand Down Expand Up @@ -225,6 +225,46 @@ func RunWorkflowOnGitHub(ctx context.Context, workflowIdOrName string, enable bo
fmt.Printf("Using lock file: %s\n", lockFileName)
}

// Check for missing or outdated lock files (when not using --push)
if !push && repoOverride == "" {
workflowMarkdownPath := strings.TrimSuffix(lockFilePath, ".lock.yml") + ".md"
if status, err := checkLockFileStatus(workflowMarkdownPath); err == nil {
if status.Missing {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Lock file is missing"))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Run 'gh aw run %s --push' to automatically compile and push the lock file", workflowIdOrName)))
} else if status.Outdated {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Lock file is outdated (workflow file is newer)"))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Run 'gh aw run %s --push' to automatically compile and push the lock file", workflowIdOrName)))
}
}
}

// Handle --push flag: commit and push workflow files before running
if push {
// Only valid for local workflows
if repoOverride != "" {
return fmt.Errorf("--push flag is only supported for local workflows, not remote repositories")
}

if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Collecting workflow files for push..."))
}

// Collect the workflow .md file, .lock.yml file, and transitive imports
workflowMarkdownPath := strings.TrimSuffix(lockFilePath, ".lock.yml") + ".md"
files, err := collectWorkflowFiles(workflowMarkdownPath, verbose)
if err != nil {
return fmt.Errorf("failed to collect workflow files: %w", err)
}

// Commit and push the files (includes branch verification if --ref is specified)
if err := pushWorkflowFiles(workflowIdOrName, files, refOverride, verbose); err != nil {
return fmt.Errorf("failed to push workflow files: %w", err)
}

fmt.Println(console.FormatSuccessMessage(fmt.Sprintf("Successfully pushed %d file(s) for workflow %s", len(files), workflowIdOrName)))
}

// Handle secret pushing if requested
var secretTracker *TrialSecretTracker
if pushSecrets {
Expand Down Expand Up @@ -472,7 +512,7 @@ func RunWorkflowOnGitHub(ctx context.Context, workflowIdOrName string, enable bo
}

// RunWorkflowsOnGitHub runs multiple agentic workflows on GitHub Actions, optionally repeating a specified number of times
func RunWorkflowsOnGitHub(ctx context.Context, workflowNames []string, repeatCount int, enable bool, engineOverride string, repoOverride string, refOverride string, autoMergePRs bool, pushSecrets bool, inputs []string, verbose bool) error {
func RunWorkflowsOnGitHub(ctx context.Context, workflowNames []string, repeatCount int, enable bool, engineOverride string, repoOverride string, refOverride string, autoMergePRs bool, pushSecrets bool, push bool, inputs []string, verbose bool) error {
if len(workflowNames) == 0 {
return fmt.Errorf("at least one workflow name or ID is required")
}
Expand Down Expand Up @@ -535,7 +575,7 @@ func RunWorkflowsOnGitHub(ctx context.Context, workflowNames []string, repeatCou
fmt.Println(console.FormatProgressMessage(fmt.Sprintf("Running workflow %d/%d: %s", i+1, len(workflowNames), workflowName)))
}

if err := RunWorkflowOnGitHub(ctx, workflowName, enable, engineOverride, repoOverride, refOverride, autoMergePRs, pushSecrets, waitForCompletion, inputs, verbose); err != nil {
if err := RunWorkflowOnGitHub(ctx, workflowName, enable, engineOverride, repoOverride, refOverride, autoMergePRs, pushSecrets, push, waitForCompletion, inputs, verbose); err != nil {
return fmt.Errorf("failed to run workflow '%s': %w", workflowName, err)
}

Expand Down
Loading
Loading