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
26 changes: 26 additions & 0 deletions cli/azd/pkg/templates/gh_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
93 changes: 93 additions & 0 deletions cli/azd/pkg/templates/gh_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
Loading