Skip to content

Commit 7782717

Browse files
dsymeCopilot
andauthored
🔑 Add flag for using local secrets during workflow execution (#2841)
* add flag for using local secrets * Update pkg/cli/run_command.go Co-authored-by: Copilot <[email protected]> * Update pkg/cli/run_command.go Co-authored-by: Copilot <[email protected]> * fmt --------- Co-authored-by: Copilot <[email protected]>
1 parent 5813e6f commit 7782717

File tree

5 files changed

+190
-46
lines changed

5 files changed

+190
-46
lines changed

‎cmd/gh-aw/main.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,13 +208,14 @@ Examples:
208208
engineOverride, _ := cmd.Flags().GetString("engine")
209209
repoOverride, _ := cmd.Flags().GetString("repo")
210210
autoMergePRs, _ := cmd.Flags().GetBool("auto-merge-prs")
211+
pushSecrets, _ := cmd.Flags().GetBool("use-local-secrets")
211212

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

217-
if err := cli.RunWorkflowsOnGitHub(args, repeatCount, enable, engineOverride, repoOverride, autoMergePRs, verboseFlag); err != nil {
218+
if err := cli.RunWorkflowsOnGitHub(args, repeatCount, enable, engineOverride, repoOverride, autoMergePRs, pushSecrets, verboseFlag); err != nil {
218219
fmt.Fprintln(os.Stderr, console.FormatError(console.CompilerError{
219220
Type: "error",
220221
Message: fmt.Sprintf("running workflows on GitHub Actions: %v", err),
@@ -296,6 +297,7 @@ func init() {
296297
runCmd.Flags().StringP("engine", "a", "", "Override AI engine (claude, codex, copilot, custom)")
297298
runCmd.Flags().StringP("repo", "r", "", "Repository to run the workflow in (owner/repo format)")
298299
runCmd.Flags().Bool("auto-merge-prs", false, "Auto-merge any pull requests created during the workflow execution")
300+
runCmd.Flags().Bool("use-local-secrets", false, "Use local environment API key secrets for workflow execution (pushes and cleans up secrets in repository)")
299301

300302
// Create and setup status command
301303
statusCmd := cli.NewStatusCommand()

‎docs/src/content/docs/tools/cli.md‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ gh aw init # Initialize repository (first-
2525
gh aw add githubnext/agentics/ci-doctor # Add workflow and compile to GitHub Actions
2626
gh aw compile # Recompile to GitHub Actions
2727
gh aw trial githubnext/agentics/ci-doctor # Test workflow safely before adding
28-
gh aw trial ./my-workflow.md # Test local workflow during development
28+
gh aw trial ./my-workflow.md --use-local-secrets # Test local workflow with local API keys
2929
gh aw update # Update all workflows with source field
3030
gh aw status # Check status
3131
gh aw run daily-perf # Execute workflow
@@ -276,6 +276,7 @@ These commands control the execution and state of your compiled agentic workflow
276276
gh aw run WorkflowName # Run single workflow
277277
gh aw run WorkflowName1 WorkflowName2 # Run multiple workflows
278278
gh aw run WorkflowName --repeat 3 # Run 3 times total
279+
gh aw run workflow --use-local-secrets # Use local API keys for execution
279280
gh aw run weekly-research --enable-if-needed --input priority=high
280281
```
281282

@@ -287,6 +288,7 @@ Test workflows safely in a temporary private repository without affecting your t
287288
gh aw trial githubnext/agentics/ci-doctor # Test from source repo
288289
gh aw trial ./my-local-workflow.md # Test local file
289290
gh aw trial workflow1 workflow2 # Compare multiple workflows
291+
gh aw trial ./workflow.md --use-local-secrets # Use local API keys for trial
290292
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
291293
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.
292294
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"
298300
Other flags:
299301
--engine ENGINE # Override engine (default: from frontmatter)
300302
--auto-merge-prs # Auto-merge PRs created during trial
303+
--use-local-secrets # Use local environment API keys (pushes/cleans up secrets)
301304
--repeat N # Repeat N times
302305
--force-delete-host-repo-before # Force delete existing host repo BEFORE start
303306
--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
310313

311314
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.
312315

316+
### Using Local API Keys
317+
318+
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:
319+
320+
```bash
321+
gh aw run my-workflow --use-local-secrets # Use local API keys for run
322+
gh aw trial ./workflow.md --use-local-secrets # Use local API keys for trial
323+
```
324+
325+
**How it works:**
326+
- Reads API keys from environment variables (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `COPILOT_CLI_TOKEN`, etc.)
327+
- Temporarily pushes the required secrets to the repository before workflow execution
328+
- Automatically cleans up (deletes) the secrets after completion
329+
- Only pushes secrets that are actually needed by the workflow's AI engine
330+
331+
**When to use:**
332+
- Testing workflows that require AI engine secrets not yet configured in the repository
333+
- Trial mode when you want to test with your local API keys
334+
- Development environments where you don't want to permanently store secrets
335+
336+
**Security notes:**
337+
- Secrets are only pushed temporarily and are cleaned up automatically
338+
- Use with caution in shared or production repositories
339+
- Consider using repository secrets for permanent deployments
340+
313341
### Workflow State Management
314342

315343
```bash

‎pkg/cli/commands_test.go‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -335,39 +335,39 @@ func TestDisableWorkflowsFailureScenarios(t *testing.T) {
335335

336336
func TestRunWorkflowOnGitHub(t *testing.T) {
337337
// Test with empty workflow name
338-
err := RunWorkflowOnGitHub("", false, "", "", false, false)
338+
err := RunWorkflowOnGitHub("", false, "", "", false, false, false)
339339
if err == nil {
340340
t.Error("RunWorkflowOnGitHub should return error for empty workflow name")
341341
}
342342

343343
// Test with nonexistent workflow (this will fail but gracefully)
344-
err = RunWorkflowOnGitHub("nonexistent-workflow", false, "", "", false, false)
344+
err = RunWorkflowOnGitHub("nonexistent-workflow", false, "", "", false, false, false)
345345
if err == nil {
346346
t.Error("RunWorkflowOnGitHub should return error for non-existent workflow")
347347
}
348348
}
349349

350350
func TestRunWorkflowsOnGitHub(t *testing.T) {
351351
// Test with empty workflow list
352-
err := RunWorkflowsOnGitHub([]string{}, 0, false, "", "", false, false)
352+
err := RunWorkflowsOnGitHub([]string{}, 0, false, "", "", false, false, false)
353353
if err == nil {
354354
t.Error("RunWorkflowsOnGitHub should return error for empty workflow list")
355355
}
356356

357357
// Test with workflow list containing empty name
358-
err = RunWorkflowsOnGitHub([]string{"valid-workflow", ""}, 0, false, "", "", false, false)
358+
err = RunWorkflowsOnGitHub([]string{"valid-workflow", ""}, 0, false, "", "", false, false, false)
359359
if err == nil {
360360
t.Error("RunWorkflowsOnGitHub should return error for workflow list containing empty name")
361361
}
362362

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

369369
// Test with negative repeat seconds (should work as 0)
370-
err = RunWorkflowsOnGitHub([]string{"nonexistent-workflow"}, -1, false, "", "", false, false)
370+
err = RunWorkflowsOnGitHub([]string{"nonexistent-workflow"}, -1, false, "", "", false, false, false)
371371
if err == nil {
372372
t.Error("RunWorkflowsOnGitHub should return error for non-existent workflow regardless of repeat value")
373373
}
@@ -402,12 +402,12 @@ func TestAllCommandsExist(t *testing.T) {
402402
_, err := CompileWorkflows(config)
403403
return err
404404
}, false, "CompileWorkflows"}, // Should compile existing markdown files successfully
405-
{func() error { return RemoveWorkflows("test", false) }, false, "RemoveWorkflows"}, // Should handle missing directory gracefully
406-
{func() error { return StatusWorkflows("test", false, false) }, false, "StatusWorkflows"}, // Should handle missing directory gracefully
407-
{func() error { return EnableWorkflows("test") }, true, "EnableWorkflows"}, // Should now error when no workflows found to enable
408-
{func() error { return DisableWorkflows("test") }, true, "DisableWorkflows"}, // Should now also error when no workflows found to disable
409-
{func() error { return RunWorkflowOnGitHub("", false, "", "", false, false) }, true, "RunWorkflowOnGitHub"}, // Should error with empty workflow name
410-
{func() error { return RunWorkflowsOnGitHub([]string{}, 0, false, "", "", false, false) }, true, "RunWorkflowsOnGitHub"}, // Should error with empty workflow list
405+
{func() error { return RemoveWorkflows("test", false) }, false, "RemoveWorkflows"}, // Should handle missing directory gracefully
406+
{func() error { return StatusWorkflows("test", false, false) }, false, "StatusWorkflows"}, // Should handle missing directory gracefully
407+
{func() error { return EnableWorkflows("test") }, true, "EnableWorkflows"}, // Should now error when no workflows found to enable
408+
{func() error { return DisableWorkflows("test") }, true, "DisableWorkflows"}, // Should now also error when no workflows found to disable
409+
{func() error { return RunWorkflowOnGitHub("", false, "", "", false, false, false) }, true, "RunWorkflowOnGitHub"}, // Should error with empty workflow name
410+
{func() error { return RunWorkflowsOnGitHub([]string{}, 0, false, "", "", false, false, false) }, true, "RunWorkflowsOnGitHub"}, // Should error with empty workflow list
411411
}
412412

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

10471047
func TestRunWorkflowOnGitHubWithEnable(t *testing.T) {
10481048
// Test with enable flag enabled (should not error for basic validation)
1049-
err := RunWorkflowOnGitHub("nonexistent-workflow", true, "", "", false, false)
1049+
err := RunWorkflowOnGitHub("nonexistent-workflow", true, "", "", false, false, false)
10501050
if err == nil {
10511051
t.Error("RunWorkflowOnGitHub should return error for non-existent workflow even with enable flag")
10521052
}
10531053

10541054
// Test with empty workflow name and enable flag
1055-
err = RunWorkflowOnGitHub("", true, "", "", false, false)
1055+
err = RunWorkflowOnGitHub("", true, "", "", false, false, false)
10561056
if err == nil {
10571057
t.Error("RunWorkflowOnGitHub should return error for empty workflow name regardless of enable flag")
10581058
}

‎pkg/cli/run_command.go‎

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ import (
1414
"github.com/githubnext/gh-aw/pkg/constants"
1515
"github.com/githubnext/gh-aw/pkg/logger"
1616
"github.com/githubnext/gh-aw/pkg/parser"
17+
"github.com/githubnext/gh-aw/pkg/workflow"
1718
)
1819

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

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

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

167+
// Handle secret pushing if requested
168+
var secretTracker *TrialSecretTracker
169+
if pushSecrets {
170+
// Determine target repository
171+
var targetRepo string
172+
if repoOverride != "" {
173+
targetRepo = repoOverride
174+
} else {
175+
// Get current repository slug
176+
currentRepo, err := GetCurrentRepoSlug()
177+
if err != nil {
178+
return fmt.Errorf("failed to determine current repository for secret handling: %w", err)
179+
}
180+
targetRepo = currentRepo
181+
}
182+
183+
secretTracker = NewTrialSecretTracker(targetRepo)
184+
runLog.Printf("Created secret tracker for repository: %s", targetRepo)
185+
186+
// Set up secret cleanup to always run on exit
187+
defer func() {
188+
if err := cleanupTrialSecrets(targetRepo, secretTracker, verbose); err != nil {
189+
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to cleanup secrets: %v", err)))
190+
}
191+
}()
192+
193+
// Add GitHub token secret
194+
if err := addGitHubTokenSecret(targetRepo, secretTracker, verbose); err != nil {
195+
return fmt.Errorf("failed to add GitHub token secret: %w", err)
196+
}
197+
198+
// Determine and add engine secrets
199+
if repoOverride == "" && lockFilePath != "" {
200+
// For local workflows, read and parse the workflow to determine engine requirements
201+
workflowMarkdownPath := strings.TrimSuffix(lockFilePath, ".lock.yml") + ".md"
202+
config := CompileConfig{
203+
MarkdownFiles: []string{workflowMarkdownPath},
204+
Verbose: false, // Don't be verbose during secret determination
205+
EngineOverride: engineOverride,
206+
Validate: false,
207+
Watch: false,
208+
WorkflowDir: "",
209+
SkipInstructions: true,
210+
NoEmit: true, // Don't emit files, just compile for analysis
211+
Purge: false,
212+
TrialMode: false,
213+
TrialLogicalRepoSlug: "",
214+
Strict: false,
215+
}
216+
workflowDataList, err := CompileWorkflows(config)
217+
if err == nil && len(workflowDataList) == 1 {
218+
workflowData := workflowDataList[0]
219+
if err := determineAndAddEngineSecret(workflowData.EngineConfig, targetRepo, secretTracker, engineOverride, verbose); err != nil {
220+
// Log warning but don't fail - the workflow might still run without secrets
221+
if verbose {
222+
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to determine engine secret: %v", err)))
223+
}
224+
}
225+
} else if verbose {
226+
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Failed to compile workflow for secret determination - continuing without engine secrets"))
227+
}
228+
} else if repoOverride != "" {
229+
// For remote workflows, we can't analyze the workflow file, so create a minimal EngineConfig
230+
// with engine information and reuse the existing determineAndAddEngineSecret function
231+
var engineType string
232+
if engineOverride != "" {
233+
engineType = engineOverride
234+
} else {
235+
engineType = "copilot" // Default engine
236+
if verbose {
237+
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Using default Copilot engine for remote workflow secret handling"))
238+
}
239+
}
240+
241+
// Create minimal EngineConfig with engine information
242+
engineConfig := &workflow.EngineConfig{
243+
ID: engineType,
244+
}
245+
246+
if err := determineAndAddEngineSecret(engineConfig, targetRepo, secretTracker, engineOverride, verbose); err != nil {
247+
// Log warning but don't fail - the workflow might still run without secrets
248+
if verbose {
249+
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to determine engine secret for remote workflow: %v", err)))
250+
}
251+
}
252+
}
253+
}
254+
166255
// Build the gh workflow run command with optional repo override
167256
args := []string{"workflow", "run", lockFileName}
168257
if repoOverride != "" {
@@ -260,7 +349,7 @@ func RunWorkflowOnGitHub(workflowIdOrName string, enable bool, engineOverride st
260349
}
261350

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

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

0 commit comments

Comments
 (0)