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
4 changes: 3 additions & 1 deletion cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,14 @@ Examples:
engineOverride, _ := cmd.Flags().GetString("engine")
repoOverride, _ := cmd.Flags().GetString("repo")
autoMergePRs, _ := cmd.Flags().GetBool("auto-merge-prs")
pushSecrets, _ := cmd.Flags().GetBool("use-local-secrets")

if err := validateEngine(engineOverride); err != nil {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
os.Exit(1)
}

if err := cli.RunWorkflowsOnGitHub(args, repeatCount, enable, engineOverride, repoOverride, autoMergePRs, verboseFlag); err != nil {
if err := cli.RunWorkflowsOnGitHub(args, repeatCount, enable, engineOverride, repoOverride, autoMergePRs, pushSecrets, verboseFlag); err != nil {
fmt.Fprintln(os.Stderr, console.FormatError(console.CompilerError{
Type: "error",
Message: fmt.Sprintf("running workflows on GitHub Actions: %v", err),
Expand Down Expand Up @@ -296,6 +297,7 @@ func init() {
runCmd.Flags().StringP("engine", "a", "", "Override AI engine (claude, codex, copilot, custom)")
runCmd.Flags().StringP("repo", "r", "", "Repository to run the workflow in (owner/repo format)")
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)")

// Create and setup status command
statusCmd := cli.NewStatusCommand()
Expand Down
30 changes: 29 additions & 1 deletion docs/src/content/docs/tools/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ gh aw init # Initialize repository (first-
gh aw add githubnext/agentics/ci-doctor # Add workflow and compile to GitHub Actions
gh aw compile # Recompile to GitHub Actions
gh aw trial githubnext/agentics/ci-doctor # Test workflow safely before adding
gh aw trial ./my-workflow.md # Test local workflow during development
gh aw trial ./my-workflow.md --use-local-secrets # Test local workflow with local API keys
gh aw update # Update all workflows with source field
gh aw status # Check status
gh aw run daily-perf # Execute workflow
Expand Down Expand Up @@ -276,6 +276,7 @@ These commands control the execution and state of your compiled agentic workflow
gh aw run WorkflowName # Run single workflow
gh aw run WorkflowName1 WorkflowName2 # Run multiple workflows
gh aw run WorkflowName --repeat 3 # Run 3 times total
gh aw run workflow --use-local-secrets # Use local API keys for execution
gh aw run weekly-research --enable-if-needed --input priority=high
```

Expand All @@ -287,6 +288,7 @@ Test workflows safely in a temporary private repository without affecting your t
gh aw trial githubnext/agentics/ci-doctor # Test from source repo
gh aw trial ./my-local-workflow.md # Test local file
gh aw trial workflow1 workflow2 # Compare multiple workflows
gh aw trial ./workflow.md --use-local-secrets # Use local API keys for trial
gh aw trial ./workflow.md --logical-repo myorg/myrepo --host-repo myorg/host-repo # Act as if in a different logical repo. Uses PAT to see issues/PRs
gh aw trial ./workflow.md --clone-repo myorg/myrepo --host-repo myorg/host-repo # Copy the code of the clone repo for into host repo. Agentic will see the codebase of clone repo but not the issues/PRs.
gh aw trial ./workflow.md --append "Extra content" # Append custom content to workflow
Expand All @@ -298,6 +300,7 @@ gh aw trial githubnext/agentics/issue-triage --trigger-context "#456"
Other flags:
--engine ENGINE # Override engine (default: from frontmatter)
--auto-merge-prs # Auto-merge PRs created during trial
--use-local-secrets # Use local environment API keys (pushes/cleans up secrets)
--repeat N # Repeat N times
--force-delete-host-repo-before # Force delete existing host repo BEFORE start
--delete-host-repo-after # Delete host repo AFTER trial
Expand All @@ -310,6 +313,31 @@ When using `gh aw trial --logical-repo`, the agentic workflow operates as if it

When using `gh aw trial --clone-repo`, the agentic workflow uses the codebase from the specified clone repository while still interacting with issues and pull requests from the host repository. This allows for testing how the workflow would behave with a different codebase while maintaining access to the relevant repository data.

### Using Local API Keys

Both `run` and `trial` commands support the `--use-local-secrets` flag to automatically push required API key secrets from your local environment to the repository before execution:

```bash
gh aw run my-workflow --use-local-secrets # Use local API keys for run
gh aw trial ./workflow.md --use-local-secrets # Use local API keys for trial
```

**How it works:**
- Reads API keys from environment variables (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `COPILOT_CLI_TOKEN`, etc.)
- Temporarily pushes the required secrets to the repository before workflow execution
- Automatically cleans up (deletes) the secrets after completion
- Only pushes secrets that are actually needed by the workflow's AI engine

**When to use:**
- Testing workflows that require AI engine secrets not yet configured in the repository
- Trial mode when you want to test with your local API keys
- Development environments where you don't want to permanently store secrets

**Security notes:**
- Secrets are only pushed temporarily and are cleaned up automatically
- Use with caution in shared or production repositories
- Consider using repository secrets for permanent deployments

### Workflow State Management

```bash
Expand Down
28 changes: 14 additions & 14 deletions pkg/cli/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,39 +335,39 @@ func TestDisableWorkflowsFailureScenarios(t *testing.T) {

func TestRunWorkflowOnGitHub(t *testing.T) {
// Test with empty workflow name
err := RunWorkflowOnGitHub("", false, "", "", false, false)
err := RunWorkflowOnGitHub("", false, "", "", false, false, 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("nonexistent-workflow", false, "", "", false, false)
err = RunWorkflowOnGitHub("nonexistent-workflow", false, "", "", false, false, 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([]string{}, 0, false, "", "", false, false)
err := RunWorkflowsOnGitHub([]string{}, 0, false, "", "", false, false, false)
if err == nil {
t.Error("RunWorkflowsOnGitHub should return error for empty workflow list")
}

// Test with workflow list containing empty name
err = RunWorkflowsOnGitHub([]string{"valid-workflow", ""}, 0, false, "", "", false, false)
err = RunWorkflowsOnGitHub([]string{"valid-workflow", ""}, 0, false, "", "", false, false, 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([]string{"nonexistent-workflow1", "nonexistent-workflow2"}, 0, false, "", "", false, false)
err = RunWorkflowsOnGitHub([]string{"nonexistent-workflow1", "nonexistent-workflow2"}, 0, false, "", "", false, false, 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([]string{"nonexistent-workflow"}, -1, false, "", "", false, false)
err = RunWorkflowsOnGitHub([]string{"nonexistent-workflow"}, -1, false, "", "", false, false, false)
if err == nil {
t.Error("RunWorkflowsOnGitHub should return error for non-existent workflow regardless of repeat value")
}
Expand Down Expand Up @@ -402,12 +402,12 @@ func TestAllCommandsExist(t *testing.T) {
_, err := CompileWorkflows(config)
return err
}, false, "CompileWorkflows"}, // Should compile existing markdown files successfully
{func() error { return RemoveWorkflows("test", false) }, false, "RemoveWorkflows"}, // Should handle missing directory gracefully
{func() error { return StatusWorkflows("test", false, false) }, false, "StatusWorkflows"}, // Should handle missing directory gracefully
{func() error { return EnableWorkflows("test") }, true, "EnableWorkflows"}, // Should now error when no workflows found to enable
{func() error { return DisableWorkflows("test") }, true, "DisableWorkflows"}, // Should now also error when no workflows found to disable
{func() error { return RunWorkflowOnGitHub("", false, "", "", false, false) }, true, "RunWorkflowOnGitHub"}, // Should error with empty workflow name
{func() error { return RunWorkflowsOnGitHub([]string{}, 0, false, "", "", false, false) }, true, "RunWorkflowsOnGitHub"}, // Should error with empty workflow list
{func() error { return RemoveWorkflows("test", false) }, false, "RemoveWorkflows"}, // Should handle missing directory gracefully
{func() error { return StatusWorkflows("test", false, false) }, false, "StatusWorkflows"}, // Should handle missing directory gracefully
{func() error { return EnableWorkflows("test") }, true, "EnableWorkflows"}, // Should now error when no workflows found to enable
{func() error { return DisableWorkflows("test") }, true, "DisableWorkflows"}, // Should now also error when no workflows found to disable
{func() error { return RunWorkflowOnGitHub("", false, "", "", false, false, false) }, true, "RunWorkflowOnGitHub"}, // Should error with empty workflow name
{func() error { return RunWorkflowsOnGitHub([]string{}, 0, false, "", "", false, false, false) }, true, "RunWorkflowsOnGitHub"}, // Should error with empty workflow list
}

for _, test := range tests {
Expand Down Expand Up @@ -1046,13 +1046,13 @@ func TestCalculateTimeRemaining(t *testing.T) {

func TestRunWorkflowOnGitHubWithEnable(t *testing.T) {
// Test with enable flag enabled (should not error for basic validation)
err := RunWorkflowOnGitHub("nonexistent-workflow", true, "", "", false, false)
err := RunWorkflowOnGitHub("nonexistent-workflow", true, "", "", false, false, 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("", true, "", "", false, false)
err = RunWorkflowOnGitHub("", true, "", "", false, false, false)
if err == nil {
t.Error("RunWorkflowOnGitHub should return error for empty workflow name regardless of enable flag")
}
Expand Down
95 changes: 92 additions & 3 deletions pkg/cli/run_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ import (
"github.com/githubnext/gh-aw/pkg/constants"
"github.com/githubnext/gh-aw/pkg/logger"
"github.com/githubnext/gh-aw/pkg/parser"
"github.com/githubnext/gh-aw/pkg/workflow"
)

var runLog = logger.New("cli:run_command")

// RunWorkflowOnGitHub runs an agentic workflow on GitHub Actions
func RunWorkflowOnGitHub(workflowIdOrName string, enable bool, engineOverride string, repoOverride string, autoMergePRs bool, verbose bool) error {
func RunWorkflowOnGitHub(workflowIdOrName string, enable bool, engineOverride string, repoOverride string, autoMergePRs bool, pushSecrets bool, verbose bool) error {
runLog.Printf("Starting workflow run: workflow=%s, enable=%v, engineOverride=%s, repo=%s", workflowIdOrName, enable, engineOverride, repoOverride)

if workflowIdOrName == "" {
Expand Down Expand Up @@ -163,6 +164,94 @@ func RunWorkflowOnGitHub(workflowIdOrName string, enable bool, engineOverride st
fmt.Printf("Using lock file: %s\n", lockFileName)
}

// Handle secret pushing if requested
var secretTracker *TrialSecretTracker
if pushSecrets {
// Determine target repository
var targetRepo string
if repoOverride != "" {
targetRepo = repoOverride
} else {
// Get current repository slug
currentRepo, err := GetCurrentRepoSlug()
if err != nil {
return fmt.Errorf("failed to determine current repository for secret handling: %w", err)
}
targetRepo = currentRepo
}

secretTracker = NewTrialSecretTracker(targetRepo)
runLog.Printf("Created secret tracker for repository: %s", targetRepo)

// Set up secret cleanup to always run on exit
defer func() {
if err := cleanupTrialSecrets(targetRepo, secretTracker, verbose); err != nil {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to cleanup secrets: %v", err)))
}
}()

// Add GitHub token secret
if err := addGitHubTokenSecret(targetRepo, secretTracker, verbose); err != nil {
return fmt.Errorf("failed to add GitHub token secret: %w", err)
}

// Determine and add engine secrets
if repoOverride == "" && lockFilePath != "" {
// For local workflows, read and parse the workflow to determine engine requirements
workflowMarkdownPath := strings.TrimSuffix(lockFilePath, ".lock.yml") + ".md"
config := CompileConfig{
MarkdownFiles: []string{workflowMarkdownPath},
Verbose: false, // Don't be verbose during secret determination
EngineOverride: engineOverride,
Validate: false,
Watch: false,
WorkflowDir: "",
SkipInstructions: true,
NoEmit: true, // Don't emit files, just compile for analysis
Purge: false,
TrialMode: false,
TrialLogicalRepoSlug: "",
Strict: false,
}
workflowDataList, err := CompileWorkflows(config)
if err == nil && len(workflowDataList) == 1 {
workflowData := workflowDataList[0]
if err := determineAndAddEngineSecret(workflowData.EngineConfig, targetRepo, secretTracker, engineOverride, verbose); err != nil {
// Log warning but don't fail - the workflow might still run without secrets
if verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to determine engine secret: %v", err)))
}
}
} else if verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Failed to compile workflow for secret determination - continuing without engine secrets"))
}
} else if repoOverride != "" {
// For remote workflows, we can't analyze the workflow file, so create a minimal EngineConfig
// with engine information and reuse the existing determineAndAddEngineSecret function
var engineType string
if engineOverride != "" {
engineType = engineOverride
} else {
engineType = "copilot" // Default engine
if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Using default Copilot engine for remote workflow secret handling"))
}
}

// Create minimal EngineConfig with engine information
engineConfig := &workflow.EngineConfig{
ID: engineType,
}

if err := determineAndAddEngineSecret(engineConfig, targetRepo, secretTracker, engineOverride, verbose); err != nil {
// Log warning but don't fail - the workflow might still run without secrets
if verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to determine engine secret for remote workflow: %v", err)))
}
}
}
}

// Build the gh workflow run command with optional repo override
args := []string{"workflow", "run", lockFileName}
if repoOverride != "" {
Expand Down Expand Up @@ -260,7 +349,7 @@ func RunWorkflowOnGitHub(workflowIdOrName string, enable bool, engineOverride st
}

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

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

Expand Down
Loading
Loading