Skip to content

Commit 6b2ecff

Browse files
Copilotthardeck
andcommitted
Backport #4060: Fix Azure DevOps webhooks with spaces and URL formats
Co-authored-by: thardeck <[email protected]>
1 parent 67ccc34 commit 6b2ecff

File tree

2 files changed

+130
-1
lines changed

2 files changed

+130
-1
lines changed

pkg/webhook/webhook.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,9 @@ func (w *Webhook) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
197197
w.logAndReturn(rw, err)
198198
return
199199
}
200-
path := strings.Replace(u.Path[1:], "/_git/", "(/_git)?/", 1)
200+
201+
path := strings.Replace(u.EscapedPath()[1:], "/_git/", "(/_git)?/", 1)
202+
201203
regexpStr := `(?i)(http://|https://|\w+@|ssh://(\w+@)?|git@(ssh\.)?)` + u.Hostname() +
202204
"(:[0-9]+|)[:/](v\\d/)?" + path + "(\\.git)?"
203205
repoRegexp, err := regexp.Compile(regexpStr)
@@ -389,6 +391,27 @@ func parsePayload(payload interface{}) (revision, branch, tag string, repoURLs [
389391
revision = t.After
390392
case azuredevops.GitPushEvent:
391393
repoURLs = append(repoURLs, t.Resource.Repository.RemoteURL)
394+
395+
// This is to make sure that there's URL matching between:
396+
// 1. https://org.visualstudio.com/project/_git/repo
397+
// 2. https://dev.azure.com/org/project/_git/repo
398+
// 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)
399+
// There are multiple URLs formats and these may overlap in different areas of Azure DevOps
400+
for i, u := range repoURLs {
401+
parsed, err := url.Parse(u)
402+
if err != nil {
403+
continue
404+
}
405+
if strings.HasSuffix(parsed.Hostname(), ".visualstudio.com") {
406+
org := strings.SplitN(parsed.Hostname(), ".", 2)[0]
407+
parsed.Host = "dev.azure.com"
408+
// parsed.Path is prefixed with a slash, hence no need to add it to the formatting
409+
// string.
410+
parsed.Path = fmt.Sprintf("/%s%s", org, parsed.Path)
411+
repoURLs[i] = parsed.String()
412+
}
413+
}
414+
392415
for _, refUpdate := range t.Resource.RefUpdates {
393416
branch, tag = getBranchTagFromRef(refUpdate.Name)
394417
revision = refUpdate.NewObjectID

pkg/webhook/webhook_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,112 @@ func TestAzureDevopsWebhook(t *testing.T) {
101101
}
102102
}
103103

104+
func TestAzureDevopsWebhookWithURLSpacing(t *testing.T) {
105+
cases := []struct {
106+
name string
107+
repoURL string
108+
}{
109+
{
110+
name: "legacy URL",
111+
repoURL: "https://visualstudio.com/fleet/git%20test/_git/git%20test",
112+
},
113+
{
114+
name: "newer URL",
115+
repoURL: "https://dev.azure.com/fleet/git%20test/_git/git%20test",
116+
},
117+
}
118+
119+
for _, c := range cases {
120+
t.Run(c.name, func(t *testing.T) {
121+
122+
const commit = "f00c3a181697bb3829a6462e931c7456bbed557b"
123+
gitRepo := &v1alpha1.GitRepo{
124+
ObjectMeta: metav1.ObjectMeta{
125+
Name: "test",
126+
},
127+
Spec: v1alpha1.GitRepoSpec{
128+
Repo: c.repoURL,
129+
Branch: "main",
130+
},
131+
}
132+
scheme := runtime.NewScheme()
133+
err := v1alpha1.AddToScheme(scheme)
134+
if err != nil {
135+
t.Errorf("unexpected error %v", err)
136+
}
137+
client := cfake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(gitRepo).WithStatusSubresource(gitRepo).Build()
138+
w := &Webhook{client: client}
139+
w.azureDevops, _ = azuredevops.New()
140+
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"}`)
141+
bodyReader := bytes.NewReader(jsonBody)
142+
req, err := http.NewRequest(http.MethodPost, c.repoURL, bodyReader)
143+
if err != nil {
144+
t.Errorf("unexpected err %v", err)
145+
}
146+
h := http.Header{}
147+
h.Add("X-Vss-Activityid", "xxx")
148+
req.Header = h
149+
150+
w.ServeHTTP(&responseWriter{}, req)
151+
152+
updatedGitRepo := &v1alpha1.GitRepo{}
153+
err = client.Get(context.TODO(), types.NamespacedName{Name: gitRepo.Name, Namespace: gitRepo.Namespace}, updatedGitRepo)
154+
if err != nil {
155+
t.Errorf("unexpected err %v", err)
156+
}
157+
if updatedGitRepo.Status.WebhookCommit != commit {
158+
t.Errorf("expected webhook commit %v, but got %v", commit, updatedGitRepo.Status.WebhookCommit)
159+
}
160+
})
161+
}
162+
}
163+
164+
func TestAzureDevopsWebhookWithURLMatching(t *testing.T) {
165+
const commit = "f00c3a181697bb3829a6462e931c7456bbed557b"
166+
const repoURL = "https://dev.azure.com/fleet/git-test/_git/git-test"
167+
168+
// Should be matched to repoURL
169+
const remoteURL = "https://fleet.visualstudio.com/git-test/_git/git-test"
170+
171+
gitRepo := &v1alpha1.GitRepo{
172+
ObjectMeta: metav1.ObjectMeta{
173+
Name: "test",
174+
},
175+
Spec: v1alpha1.GitRepoSpec{
176+
Repo: repoURL,
177+
Branch: "main",
178+
},
179+
}
180+
scheme := runtime.NewScheme()
181+
err := v1alpha1.AddToScheme(scheme)
182+
if err != nil {
183+
t.Errorf("unexpected error %v", err)
184+
}
185+
client := cfake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(gitRepo).WithStatusSubresource(gitRepo).Build()
186+
w := &Webhook{client: client}
187+
w.azureDevops, _ = azuredevops.New()
188+
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"}`)
189+
bodyReader := bytes.NewReader(jsonBody)
190+
req, err := http.NewRequest(http.MethodPost, repoURL, bodyReader)
191+
if err != nil {
192+
t.Errorf("unexpected err %v", err)
193+
}
194+
h := http.Header{}
195+
h.Add("X-Vss-Activityid", "xxx")
196+
req.Header = h
197+
198+
w.ServeHTTP(&responseWriter{}, req)
199+
200+
updatedGitRepo := &v1alpha1.GitRepo{}
201+
err = client.Get(context.TODO(), types.NamespacedName{Name: gitRepo.Name, Namespace: gitRepo.Namespace}, updatedGitRepo)
202+
if err != nil {
203+
t.Errorf("unexpected err %v", err)
204+
}
205+
if updatedGitRepo.Status.WebhookCommit != commit {
206+
t.Errorf("expected webhook commit %v, but got %v", commit, updatedGitRepo.Status.WebhookCommit)
207+
}
208+
}
209+
104210
func TestAzureDevopsWebhookWithSSHURL(t *testing.T) {
105211
const (
106212
commit = "f00c3a181697bb3829a6462e931c7456bbed557b"

0 commit comments

Comments
 (0)