From 50cb2e282faff18711889ebba1ebd1c663549394 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Fri, 31 Oct 2025 01:21:02 +0000 Subject: [PATCH 1/4] add flag for using local secrets --- cmd/gh-aw/main.go | 4 +- docs/src/content/docs/tools/cli.md | 30 +++++++++- pkg/cli/commands_test.go | 20 +++---- pkg/cli/run_command.go | 95 +++++++++++++++++++++++++++++- pkg/cli/trial_command.go | 79 ++++++++++++++++--------- 5 files changed, 186 insertions(+), 42 deletions(-) diff --git a/cmd/gh-aw/main.go b/cmd/gh-aw/main.go index 1cc28a4a86..7c26dc0a3c 100644 --- a/cmd/gh-aw/main.go +++ b/cmd/gh-aw/main.go @@ -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), @@ -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() diff --git a/docs/src/content/docs/tools/cli.md b/docs/src/content/docs/tools/cli.md index c6106975a8..05ae6cf003 100644 --- a/docs/src/content/docs/tools/cli.md +++ b/docs/src/content/docs/tools/cli.md @@ -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 @@ -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 ``` @@ -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 @@ -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 @@ -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 diff --git a/pkg/cli/commands_test.go b/pkg/cli/commands_test.go index ec23f53e94..f2d05a672c 100644 --- a/pkg/cli/commands_test.go +++ b/pkg/cli/commands_test.go @@ -335,13 +335,13 @@ 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") } @@ -349,25 +349,25 @@ func TestRunWorkflowOnGitHub(t *testing.T) { 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") } @@ -406,8 +406,8 @@ func TestAllCommandsExist(t *testing.T) { {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 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 { @@ -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") } diff --git a/pkg/cli/run_command.go b/pkg/cli/run_command.go index f4e8ea83db..0badba67a0 100644 --- a/pkg/cli/run_command.go +++ b/pkg/cli/run_command.go @@ -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 == "" { @@ -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 WorkflowData + // 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 WorkflowData with engine config + 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 != "" { @@ -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") } @@ -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) } diff --git a/pkg/cli/trial_command.go b/pkg/cli/trial_command.go index b1b5ecf551..ba37a5d387 100644 --- a/pkg/cli/trial_command.go +++ b/pkg/cli/trial_command.go @@ -104,6 +104,7 @@ Trial results are saved both locally (in trials/ directory) and in the host repo logicalRepoSpec, _ := cmd.Flags().GetString("logical-repo") cloneRepoSpec, _ := cmd.Flags().GetString("clone-repo") hostRepoSpec, _ := cmd.Flags().GetString("host-repo") + repoSpec, _ := cmd.Flags().GetString("repo") deleteHostRepo, _ := cmd.Flags().GetBool("delete-host-repo-after") forceDeleteHostRepo, _ := cmd.Flags().GetBool("force-delete-host-repo-before") yes, _ := cmd.Flags().GetBool("yes") @@ -113,14 +114,22 @@ Trial results are saved both locally (in trials/ directory) and in the host repo autoMergePRs, _ := cmd.Flags().GetBool("auto-merge-prs") engineOverride, _ := cmd.Flags().GetString("engine") appendText, _ := cmd.Flags().GetString("append") + pushSecrets, _ := cmd.Flags().GetBool("use-local-secrets") verbose, _ := cmd.Root().PersistentFlags().GetBool("verbose") if err := validateEngine(engineOverride); err != nil { fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error())) os.Exit(1) } + if hostRepoSpec != "" && repoSpec != "" { + fmt.Fprintln(os.Stderr, console.FormatErrorMessage("Please use either --host-repo or --repo, they are identical")) + os.Exit(1) + } + if repoSpec != "" { + hostRepoSpec = repoSpec + } - if err := RunWorkflowTrials(workflowSpecs, logicalRepoSpec, cloneRepoSpec, hostRepoSpec, deleteHostRepo, forceDeleteHostRepo, yes, timeout, triggerContext, repeatCount, autoMergePRs, engineOverride, appendText, verbose); err != nil { + if err := RunWorkflowTrials(workflowSpecs, logicalRepoSpec, cloneRepoSpec, hostRepoSpec, deleteHostRepo, forceDeleteHostRepo, yes, timeout, triggerContext, repeatCount, autoMergePRs, engineOverride, appendText, pushSecrets, verbose); err != nil { fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error())) os.Exit(1) } @@ -139,6 +148,7 @@ Trial results are saved both locally (in trials/ directory) and in the host repo } cmd.Flags().String("host-repo", "", fmt.Sprintf("Custom host repository slug (defaults to '%s'). Use '.' for current repository", defaultHostRepo)) + cmd.Flags().String("repo", "", "Alias for --host-repo") cmd.Flags().Bool("delete-host-repo-after", false, "Delete the host repository after completion (default: keep)") cmd.Flags().Bool("force-delete-host-repo-before", false, "Force delete the host repository before creation, if it exists before creating it") cmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompts") @@ -148,12 +158,13 @@ Trial results are saved both locally (in trials/ directory) and in the host repo cmd.Flags().Bool("auto-merge-prs", false, "Auto-merge any pull requests created during the trial (requires --clone-repo)") cmd.Flags().StringP("engine", "a", "", "Override AI engine (claude, codex, copilot, custom)") cmd.Flags().String("append", "", "Append extra content to the end of agentic workflow on installation") + cmd.Flags().Bool("use-local-secrets", false, "Use local environment API key secrets for trial execution (pushes and cleans up secrets in repository)") return cmd } // RunWorkflowTrials executes the main logic for trialing one or more workflows -func RunWorkflowTrials(workflowSpecs []string, logicalRepoSpec string, cloneRepoSpec string, hostRepoSpec string, deleteHostRepo, forceDeleteHostRepo, quiet bool, timeoutMinutes int, triggerContext string, repeatCount int, autoMergePRs bool, engineOverride string, appendText string, verbose bool) error { +func RunWorkflowTrials(workflowSpecs []string, logicalRepoSpec string, cloneRepoSpec string, hostRepoSpec string, deleteHostRepo, forceDeleteHostRepo, quiet bool, timeoutMinutes int, triggerContext string, repeatCount int, autoMergePRs bool, engineOverride string, appendText string, pushSecrets bool, verbose bool) error { // Parse all workflow specifications var parsedSpecs []*WorkflowSpec for _, spec := range workflowSpecs { @@ -251,15 +262,20 @@ func RunWorkflowTrials(workflowSpecs []string, logicalRepoSpec string, cloneRepo } // Step 2.5: Create secret tracker - secretTracker := NewTrialSecretTracker(hostRepoSlug) - trialLog.Print("Created secret tracker for trial") + var secretTracker *TrialSecretTracker + if pushSecrets { + secretTracker = NewTrialSecretTracker(hostRepoSlug) + trialLog.Print("Created secret tracker for trial") - // Set up secret cleanup to always run on exit - defer func() { - if err := cleanupTrialSecrets(hostRepoSlug, secretTracker, verbose); err != nil { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to cleanup secrets: %v", err))) - } - }() + // Set up secret cleanup to always run on exit + defer func() { + if err := cleanupTrialSecrets(hostRepoSlug, secretTracker, verbose); err != nil { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to cleanup secrets: %v", err))) + } + }() + } else { + trialLog.Print("Secret pushing disabled, workflows must have required secrets already configured") + } // Set up cleanup if requested if deleteHostRepo { @@ -308,12 +324,12 @@ func RunWorkflowTrials(workflowSpecs []string, logicalRepoSpec string, cloneRepo fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("=== Running trial for workflow: %s ===", parsedSpec.WorkflowName))) // Install workflow with trial mode compilation - if err := installWorkflowInTrialMode(tempDir, parsedSpec, logicalRepoSlug, cloneRepoSlug, hostRepoSlug, secretTracker, engineOverride, appendText, verbose); err != nil { + if err := installWorkflowInTrialMode(tempDir, parsedSpec, logicalRepoSlug, cloneRepoSlug, hostRepoSlug, secretTracker, engineOverride, appendText, pushSecrets, verbose); err != nil { return fmt.Errorf("failed to install workflow '%s' in trial mode: %w", parsedSpec.WorkflowName, err) } // Add user's PAT as repository secret (only once) - if i == 0 { + if i == 0 && pushSecrets { if err := addGitHubTokenSecret(hostRepoSlug, secretTracker, verbose); err != nil { return fmt.Errorf("failed to add GitHub token secret: %w", err) } @@ -677,7 +693,7 @@ func cloneTrialHostRepository(repoSlug string, verbose bool) (string, error) { } // installWorkflowInTrialMode installs a workflow in trial mode using a parsed spec -func installWorkflowInTrialMode(tempDir string, parsedSpec *WorkflowSpec, logicalRepoSlug, cloneRepoSlug, hostRepoSlug string, secretTracker *TrialSecretTracker, engineOverride string, appendText string, verbose bool) error { +func installWorkflowInTrialMode(tempDir string, parsedSpec *WorkflowSpec, logicalRepoSlug, cloneRepoSlug, hostRepoSlug string, secretTracker *TrialSecretTracker, engineOverride string, appendText string, pushSecrets bool, verbose bool) error { // Change to temp directory originalDir, err := os.Getwd() if err != nil { @@ -744,8 +760,10 @@ func installWorkflowInTrialMode(tempDir string, parsedSpec *WorkflowSpec, logica workflowData := workflowDataList[0] // Determine required engine secret from workflow data - if err := determineAndAddEngineSecret(workflowData, hostRepoSlug, secretTracker, engineOverride, verbose); err != nil { - return fmt.Errorf("failed to determine engine secret: %w", err) + if pushSecrets { + if err := determineAndAddEngineSecret(workflowData.EngineConfig, hostRepoSlug, secretTracker, engineOverride, verbose); err != nil { + return fmt.Errorf("failed to determine engine secret: %w", err) + } } // Commit and push the changes @@ -838,8 +856,10 @@ func addGitHubTokenSecret(repoSlug string, tracker *TrialSecretTracker, verbose return fmt.Errorf("failed to set repository secret: %w (output: %s)", err, string(output)) } - // Mark as successfully added - tracker.AddedSecrets[secretName] = true + // Mark as successfully added (only if tracker is provided) + if tracker != nil { + tracker.AddedSecrets[secretName] = true + } if verbose { fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Added %s secret to host repository", secretName))) @@ -924,7 +944,7 @@ func parseIssueSpec(input string) string { } // determineAndAddEngineSecret determines and sets the appropriate engine secret based on workflow configuration with tracking -func determineAndAddEngineSecret(workflowData *workflow.WorkflowData, hostRepoSlug string, tracker *TrialSecretTracker, engineOverride string, verbose bool) error { +func determineAndAddEngineSecret(engineConfig *workflow.EngineConfig, hostRepoSlug string, tracker *TrialSecretTracker, engineOverride string, verbose bool) error { var engineType string if verbose { @@ -939,14 +959,9 @@ func determineAndAddEngineSecret(workflowData *workflow.WorkflowData, hostRepoSl engineType = engineOverride fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Using engine override: %s", engineType))) } else { - // Find the matching workflow and determine its engine - // Check both the original filename-based name and the processed display name - if verbose { - fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Found matching workflow: %s", workflowData.Name))) - } // Check if engine is specified in the EngineConfig - if workflowData.EngineConfig != nil && workflowData.EngineConfig.ID != "" { - engineType = workflowData.EngineConfig.ID + if engineConfig != nil && engineConfig.ID != "" { + engineType = engineConfig.ID if verbose { fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Found engine in EngineConfig: %s", engineType))) } @@ -1034,8 +1049,10 @@ func addEngineSecret(secretName, hostRepoSlug string, tracker *TrialSecretTracke return fmt.Errorf("failed to add %s secret: %w\nOutput: %s", secretName, err, string(output)) } - // Mark as successfully added - tracker.AddedSecrets[secretName] = true + // Mark as successfully added (only if tracker is provided) + if tracker != nil { + tracker.AddedSecrets[secretName] = true + } if verbose { fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Successfully added %s secret", secretName))) @@ -1162,6 +1179,14 @@ func commitAndPushWorkflow(tempDir, workflowName string, verbose bool) error { // cleanupTrialSecrets removes API key secrets from the host repository for security, based on tracking information func cleanupTrialSecrets(repoSlug string, tracker *TrialSecretTracker, verbose bool) error { + // Skip cleanup if no tracker was provided (secrets were not pushed) + if tracker == nil { + if verbose { + fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No secrets to clean up (secret pushing was disabled)")) + } + return nil + } + if verbose { fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Cleaning up API key secrets from host repository")) } From be4b57082f4008bc702c2e0d5743d46e8e6bb449 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Fri, 31 Oct 2025 01:31:03 +0000 Subject: [PATCH 2/4] Update pkg/cli/run_command.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/cli/run_command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cli/run_command.go b/pkg/cli/run_command.go index 0badba67a0..17ad5de010 100644 --- a/pkg/cli/run_command.go +++ b/pkg/cli/run_command.go @@ -238,7 +238,7 @@ func RunWorkflowOnGitHub(workflowIdOrName string, enable bool, engineOverride st } } - // Create minimal WorkflowData with engine config + // Create minimal EngineConfig with engine information engineConfig := &workflow.EngineConfig{ ID: engineType, } From 463d13858b47e1ea4ef1146872a7fc5bdfdfa776 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Fri, 31 Oct 2025 01:31:11 +0000 Subject: [PATCH 3/4] Update pkg/cli/run_command.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/cli/run_command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cli/run_command.go b/pkg/cli/run_command.go index 17ad5de010..bd1bc71d57 100644 --- a/pkg/cli/run_command.go +++ b/pkg/cli/run_command.go @@ -226,7 +226,7 @@ func RunWorkflowOnGitHub(workflowIdOrName string, enable bool, engineOverride st 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 WorkflowData + // 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 != "" { From eaccb3d653cfe5911e742804c64ece2bc3657f6d Mon Sep 17 00:00:00 2001 From: Don Syme Date: Fri, 31 Oct 2025 01:33:23 +0000 Subject: [PATCH 4/4] fmt --- pkg/cli/commands_test.go | 8 ++++---- pkg/cli/run_command.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/cli/commands_test.go b/pkg/cli/commands_test.go index f2d05a672c..46bd70d39d 100644 --- a/pkg/cli/commands_test.go +++ b/pkg/cli/commands_test.go @@ -402,10 +402,10 @@ 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 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 } diff --git a/pkg/cli/run_command.go b/pkg/cli/run_command.go index 0badba67a0..1a17e6d9d5 100644 --- a/pkg/cli/run_command.go +++ b/pkg/cli/run_command.go @@ -237,12 +237,12 @@ func RunWorkflowOnGitHub(workflowIdOrName string, enable bool, engineOverride st fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Using default Copilot engine for remote workflow secret handling")) } } - + // Create minimal WorkflowData with engine config 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 {