diff --git a/cli/azd/pkg/pipeline/github_provider.go b/cli/azd/pkg/pipeline/github_provider.go index c8942c5b3cd..61b3e4b11d4 100644 --- a/cli/azd/pkg/pipeline/github_provider.go +++ b/cli/azd/pkg/pipeline/github_provider.go @@ -10,7 +10,6 @@ import ( "fmt" "io/fs" "maps" - "net/url" "path/filepath" "regexp" "slices" @@ -141,6 +140,10 @@ var gitHubRemoteHttpsUrlRegex = regexp.MustCompile(`^https://(?:www\.)?[a-zA-Z0- // ErrRemoteHostIsNotGitHub the error used when a non GitHub remote is found var ErrRemoteHostIsNotGitHub = errors.New("not a github host") +// credentialNameSanitizer replaces characters not allowed in federated identity credential names. +// Credential names only allow letters (A-Z, a-z), numbers, hyphens, and underscores. +var credentialNameSanitizer = regexp.MustCompile(`[^A-Za-z0-9\-_]`) + // gitRepoDetails extracts the information from a GitHub remote url into general scm concepts // like owner, name and path func (p *GitHubScmProvider) gitRepoDetails(ctx context.Context, remoteUrl string) (*gitRepositoryDetails, error) { @@ -385,11 +388,11 @@ func (p *GitHubCiProvider) credentialOptions( } repoSlug := repoDetails.owner + "/" + repoDetails.repoName - credentialSafeName := strings.ReplaceAll(repoSlug, "/", "-") + credentialSafeName := credentialNameSanitizer.ReplaceAllString(repoSlug, "-") federatedCredentials := []*graphsdk.FederatedIdentityCredential{ { - Name: url.PathEscape(fmt.Sprintf("%s-pull_request", credentialSafeName)), + Name: fmt.Sprintf("%s-pull_request", credentialSafeName), Issuer: federatedIdentityIssuer, Subject: fmt.Sprintf("repo:%s:pull_request", repoSlug), Description: to.Ptr("Created by Azure Developer CLI"), @@ -398,9 +401,9 @@ func (p *GitHubCiProvider) credentialOptions( } for _, branch := range branches { - safeBranchName := regexp.MustCompile(`[^A-Za-z0-9-]`).ReplaceAllString(branch, "-") + safeBranchName := credentialNameSanitizer.ReplaceAllString(branch, "-") branchCredentials := &graphsdk.FederatedIdentityCredential{ - Name: url.PathEscape(fmt.Sprintf("%s-%s", credentialSafeName, safeBranchName)), + Name: fmt.Sprintf("%s-%s", credentialSafeName, safeBranchName), Issuer: federatedIdentityIssuer, Subject: fmt.Sprintf("repo:%s:ref:refs/heads/%s", repoSlug, branch), Description: to.Ptr("Created by Azure Developer CLI"), diff --git a/cli/azd/pkg/pipeline/github_provider_test.go b/cli/azd/pkg/pipeline/github_provider_test.go index 43b37626fe2..ff1065c1625 100644 --- a/cli/azd/pkg/pipeline/github_provider_test.go +++ b/cli/azd/pkg/pipeline/github_provider_test.go @@ -118,3 +118,24 @@ func setupGithubCliMocks(mockContext *mocks.MockContext) { return exec.NewRunResult(0, fmt.Sprintf("gh version %s", github.Version), ""), nil }) } + +func Test_credentialNameSanitizer(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {"simple repo slug", "Azure/azure-dev", "Azure-azure-dev"}, + {"repo with dots", "my-org/my.repo.name", "my-org-my-repo-name"}, + {"repo with multiple special chars", "org/repo@v2.0", "org-repo-v2-0"}, + {"already safe", "my-org-my-repo", "my-org-my-repo"}, + {"underscores preserved", "org/my_repo", "org-my_repo"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := credentialNameSanitizer.ReplaceAllString(tt.input, "-") + require.Equal(t, tt.expected, result) + }) + } +}