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
75 changes: 74 additions & 1 deletion actions/setup/sh/validate_multi_secret.sh
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,74 @@ if [ "$all_empty" = true ]; then
exit 1
fi

# Validate COPILOT_GITHUB_TOKEN is a fine-grained PAT if it's one of the secrets being validated
for secret_name in "${SECRET_NAMES[@]}"; do
if [ "$secret_name" = "COPILOT_GITHUB_TOKEN" ]; then
secret_value="${!secret_name}"
if [ -n "$secret_value" ]; then
# Check token type by prefix
# github_pat_ = Fine-grained PAT (valid)
# ghp_ = Classic PAT (invalid)
# gho_ = OAuth token (invalid)
if [[ "$secret_value" == ghp_* ]]; then
{
echo "❌ Error: COPILOT_GITHUB_TOKEN is a classic Personal Access Token (ghp_...)"
echo "Classic PATs are not supported for GitHub Copilot."
echo "Please create a fine-grained PAT (github_pat_...) at:"
echo "https://github.com/settings/personal-access-tokens/new"
echo ""
echo "Configure the token with:"
echo "• Resource owner: Your personal account"
echo "• Repository access: \"Public repositories\""
echo "• Account permissions → Copilot Requests: Read-only"
} >> "$GITHUB_STEP_SUMMARY"

echo "Error: COPILOT_GITHUB_TOKEN is a classic Personal Access Token (ghp_...)" >&2
echo "Classic PATs are not supported for GitHub Copilot." >&2
echo "Please create a fine-grained PAT (github_pat_...) at: https://github.com/settings/personal-access-tokens/new" >&2

if [ -n "$GITHUB_OUTPUT" ]; then
echo "verification_result=failed" >> "$GITHUB_OUTPUT"
fi
exit 1
elif [[ "$secret_value" == gho_* ]]; then
{
echo "❌ Error: COPILOT_GITHUB_TOKEN is an OAuth token (gho_...)"
echo "OAuth tokens are not supported for GitHub Copilot."
echo "Please create a fine-grained PAT (github_pat_...) at:"
echo "https://github.com/settings/personal-access-tokens/new"
} >> "$GITHUB_STEP_SUMMARY"

echo "Error: COPILOT_GITHUB_TOKEN is an OAuth token (gho_...)" >&2
echo "OAuth tokens are not supported for GitHub Copilot." >&2
echo "Please create a fine-grained PAT (github_pat_...) at: https://github.com/settings/personal-access-tokens/new" >&2

if [ -n "$GITHUB_OUTPUT" ]; then
echo "verification_result=failed" >> "$GITHUB_OUTPUT"
fi
exit 1
elif [[ "$secret_value" != github_pat_* ]]; then
{
echo "❌ Error: COPILOT_GITHUB_TOKEN has an unrecognized format"
echo "GitHub Copilot requires a fine-grained PAT (starting with 'github_pat_')."
echo "Please create a fine-grained PAT at:"
echo "https://github.com/settings/personal-access-tokens/new"
} >> "$GITHUB_STEP_SUMMARY"

echo "Error: COPILOT_GITHUB_TOKEN has an unrecognized format" >&2
echo "GitHub Copilot requires a fine-grained PAT (starting with 'github_pat_')." >&2
echo "Please create a fine-grained PAT at: https://github.com/settings/personal-access-tokens/new" >&2

if [ -n "$GITHUB_OUTPUT" ]; then
echo "verification_result=failed" >> "$GITHUB_OUTPUT"
fi
exit 1
fi
fi
break
fi
done

# Log success in collapsible section
echo "<details>"
echo "<summary>Agent Environment Validation</summary>"
Expand All @@ -97,7 +165,12 @@ echo ""
first_secret="${SECRET_NAMES[0]}"
first_value="${!first_secret}"
if [ -n "$first_value" ]; then
echo "✅ $first_secret: Configured"
# Show extra info for COPILOT_GITHUB_TOKEN indicating fine-grained PAT
if [ "$first_secret" = "COPILOT_GITHUB_TOKEN" ]; then
echo "✅ $first_secret: Configured (fine-grained PAT)"
else
echo "✅ $first_secret: Configured"
fi
# Middle secrets use elif (if there are more than 2 secrets)
elif [ "${#SECRET_NAMES[@]}" -gt 2 ]; then
found=false
Expand Down
17 changes: 13 additions & 4 deletions cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,6 @@ Examples:
repoOverride, _ := cmd.Flags().GetString("repo")
refOverride, _ := cmd.Flags().GetString("ref")
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")
dryRun, _ := cmd.Flags().GetBool("dry-run")
Expand All @@ -395,10 +394,21 @@ Examples:
return fmt.Errorf("workflow inputs cannot be specified in interactive mode (they will be collected interactively)")
}

return cli.RunWorkflowInteractively(cmd.Context(), verboseFlag, repoOverride, refOverride, autoMergePRs, pushSecrets, push, engineOverride, dryRun)
return cli.RunWorkflowInteractively(cmd.Context(), verboseFlag, repoOverride, refOverride, autoMergePRs, push, engineOverride, dryRun)
}

return cli.RunWorkflowsOnGitHub(cmd.Context(), args, repeatCount, enable, engineOverride, repoOverride, refOverride, autoMergePRs, pushSecrets, push, inputs, verboseFlag, dryRun)
return cli.RunWorkflowsOnGitHub(cmd.Context(), args, cli.RunOptions{
RepeatCount: repeatCount,
Enable: enable,
EngineOverride: engineOverride,
RepoOverride: repoOverride,
RefOverride: refOverride,
AutoMergePRs: autoMergePRs,
Push: push,
Inputs: inputs,
Verbose: verboseFlag,
DryRun: dryRun,
})
},
}

Expand Down Expand Up @@ -577,7 +587,6 @@ Use "` + string(constants.CLIExtensionPrefix) + ` help all" to show help for all
runCmd.Flags().StringP("repo", "r", "", "Target repository (owner/repo format). Defaults to current repository")
runCmd.Flags().String("ref", "", "Branch or tag name to run the workflow on (default: current branch)")
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")
runCmd.Flags().Bool("dry-run", false, "Validate workflow without actually triggering execution on GitHub Actions")
Expand Down
1 change: 0 additions & 1 deletion docs/interactive-run-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ All standard `run` command flags work in interactive mode:
- `--ref branch` - Run on a specific branch
- `--engine copilot` - Override AI engine
- `--auto-merge-prs` - Auto-merge created PRs
- `--use-local-secrets` - Use local secrets
- `--push` - Push changes before running

## Limitations
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/patterns/trialops.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ jobs:
|-------|----------|
| `workflow not found` | Use correct format: `owner/repo/workflow-name`, `owner/repo/.github/workflows/workflow.md`, or `./local-workflow.md` |
| `workflow_dispatch not supported` | Add `workflow_dispatch:` to workflow frontmatter `on:` section |
| `authentication failed` | Set API keys: `COPILOT_GITHUB_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`. Use `--use-local-secrets` to push to trial repo |
| `authentication failed` | Set API keys: `COPILOT_GITHUB_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN` or `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`. Trial automatically prompts for missing secrets and uploads them to the trial repo |
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

The troubleshooting guidance says trials automatically prompt for missing secrets and upload them, but in code this only happens when an explicit engine override is provided. Update the doc to match behavior, or ensure secrets for the engine configured in workflow frontmatter/default even when --engine is not used.

Copilot uses AI. Check for mistakes.
| `failed to create trial repository` | Check `gh auth status`, verify quota with `gh api user \| jq .plan`, try explicit `--host-repo name` |
| `execution timed out` | Increase with `--timeout 60` (minutes, default: 30) |
| No issues/PRs created | Configure `safe-outputs` in workflow frontmatter, check Actions logs for errors |
Expand Down
8 changes: 4 additions & 4 deletions docs/src/content/docs/setup/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,13 +233,14 @@ Test workflows in temporary private repositories (default) or run directly in sp

```bash wrap
gh aw trial githubnext/agentics/ci-doctor # Test remote workflow
gh aw trial ./workflow.md --use-local-secrets # Test with local API keys
gh aw trial ./workflow.md --logical-repo owner/repo # Act as different repo
gh aw trial ./workflow.md --repo owner/repo # Run directly in repository
gh aw trial ./workflow.md --dry-run # Preview without executing
```

**Options:** `-e`, `--engine`, `--auto-merge-prs`, `--repeat`, `--delete-host-repo-after`, `--use-local-secrets`, `--logical-repo`, `--clone-repo`, `--trigger-context`, `--repo`, `--dry-run`
**Options:** `-e`, `--engine`, `--auto-merge-prs`, `--repeat`, `--delete-host-repo-after`, `--logical-repo`, `--clone-repo`, `--trigger-context`, `--repo`, `--dry-run`

**Secret Handling:** API keys required for the selected engine are automatically checked. If missing from the target repository, they are prompted for interactively and uploaded.
Comment on lines +241 to +243
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

This documentation claims trial will automatically prompt for missing secrets and upload them, but the current implementation only calls secret prompting/upload when --engine (engine override) is provided. Either update the docs to reflect the actual behavior, or expand the trial flow to detect the workflow’s configured engine(s) and ensure their secrets even without an override.

Copilot uses AI. Check for mistakes.

#### `run`

Expand All @@ -249,12 +250,11 @@ Execute workflows immediately in GitHub Actions. Displays workflow URL for track
gh aw run workflow # Run workflow
gh aw run workflow1 workflow2 # Run multiple workflows
gh aw run workflow --repeat 3 # Repeat 3 times
gh aw run workflow --use-local-secrets # Use local API keys
gh aw run workflow --push # Auto-commit, push, and dispatch workflow
gh aw run workflow --push --ref main # Push to specific branch
```

**Options:** `--repeat`, `--use-local-secrets`, `--push` (see [--push flag](#the---push-flag)), `--ref`
**Options:** `--repeat`, `--push` (see [--push flag](#the---push-flag)), `--ref`, `--auto-merge-prs`, `--enable-if-needed`

When `--push` is used, automatically recompiles outdated `.lock.yml` files, stages all transitive imports, and triggers workflow run after successful push. Without `--push`, warnings are displayed for missing or outdated lock files.

Expand Down
29 changes: 17 additions & 12 deletions pkg/cli/add_interactive_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/charmbracelet/huh"
"github.com/github/gh-aw/pkg/console"
"github.com/github/gh-aw/pkg/constants"
"github.com/github/gh-aw/pkg/parser"
"github.com/github/gh-aw/pkg/stringutil"
)

// selectAIEngineAndKey prompts the user to select an AI engine and provide API key
Expand Down Expand Up @@ -63,10 +63,6 @@ func (c *AddInteractiveConfig) selectAIEngineAndKey() error {
break
}
}
// Priority 3: Check if user likely has Copilot (default)
if token, err := parser.GetGitHubToken(); err == nil && token != "" {
defaultEngine = string(constants.CopilotEngine)
}
}
}

Expand Down Expand Up @@ -150,12 +146,20 @@ func (c *AddInteractiveConfig) collectCopilotPAT() error {
// Check if COPILOT_GITHUB_TOKEN is already in environment
existingToken := os.Getenv("COPILOT_GITHUB_TOKEN")
if existingToken != "" {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Found COPILOT_GITHUB_TOKEN in environment"))
return nil
// Validate the existing token is a fine-grained PAT
if err := stringutil.ValidateCopilotPAT(existingToken); err != nil {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("COPILOT_GITHUB_TOKEN in environment is not a fine-grained PAT: %s", stringutil.GetPATTypeDescription(existingToken))))
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
// Continue to prompt for a new token
} else {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Found valid fine-grained COPILOT_GITHUB_TOKEN in environment"))
return nil
}
}

fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "GitHub Copilot requires a Personal Access Token (PAT) with Copilot permissions.")
fmt.Fprintln(os.Stderr, "GitHub Copilot requires a fine-grained Personal Access Token (PAT) with Copilot permissions.")
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Classic PATs (ghp_...) are not supported. You must use a fine-grained PAT (github_pat_...)."))
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Please create a token at:")
fmt.Fprintln(os.Stderr, console.FormatCommandMessage(" https://github.com/settings/personal-access-tokens/new"))
Expand All @@ -172,15 +176,16 @@ func (c *AddInteractiveConfig) collectCopilotPAT() error {
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("After creating, please paste your Copilot PAT:").
Description("The token will be stored securely as a repository secret").
Title("After creating, please paste your fine-grained Copilot PAT:").
Description("Must start with 'github_pat_'. Classic PATs (ghp_...) are not supported.").
EchoMode(huh.EchoModePassword).
Value(&token).
Validate(func(s string) error {
if len(s) < 10 {
return fmt.Errorf("token appears to be too short")
}
return nil
// Validate it's a fine-grained PAT
return stringutil.ValidateCopilotPAT(s)
}),
),
).WithAccessible(console.IsAccessibleMode())
Expand All @@ -191,7 +196,7 @@ func (c *AddInteractiveConfig) collectCopilotPAT() error {

// Store in environment for later use
_ = os.Setenv("COPILOT_GITHUB_TOKEN", token)
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Copilot token received"))
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Valid fine-grained Copilot token received"))

return nil
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/add_interactive_workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func (c *AddInteractiveConfig) checkStatusAndOfferRun(ctx context.Context) error
if parsed != nil {
fmt.Fprintln(os.Stderr, "")

if err := RunSpecificWorkflowInteractively(ctx, parsed.WorkflowName, c.Verbose, c.EngineOverride, c.RepoOverride, "", false, false, false, false); err != nil {
if err := RunSpecificWorkflowInteractively(ctx, parsed.WorkflowName, c.Verbose, c.EngineOverride, c.RepoOverride, "", false, false, false); err != nil {
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Failed to run workflow: %v", err)))
c.showFinalInstructions()
return nil
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 @@ -339,39 +339,39 @@ func TestDisableWorkflowsFailureScenarios(t *testing.T) {

func TestRunWorkflowOnGitHub(t *testing.T) {
// Test with empty workflow name
err := RunWorkflowOnGitHub(context.Background(), "", false, "", "", "", false, false, false, false, []string{}, false, false)
err := RunWorkflowOnGitHub(context.Background(), "", RunOptions{})
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, false, []string{}, false, false)
err = RunWorkflowOnGitHub(context.Background(), "nonexistent-workflow", RunOptions{})
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, false, []string{}, false, false)
err := RunWorkflowsOnGitHub(context.Background(), []string{}, RunOptions{})
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, false, []string{}, false, false)
err = RunWorkflowsOnGitHub(context.Background(), []string{"valid-workflow", ""}, RunOptions{})
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, false, []string{}, false, false)
err = RunWorkflowsOnGitHub(context.Background(), []string{"nonexistent-workflow1", "nonexistent-workflow2"}, RunOptions{})
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, false, []string{}, false, false)
err = RunWorkflowsOnGitHub(context.Background(), []string{"nonexistent-workflow"}, RunOptions{RepeatCount: -1})
if err == nil {
t.Error("RunWorkflowsOnGitHub should return error for non-existent workflow regardless of repeat value")
}
Expand Down Expand Up @@ -482,10 +482,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, false, []string{}, false, false)
return RunWorkflowOnGitHub(context.Background(), "", RunOptions{})
}, true, "RunWorkflowOnGitHub"}, // Should error with empty workflow name
{func() error {
return RunWorkflowsOnGitHub(context.Background(), []string{}, 0, false, "", "", "", false, false, false, []string{}, false, false)
return RunWorkflowsOnGitHub(context.Background(), []string{}, RunOptions{})
}, true, "RunWorkflowsOnGitHub"}, // Should error with empty workflow list
}

Expand Down Expand Up @@ -1134,13 +1134,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, false, []string{}, false, false)
err := RunWorkflowOnGitHub(context.Background(), "nonexistent-workflow", RunOptions{Enable: true})
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, false, []string{}, false, false)
err = RunWorkflowOnGitHub(context.Background(), "", RunOptions{Enable: true})
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 @@ -17,7 +17,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, false, []string{}, false, false)
err := RunWorkflowOnGitHub(ctx, "test-workflow", RunOptions{})

// Should return context.Canceled error
assert.ErrorIs(t, err, context.Canceled, "Should return context.Canceled error when context is cancelled")
Expand All @@ -30,7 +30,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, false, []string{}, false, false)
err := RunWorkflowsOnGitHub(ctx, []string{"test-workflow"}, RunOptions{})

// Should return context.Canceled error
assert.ErrorIs(t, err, context.Canceled, "Should return context.Canceled error when context is cancelled")
Expand Down Expand Up @@ -98,7 +98,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, false, []string{}, false, false)
err := RunWorkflowsOnGitHub(ctx, []string{"nonexistent-workflow-1", "nonexistent-workflow-2"}, RunOptions{})

// Should return an error (either validation error or context error)
assert.Error(t, err, "Should return an error")
Expand Down
Loading
Loading