diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go index 38e0ca0938..8c0a03a787 100644 --- a/pkg/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -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) @@ -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 diff --git a/pkg/webhook/webhook_test.go b/pkg/webhook/webhook_test.go index bf48589428..cf90b722d2 100644 --- a/pkg/webhook/webhook_test.go +++ b/pkg/webhook/webhook_test.go @@ -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":"fleet@suse.com","date":"2025-08-26T10:16:56Z"},"committer":{"name":"fleet","email":"fleet@suse.com","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":"fleet@suse.com","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":"fleet@suse.com","date":"2025-08-26T10:16:56Z"},"committer":{"name":"fleet","email":"fleet@suse.com","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":"fleet@suse.com","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"