From 342ced047e8f4a8aacdbd8d4a4f45d39e48e4aae Mon Sep 17 00:00:00 2001 From: Shayne Boyer Date: Fri, 6 Mar 2026 22:22:00 -0500 Subject: [PATCH] fix: sanitize dots in repo name for federated identity credentials azd pipeline config fails when the GitHub repo name contains dots because federated identity credential names only allow letters, numbers, hyphens, and underscores. The repo slug was only replacing '/' with '-' but not handling dots or other special characters. Use a shared regex sanitizer (same pattern already used for branch names) to replace all invalid characters in the credential name. Also removes unnecessary url.PathEscape wrapping. Fixes #5948 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/pkg/pipeline/github_provider.go | 13 +++++++----- cli/azd/pkg/pipeline/github_provider_test.go | 21 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) 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) + }) + } +}