diff --git a/cli/azd/pkg/templates/gh_source.go b/cli/azd/pkg/templates/gh_source.go index e9232ea9899..d946d9deffc 100644 --- a/cli/azd/pkg/templates/gh_source.go +++ b/cli/azd/pkg/templates/gh_source.go @@ -59,6 +59,11 @@ func ParseGitHubUrl(ctx context.Context, urlArg string, ghCli *github.Cli) (*Git hostname = "github.com" } + // Ensure gh is authenticated before trying to resolve the branch + if err := ensureGitHubAuthenticated(ctx, ghCli, hostname); err != nil { + return nil, err + } + // Resolve the actual branch by checking with GitHub API branch, filePath, err = resolveBranchAndPath(ctx, ghCli, hostname, repoSlug, branchAndPath) if err != nil { @@ -100,6 +105,11 @@ func ParseGitHubUrl(ctx context.Context, urlArg string, ghCli *github.Cli) (*Git branchAndPath = strings.Join(pathParts[3:], "/") } + // Ensure gh is authenticated before trying to resolve the branch + if err := ensureGitHubAuthenticated(ctx, ghCli, hostname); err != nil { + return nil, err + } + // Resolve the actual branch by checking with GitHub API branch, filePath, err = resolveBranchAndPath(ctx, ghCli, hostname, repoSlug, branchAndPath) if err != nil { @@ -173,6 +183,22 @@ func branchExists(ctx context.Context, ghCli *github.Cli, hostname string, repoS return err == nil } +// ensureGitHubAuthenticated checks if the user is authenticated to GitHub and initiates login if not. +// This ensures that subsequent GitHub API calls will not fail due to authentication issues. +func ensureGitHubAuthenticated(ctx context.Context, ghCli *github.Cli, hostname string) error { + authResult, err := ghCli.GetAuthStatus(ctx, hostname) + if err != nil { + return fmt.Errorf("failed to get auth status: %w", err) + } + if !authResult.LoggedIn { + err := ghCli.Login(ctx, hostname) + if err != nil { + return fmt.Errorf("failed to login: %w", err) + } + } + return nil +} + // newGhTemplateSource creates a new template source from a Github repository. func newGhTemplateSource( ctx context.Context, name string, urlArg string, ghCli *github.Cli, console input.Console) (Source, error) { diff --git a/cli/azd/pkg/templates/gh_source_test.go b/cli/azd/pkg/templates/gh_source_test.go index 3abdd7610cd..3fc4544cb1e 100644 --- a/cli/azd/pkg/templates/gh_source_test.go +++ b/cli/azd/pkg/templates/gh_source_test.go @@ -366,6 +366,13 @@ func Test_ParseGitHubUrl_RawUrl(t *testing.T) { Stdout: github.Version.String(), }) + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains( + command, string(filepath.Separator)+"gh") && args.Args[0] == "auth" && args.Args[1] == "status" + }).Respond(exec.RunResult{ + Stdout: "Logged in to", + }) + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { return strings.Contains(command, string(filepath.Separator)+"gh") && args.Args[0] == "api" }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { @@ -405,6 +412,13 @@ func Test_ParseGitHubUrl_BlobUrl(t *testing.T) { Stdout: github.Version.String(), }) + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains( + command, string(filepath.Separator)+"gh") && args.Args[0] == "auth" && args.Args[1] == "status" + }).Respond(exec.RunResult{ + Stdout: "Logged in to", + }) + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { return strings.Contains(command, string(filepath.Separator)+"gh") && args.Args[0] == "api" }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { @@ -439,6 +453,13 @@ func Test_ParseGitHubUrl_TreeUrl(t *testing.T) { Stdout: github.Version.String(), }) + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains( + command, string(filepath.Separator)+"gh") && args.Args[0] == "auth" && args.Args[1] == "status" + }).Respond(exec.RunResult{ + Stdout: "Logged in to", + }) + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { return strings.Contains(command, string(filepath.Separator)+"gh") && args.Args[0] == "api" }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { @@ -503,6 +524,13 @@ func Test_ParseGitHubUrl_BranchWithSlashes(t *testing.T) { Stdout: github.Version.String(), }) + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains( + command, string(filepath.Separator)+"gh") && args.Args[0] == "auth" && args.Args[1] == "status" + }).Respond(exec.RunResult{ + Stdout: "Logged in to", + }) + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { return strings.Contains(command, string(filepath.Separator)+"gh") && args.Args[0] == "api" }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { @@ -547,6 +575,13 @@ func Test_ParseGitHubUrl_EnterpriseUrl(t *testing.T) { Stdout: github.Version.String(), }) + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains( + command, string(filepath.Separator)+"gh") && args.Args[0] == "auth" && args.Args[1] == "status" + }).Respond(exec.RunResult{ + Stdout: "Logged in to", + }) + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { return strings.Contains(command, string(filepath.Separator)+"gh") && args.Args[0] == "api" }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { @@ -606,3 +641,61 @@ func Test_ParseGitHubUrl_InvalidUrl(t *testing.T) { }) } } + +// Test_ParseGitHubUrl_NotAuthenticated tests that ParseGitHubUrl properly handles unauthenticated scenarios +func Test_ParseGitHubUrl_NotAuthenticated(t *testing.T) { + mockContext := mocks.NewMockContext(context.Background()) + + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, string(filepath.Separator)+"gh") && args.Args[0] == "--version" + }).Respond(exec.RunResult{ + Stdout: github.Version.String(), + }) + + // Simulate not authenticated scenario + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains( + command, string(filepath.Separator)+"gh") && args.Args[0] == "auth" && args.Args[1] == "status" + }).Respond(exec.RunResult{ + Stdout: "", + Stderr: "To get started with GitHub CLI, please run: gh auth login", + ExitCode: 1, + }) + + // Mock the login call + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains( + command, string(filepath.Separator)+"gh") && args.Args[0] == "auth" && args.Args[1] == "login" + }).Respond(exec.RunResult{ + Stdout: "✓ Logged in as user", + }) + + // After login, branch API calls should succeed + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, string(filepath.Separator)+"gh") && args.Args[0] == "api" + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + apiURL := args.Args[1] + if strings.Contains(apiURL, "/branches/") { + if strings.HasSuffix(apiURL, "/branches/main") { + return exec.RunResult{Stdout: `{"name":"main"}`}, nil + } + return exec.RunResult{Stdout: "", Stderr: "Not Found", ExitCode: 404}, fmt.Errorf("not found") + } + return exec.RunResult{Stdout: ""}, fmt.Errorf("unexpected API call") + }) + + ghCli, err := github.NewGitHubCli(*mockContext.Context, mockContext.Console, mockContext.CommandRunner) + require.NoError(t, err) + + // This should trigger authentication before attempting to resolve the branch + urlInfo, err := ParseGitHubUrl( + *mockContext.Context, + "https://github.com/owner/repo/blob/main/path/to/file.yaml", + ghCli, + ) + require.NoError(t, err) + require.Equal(t, "github.com", urlInfo.Hostname) + require.Equal(t, "owner/repo", urlInfo.RepoSlug) + require.Equal(t, "main", urlInfo.Branch) + require.Equal(t, "path/to/file.yaml", urlInfo.FilePath) +}