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
13 changes: 8 additions & 5 deletions cli/azd/pkg/pipeline/github_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"fmt"
"io/fs"
"maps"
"net/url"
"path/filepath"
"regexp"
"slices"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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"),
Expand All @@ -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"),
Expand Down
21 changes: 21 additions & 0 deletions cli/azd/pkg/pipeline/github_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
Loading