Skip to content
Open
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
25 changes: 24 additions & 1 deletion pkg/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ func (w *Webhook) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
w.logAndReturn(rw, err)
return
}
path := strings.Replace(u.Path[1:], "/_git/", "(/_git)?/", 1)

path := strings.Replace(u.EscapedPath()[1:], "/_git/", "(/_git)?/", 1)

regexpStr := `(?i)(http://|https://|\w+@|ssh://(\w+@)?|git@(ssh\.)?)` + u.Hostname() +
"(:[0-9]+|)[:/](v\\d/)?" + path + "(\\.git)?"
repoRegexp, err := regexp.Compile(regexpStr)
Expand Down Expand Up @@ -320,6 +322,27 @@ func parsePayload(payload interface{}) (revision, branch, tag string, repoURLs [
revision = t.After
case azuredevops.GitPushEvent:
repoURLs = append(repoURLs, t.Resource.Repository.RemoteURL)

// This is to make sure that there's URL matching between:
// 1. https://org.visualstudio.com/project/_git/repo
// 2. https://dev.azure.com/org/project/_git/repo
// As stated by Microsoft [here](https://learn.microsoft.com/en-us/azure/devops/release-notes/2018/sep-10-azure-devops-launch#switch-existing-organizations-to-use-the-new-domain-name-url)
// There are multiple URLs formats and these may overlap in different areas of Azure DevOps
for i, u := range repoURLs {
parsed, err := url.Parse(u)
if err != nil {
continue
}
if strings.HasSuffix(parsed.Hostname(), ".visualstudio.com") {
org := strings.SplitN(parsed.Hostname(), ".", 2)[0]
parsed.Host = "dev.azure.com"
// parsed.Path is prefixed with a slash, hence no need to add it to the formatting
// string.
parsed.Path = fmt.Sprintf("/%s%s", org, parsed.Path)
repoURLs[i] = parsed.String()
}
}

for _, refUpdate := range t.Resource.RefUpdates {
branch, tag = getBranchTagFromRef(refUpdate.Name)
revision = refUpdate.NewObjectID
Expand Down
102 changes: 102 additions & 0 deletions pkg/webhook/webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,108 @@ func TestAzureDevopsWebhook(t *testing.T) {
}
}

func TestAzureDevopsWebhookWithURLSpacing(t *testing.T) {
cases := []struct {
name string
repoURL string
}{
{
name: "legacy URL",
repoURL: "https://visualstudio.com/fleet/git%20test/_git/git%20test",
},
{
name: "newer URL",
repoURL: "https://dev.azure.com/fleet/git%20test/_git/git%20test",
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {

const commit = "f00c3a181697bb3829a6462e931c7456bbed557b"
gitRepo := &v1alpha1.GitRepo{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
Spec: v1alpha1.GitRepoSpec{
Repo: c.repoURL,
Branch: "main",
},
}
scheme := runtime.NewScheme()
utilruntime.Must(corev1.AddToScheme(scheme))
utilruntime.Must(v1alpha1.AddToScheme(scheme))

client := cfake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(gitRepo).WithStatusSubresource(gitRepo).Build()
w := &Webhook{client: client}
jsonBody := []byte(`{"subscriptionId":"xxx","notificationId":1,"id":"xxx","eventType":"git.push","publisherId":"tfs","message":{"text":"commit pushed","html":"commit pushed"},"detailedMessage":{"text":"pushed a commit to git test"},"resource":{"commits":[{"commitId":"` + commit + `","author":{"name":"fleet","email":"[email protected]","date":"2025-08-26T10:16:56Z"},"committer":{"name":"fleet","email":"[email protected]","date":"2025-08-26T10:16:56Z"},"comment":"test commit","url":"https://dev.azure.com/fleet/_apis/git/repositories/xxx/commits/f00c3a181697bb3829a6462e931c7456bbed557b"}],"refUpdates":[{"name":"refs/heads/main","oldObjectId":"135f8a827edae980466f72eef385881bb4e158d8","newObjectId":"` + commit + `"}],"repository":{"id":"xxx","name":"git test","url":"https://dev.azure.com/fleet/_apis/git/repositories/xxx","project":{"id":"xxx","name":"git test","url":"https://dev.azure.com/fleet/_apis/projects/xxx","state":"wellFormed","visibility":"unchanged","lastUpdateTime":"0001-01-01T00:00:00"},"defaultBranch":"refs/heads/main","remoteUrl":"` + c.repoURL + `"},"pushedBy":{"displayName":"Fleet","url":"https://spsprodneu1.vssps.visualstudio.com/xxx/_apis/Identities/xxx","_links":{"avatar":{"href":"https://dev.azure.com/fleet/_apis/GraphProfile/MemberAvatars/msa.xxxx"}},"id":"xxx","uniqueName":"[email protected]","imageUrl":"https://dev.azure.com/fleet/_api/_common/identityImage?id=xxx","descriptor":"xxxx"},"pushId":22,"date":"2025-08-26T10:17:18.735088Z","url":"https://dev.azure.com/fleet/_apis/git/repositories/xxx/pushes/22","_links":{"self":{"href":"https://dev.azure.com/fleet/_apis/git/repositories/xxx/pushes/22"},"repository":{"href":"https://dev.azure.com/fleet/xxx/_apis/git/repositories/xxx"},"commits":{"href":"https://dev.azure.com/fleet/_apis/git/repositories/xxx/pushes/22/commits"},"pusher":{"href":"https://spsprodneu1.vssps.visualstudio.com/xxx/_apis/Identities/xxx"},"refs":{"href":"https://dev.azure.com/fleet/xxx/_apis/git/repositories/xxx/refs/heads/main"}}},"resourceVersion":"1.0","resourceContainers":{"collection":{"id":"xxx","baseUrl":"https://dev.azure.com/fleet/"},"account":{"id":"ec365173-fce3-4dfc-8fc2-950f0b5728b1","baseUrl":"https://dev.azure.com/fleet/"},"project":{"id":"xxx","baseUrl":"https://dev.azure.com/fleet/"}},"createdDate":"2025-08-26T10:17:26.0098694Z"}`)
bodyReader := bytes.NewReader(jsonBody)
req, err := http.NewRequest(http.MethodPost, c.repoURL, bodyReader)
if err != nil {
t.Errorf("unexpected err %v", err)
}
h := http.Header{}
h.Add("X-Vss-Activityid", "xxx")
req.Header = h

w.ServeHTTP(&responseWriter{}, req)

updatedGitRepo := &v1alpha1.GitRepo{}
err = client.Get(context.TODO(), types.NamespacedName{Name: gitRepo.Name, Namespace: gitRepo.Namespace}, updatedGitRepo)
if err != nil {
t.Errorf("unexpected err %v", err)
}
if updatedGitRepo.Status.WebhookCommit != commit {
t.Errorf("expected webhook commit %v, but got %v", commit, updatedGitRepo.Status.WebhookCommit)
}
})
}
}

func TestAzureDevopsWebhookWithURLMatching(t *testing.T) {
const commit = "f00c3a181697bb3829a6462e931c7456bbed557b"
const repoURL = "https://dev.azure.com/fleet/git-test/_git/git-test"

// Should be matched to repoURL
const remoteURL = "https://fleet.visualstudio.com/git-test/_git/git-test"

gitRepo := &v1alpha1.GitRepo{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
Spec: v1alpha1.GitRepoSpec{
Repo: repoURL,
Branch: "main",
},
}
scheme := runtime.NewScheme()
utilruntime.Must(corev1.AddToScheme(scheme))
utilruntime.Must(v1alpha1.AddToScheme(scheme))

client := cfake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(gitRepo).WithStatusSubresource(gitRepo).Build()
w := &Webhook{client: client}
jsonBody := []byte(`{"subscriptionId":"xxx","notificationId":1,"id":"xxx","eventType":"git.push","publisherId":"tfs","message":{"text":"commit pushed","html":"commit pushed"},"detailedMessage":{"text":"pushed a commit to git test"},"resource":{"commits":[{"commitId":"` + commit + `","author":{"name":"fleet","email":"[email protected]","date":"2025-08-26T10:16:56Z"},"committer":{"name":"fleet","email":"[email protected]","date":"2025-08-26T10:16:56Z"},"comment":"test commit","url":"https://dev.azure.com/fleet/_apis/git/repositories/xxx/commits/f00c3a181697bb3829a6462e931c7456bbed557b"}],"refUpdates":[{"name":"refs/heads/main","oldObjectId":"135f8a827edae980466f72eef385881bb4e158d8","newObjectId":"` + commit + `"}],"repository":{"id":"xxx","name":"git test","url":"https://dev.azure.com/fleet/_apis/git/repositories/xxx","project":{"id":"xxx","name":"git test","url":"https://dev.azure.com/fleet/_apis/projects/xxx","state":"wellFormed","visibility":"unchanged","lastUpdateTime":"0001-01-01T00:00:00"},"defaultBranch":"refs/heads/main","remoteUrl":"` + remoteURL + `"},"pushedBy":{"displayName":"Fleet","url":"https://spsprodneu1.vssps.visualstudio.com/xxx/_apis/Identities/xxx","_links":{"avatar":{"href":"https://dev.azure.com/fleet/_apis/GraphProfile/MemberAvatars/msa.xxxx"}},"id":"xxx","uniqueName":"[email protected]","imageUrl":"https://dev.azure.com/fleet/_api/_common/identityImage?id=xxx","descriptor":"xxxx"},"pushId":22,"date":"2025-08-26T10:17:18.735088Z","url":"https://dev.azure.com/fleet/_apis/git/repositories/xxx/pushes/22","_links":{"self":{"href":"https://dev.azure.com/fleet/_apis/git/repositories/xxx/pushes/22"},"repository":{"href":"https://dev.azure.com/fleet/xxx/_apis/git/repositories/xxx"},"commits":{"href":"https://dev.azure.com/fleet/_apis/git/repositories/xxx/pushes/22/commits"},"pusher":{"href":"https://spsprodneu1.vssps.visualstudio.com/xxx/_apis/Identities/xxx"},"refs":{"href":"https://dev.azure.com/fleet/xxx/_apis/git/repositories/xxx/refs/heads/main"}}},"resourceVersion":"1.0","resourceContainers":{"collection":{"id":"xxx","baseUrl":"https://fleet.visualstudio.com/"},"account":{"id":"ec365173-fce3-4dfc-8fc2-950f0b5728b1","baseUrl":"https://fleet.visualstudio.com/"},"project":{"id":"xxx","baseUrl":"https://fleet.visualstudio.com/"}},"createdDate":"2025-08-26T10:17:26.0098694Z"}`)
bodyReader := bytes.NewReader(jsonBody)
req, err := http.NewRequest(http.MethodPost, repoURL, bodyReader)
if err != nil {
t.Errorf("unexpected err %v", err)
}
h := http.Header{}
h.Add("X-Vss-Activityid", "xxx")
req.Header = h

w.ServeHTTP(&responseWriter{}, req)

updatedGitRepo := &v1alpha1.GitRepo{}
err = client.Get(context.TODO(), types.NamespacedName{Name: gitRepo.Name, Namespace: gitRepo.Namespace}, updatedGitRepo)
if err != nil {
t.Errorf("unexpected err %v", err)
}
if updatedGitRepo.Status.WebhookCommit != commit {
t.Errorf("expected webhook commit %v, but got %v", commit, updatedGitRepo.Status.WebhookCommit)
}
}

func TestAzureDevopsWebhookWithSSHURL(t *testing.T) {
const (
commit = "f00c3a181697bb3829a6462e931c7456bbed557b"
Expand Down
Loading