Skip to content
Closed
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: 2 additions & 2 deletions docs/src/content/docs/setup/quick-start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,13 @@ The wizard generates a compiled workflow file (`.lock.yml`) automatically — yo
> **Setting up `COPILOT_GITHUB_TOKEN`?**
> 1. [Create a fine-grained Personal Access Token (PAT)](https://github.com/settings/personal-access-tokens/new) under your user account.
> 2. Under **Permissions → Account permissions**, set **Copilot Requests** to **Read**, then generate the token.
> 3. Add it as a repository secret from your repository root with `gh secret set COPILOT_GITHUB_TOKEN < /path/to/token.txt`, or use the GitHub UI. See [Authentication](/gh-aw/reference/auth/#copilot_github_token) for more detail.
> 3. Add it as a repository secret from your repository root with `gh aw secrets set COPILOT_GITHUB_TOKEN --value "YOUR_COPILOT_PAT"`, or use the GitHub UI. See [Authentication](/gh-aw/reference/auth/#copilot_github_token) for more detail.
>

> [!NOTE]
> **Setting up `ANTHROPIC_API_KEY`?**
> 1. Create an API key in [Anthropic Console](https://console.anthropic.com/settings/keys).
> 2. Add it as a repository secret from your repository root with `gh secret set ANTHROPIC_API_KEY < /path/to/key.txt`, or use the GitHub UI. See [Authentication](/gh-aw/reference/auth/#anthropic_api_key) for more detail.
> 2. Add it as a repository secret from your repository root with `gh aw secrets set ANTHROPIC_API_KEY --value "YOUR_ANTHROPIC_API_KEY"`, or use the GitHub UI. See [Authentication](/gh-aw/reference/auth/#anthropic_api_key) for more detail.
>

> [!TIP]
Expand Down
64 changes: 64 additions & 0 deletions pkg/cli/claude_oauth_token_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package cli

import (
"fmt"
"os"
"strings"

"github.com/github/gh-aw/pkg/constants"
"github.com/github/gh-aw/pkg/parser"
"github.com/github/gh-aw/pkg/workflow"
)

const claudeCodeOAuthTokenEnvVar = "CLAUDE_CODE_OAUTH_TOKEN"

func validateUnsupportedClaudeOAuthTokenForEngine(engine string) error {
if strings.TrimSpace(os.Getenv(claudeCodeOAuthTokenEnvVar)) == "" {
return nil
}
if strings.EqualFold(strings.TrimSpace(engine), string(constants.ClaudeEngine)) {
return fmt.Errorf("%s is not supported for Claude workflows - set ANTHROPIC_API_KEY instead", claudeCodeOAuthTokenEnvVar)
}
return nil
}

func validateUnsupportedClaudeOAuthTokenForWorkflowFiles(workflowFiles []string, engineOverride string) error {
if err := validateUnsupportedClaudeOAuthTokenForEngine(engineOverride); err != nil {
return err
}
if strings.TrimSpace(os.Getenv(claudeCodeOAuthTokenEnvVar)) == "" {
return nil
}
for _, workflowFile := range workflowFiles {
usesClaude, err := workflowUsesClaudeEngine(workflowFile)
if err != nil {
return fmt.Errorf("failed to inspect workflow %s for engine configuration: %w", workflowFile, err)
}
if usesClaude {
return fmt.Errorf("%s is not supported for Claude workflows - set ANTHROPIC_API_KEY instead", claudeCodeOAuthTokenEnvVar)
}
}
return nil
}

func workflowUsesClaudeEngine(workflowFile string) (bool, error) {
content, err := readWorkflowFileContent(workflowFile)
if err != nil {
return false, err
}
result, err := parser.ExtractFrontmatterFromContent(content)
if err != nil {
return false, err
}

compiler := &workflow.Compiler{}
engineSetting, engineConfig := compiler.ExtractEngineConfig(result.Frontmatter)

engine := string(constants.CopilotEngine)
if engineConfig != nil && engineConfig.ID != "" {
engine = engineConfig.ID
} else if engineSetting != "" {
engine = engineSetting
}
return strings.EqualFold(engine, string(constants.ClaudeEngine)), nil
}
48 changes: 48 additions & 0 deletions pkg/cli/claude_oauth_token_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package cli

import (
"os"
"path/filepath"
"strings"
"testing"
)

func TestValidateUnsupportedClaudeOAuthTokenForEngine(t *testing.T) {
t.Setenv(claudeCodeOAuthTokenEnvVar, "")
if err := validateUnsupportedClaudeOAuthTokenForEngine("claude"); err != nil {
t.Fatalf("expected no error when token is unset, got %v", err)
}

t.Setenv(claudeCodeOAuthTokenEnvVar, "gho_test")
if err := validateUnsupportedClaudeOAuthTokenForEngine("copilot"); err != nil {
t.Fatalf("expected no error for non-claude engine, got %v", err)
}
if err := validateUnsupportedClaudeOAuthTokenForEngine("claude"); err == nil || !strings.Contains(err.Error(), "set ANTHROPIC_API_KEY instead") {
t.Fatalf("expected guidance error for claude engine, got %v", err)
}
}

func TestValidateUnsupportedClaudeOAuthTokenForWorkflowFiles(t *testing.T) {
t.Setenv(claudeCodeOAuthTokenEnvVar, "gho_test")

tempDir := t.TempDir()
copilotWorkflow := filepath.Join(tempDir, "copilot.md")
claudeWorkflow := filepath.Join(tempDir, "claude.md")

if err := os.WriteFile(copilotWorkflow, []byte("---\nengine: copilot\n---\n"), 0o644); err != nil {
t.Fatalf("failed to write copilot workflow: %v", err)
}
if err := os.WriteFile(claudeWorkflow, []byte("---\nengine: claude\n---\n"), 0o644); err != nil {
t.Fatalf("failed to write claude workflow: %v", err)
}

if err := validateUnsupportedClaudeOAuthTokenForWorkflowFiles([]string{copilotWorkflow}, ""); err != nil {
t.Fatalf("expected no error for non-claude workflow, got %v", err)
}
if err := validateUnsupportedClaudeOAuthTokenForWorkflowFiles([]string{claudeWorkflow}, ""); err == nil || !strings.Contains(err.Error(), claudeCodeOAuthTokenEnvVar) {
t.Fatalf("expected unsupported token error for claude workflow, got %v", err)
}
if err := validateUnsupportedClaudeOAuthTokenForWorkflowFiles([]string{filepath.Join(tempDir, "missing.md")}, ""); err == nil || !strings.Contains(err.Error(), "failed to inspect workflow") {
t.Fatalf("expected inspection error for missing workflow file, got %v", err)
}
}
6 changes: 6 additions & 0 deletions pkg/cli/compile_pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ func compileSpecificFiles(
continue
}
compileOrchestrationLog.Printf("Resolved to: %s", resolvedFile)
if err := validateUnsupportedClaudeOAuthTokenForWorkflowFiles([]string{resolvedFile}, config.EngineOverride); err != nil {
return workflowDataList, err
}

// Update result with resolved file name
result.Workflow = filepath.Base(resolvedFile)
Expand Down Expand Up @@ -276,6 +279,9 @@ func compileAllFilesInDirectory(
if len(mdFiles) == 0 {
return nil, fmt.Errorf("no workflow markdown files found in %s (workflow files must start with a frontmatter opener on the first line)", workflowsDir)
}
if err := validateUnsupportedClaudeOAuthTokenForWorkflowFiles(mdFiles, config.EngineOverride); err != nil {
return nil, err
}

compileOrchestrationLog.Printf("Found %d markdown files to compile", len(mdFiles))
if config.Verbose {
Expand Down
6 changes: 6 additions & 0 deletions pkg/cli/run_workflow_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ func RunWorkflowOnGitHub(ctx context.Context, workflowIdOrName string, opts RunO
if workflowIdOrName == "" {
return errors.New("workflow name or ID is required")
}
if err := validateUnsupportedClaudeOAuthTokenForEngine(opts.EngineOverride); err != nil {
return err
}

// Validate input format early before attempting workflow validation
for _, input := range opts.Inputs {
Expand Down Expand Up @@ -108,6 +111,9 @@ func RunWorkflowOnGitHub(ctx context.Context, workflowIdOrName string, opts RunO
// Return error directly without wrapping - it already contains formatted message with suggestions
return err
}
if err := validateUnsupportedClaudeOAuthTokenForWorkflowFiles([]string{workflowFile}, opts.EngineOverride); err != nil {
return err
}

// Check if the workflow is runnable (has workflow_dispatch trigger)
runnable, err := IsRunnable(workflowFile)
Expand Down