Repository: rancher/fleet

Issue:
Add workflow to run benchmark suite
To make sure it isn't broken.


Discussion:

Solution patch:
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
new file mode 100644
index 00000000..1abaeab1
--- /dev/null
+++ b/.github/workflows/benchmark.yml
@@ -0,0 +1,114 @@
+name: Benchmarks
+
+on:
+  workflow_dispatch:
+    inputs:
+      timeout:
+        description: Eventually Default Timeout (e.g., 2m)
+        required: false
+        default: 2m
+      namespace:
+        description: Fleet namespace
+        required: false
+        default: fleet-local
+  schedule:
+    - cron: "0 15 * * *"
+
+permissions:
+  contents: read
+
+concurrency:
+  group: benchmarks-${{ github.ref }}
+  cancel-in-progress: true
+
+env:
+  GOARCH: amd64
+  CGO_ENABLED: 0
+  SETUP_K3D_VERSION: "v5.8.3"
+  SETUP_K3S_VERSION: "v1.33.1-k3s1"
+  # Defaults for both manual and scheduled runs
+  BENCH_TIMEOUT: ${{ github.event.inputs.timeout || '2m' }}
+  BENCH_NAMESPACE: ${{ github.event.inputs.namespace || 'fleet-local' }}
+
+jobs:
+  benchmark:
+    name: Run Fleet Benchmarks
+    runs-on: ubuntu-latest
+    timeout-minutes: 90
+    steps:
+      - name: Checkout
+        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
+        with:
+          fetch-depth: 0
+
+      - name: Setup Go
+        uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6
+        with:
+          go-version-file: "go.mod"
+          check-latest: true
+
+      # No extra host dependencies required
+
+      # Rely on kubectl and helm preinstalled in GitHub runner image
+      - name: Build Fleet
+        run: |
+          ./.github/scripts/build-fleet-binaries.sh
+          ./.github/scripts/build-fleet-images.sh
+
+      - name: Check Docker installation
+        run: |
+          docker --version
+          docker info
+
+      - name: Install k3d
+        run: curl --silent --fail https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh | TAG=${{ env.SETUP_K3D_VERSION }} bash
+
+      - name: Verify k3d installation
+        run: |
+          k3d version
+          which k3d
+
+      - name: Provision k3d Cluster
+        run: |
+          k3d cluster create upstream --wait \
+            --agents 1 \
+            --image docker.io/rancher/k3s:${{ env.SETUP_K3S_VERSION }}
+
+      - name: Import Images Into k3d
+        run: |
+          ./.github/scripts/k3d-import-retry.sh rancher/fleet:dev rancher/fleet-agent:dev -c upstream
+
+      - name: Deploy Fleet
+        run: |
+          ./.github/scripts/deploy-fleet.sh
+
+      - name: Label local cluster for benchmarks
+        run: |
+          kubectl -n "$BENCH_NAMESPACE" label clusters.fleet.cattle.io local fleet.cattle.io/benchmark=true --overwrite
+
+      - name: Run Benchmarks
+        env:
+          FLEET_BENCH_TIMEOUT: ${{ env.BENCH_TIMEOUT }}
+          FLEET_BENCH_NAMESPACE: ${{ env.BENCH_NAMESPACE }}
+        run: |
+          go run ./benchmarks/cmd run -d benchmarks/db -t "$FLEET_BENCH_TIMEOUT" -n "$FLEET_BENCH_NAMESPACE"
+
+      - name: Collect benchmark report filename
+        id: report
+        shell: bash
+        run: |
+          set -euo pipefail
+          file=$(ls -1t b-*.json | head -n1)
+          # Replace characters that cause issues in artifact uploads (e.g., ':')
+          safe_file="${file//:/-}"
+          if [ "$file" != "$safe_file" ]; then
+            mv "$file" "$safe_file"
+          fi
+          echo "report=$safe_file" >> "$GITHUB_OUTPUT"
+          echo "Found report: $safe_file"
+
+      - name: Upload benchmark JSON
+        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+        with:
+          name: benchmark-report
+          path: ${{ steps.report.outputs.report }}
diff --git a/_typos.toml b/_typos.toml
index 8066a4de..5c34afae 100644
--- a/_typos.toml
+++ b/_typos.toml
@@ -12,4 +12,4 @@ extend-exclude = [
 "passin" = "passin"
 "ANDed" = "ANDed"
 "HoTWPQxTJ5dIY31" = "HoTWPQxTJ5dIY31"
-"wre" = "wre" # warning regular expression, not 'were'
+"wre" = "wre"                         # warning regular expression, not 'were'
diff --git a/internal/cmd/agent/deployer/summary/summarizers.go b/internal/cmd/agent/deployer/summary/summarizers.go
index ed10487e..48561522 100644
--- a/internal/cmd/agent/deployer/summary/summarizers.go
+++ b/internal/cmd/agent/deployer/summary/summarizers.go
@@ -80,26 +80,26 @@ var (
 	GVKConditionErrorMapping = ConditionTypeStatusErrorMapping{
 		{Group: "helm.cattle.io", Version: "v1", Kind: "HelmChart"}: {
 			"JobCreated": sets.New[metav1.ConditionStatus](),
-			"Failed":     sets.New[metav1.ConditionStatus](metav1.ConditionTrue),
+			"Failed":     sets.New(metav1.ConditionTrue),
 		},
 		{Group: "", Version: "v1", Kind: "Node"}: {
-			"OutOfDisk":          sets.New[metav1.ConditionStatus](metav1.ConditionTrue),
-			"MemoryPressure":     sets.New[metav1.ConditionStatus](metav1.ConditionTrue),
-			"DiskPressure":       sets.New[metav1.ConditionStatus](metav1.ConditionTrue),
-			"NetworkUnavailable": sets.New[metav1.ConditionStatus](metav1.ConditionTrue),
+			"OutOfDisk":          sets.New(metav1.ConditionTrue),
+			"MemoryPressure":     sets.New(metav1.ConditionTrue),
+			"DiskPressure":       sets.New(metav1.ConditionTrue),
+			"NetworkUnavailable": sets.New(metav1.ConditionTrue),
 		},
 		{Group: "apps", Version: "v1", Kind: "Deployment"}: {
-			"ReplicaFailure": sets.New[metav1.ConditionStatus](metav1.ConditionTrue),
-			"Progressing":    sets.New[metav1.ConditionStatus](metav1.ConditionFalse),
+			"ReplicaFailure": sets.New(metav1.ConditionTrue),
+			"Progressing":    sets.New(metav1.ConditionFalse),
 		},
 		{Group: "apps", Version: "v1", Kind: "ReplicaSet"}: {
-			"ReplicaFailure": sets.New[metav1.ConditionStatus](metav1.ConditionTrue),
+			"ReplicaFailure": sets.New(metav1.ConditionTrue),
 		},
 
 		// FALLBACK: In case we cannot match any Groups, Versions and Kinds then we fallback to this mapping.
 		{Group: "", Version: "", Kind: ""}: {
-			"Stalled": sets.New[metav1.ConditionStatus](metav1.ConditionTrue),
-			"Failed":  sets.New[metav1.ConditionStatus](metav1.ConditionTrue),
+			"Stalled": sets.New(metav1.ConditionTrue),
+			"Failed":  sets.New(metav1.ConditionTrue),
 		},
 	}
 
diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go
index 38e0ca09..8c0a03a7 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



Test patch:
diff --git a/internal/cmd/agent/deployer/summary/summarizers_test.go b/internal/cmd/agent/deployer/summary/summarizers_test.go
index 81683397..5732eb1e 100644
--- a/internal/cmd/agent/deployer/summary/summarizers_test.go
+++ b/internal/cmd/agent/deployer/summary/summarizers_test.go
@@ -49,8 +49,8 @@ func TestCheckErrors(t *testing.T) {
 			name: "gvk not found - summary remains the same",
 			input: input{
 				data: data.Object{
-					"APIVersion": "sample.cattle.io/v1",
-					"Kind":       "Sample",
+					"apiVersion": "sample.cattle.io/v1",
+					"kind":       "Sample",
 				},
 				summary: fleet.Summary{
 					State: "testing",
@@ -68,8 +68,8 @@ func TestCheckErrors(t *testing.T) {
 			name: "gvk found, no conditions provided",
 			input: input{
 				data: data.Object{
-					"APIVersion": "helm.cattle.io/v1",
-					"Kind":       "HelmChart",
+					"apiVersion": "helm.cattle.io/v1",
+					"kind":       "HelmChart",
 				},
 				summary: fleet.Summary{
 					State: "testing",
@@ -87,8 +87,8 @@ func TestCheckErrors(t *testing.T) {
 			name: "gvk found, condition not found",
 			input: input{
 				data: data.Object{
-					"APIVersion": "helm.cattle.io/v1",
-					"Kind":       "HelmChart",
+					"apiVersion": "helm.cattle.io/v1",
+					"kind":       "HelmChart",
 				},
 				conditions: []Condition{
 					newCondition("JobFailed", "True", "", ""),
@@ -109,8 +109,8 @@ func TestCheckErrors(t *testing.T) {
 			name: "gvk found, condition is error",
 			input: input{
 				data: data.Object{
-					"APIVersion": "helm.cattle.io/v1",
-					"Kind":       "HelmChart",
+					"apiVersion": "helm.cattle.io/v1",
+					"kind":       "HelmChart",
 				},
 				conditions: []Condition{
 					newCondition("Failed", "True", "", "Helm Install Error"),
@@ -134,8 +134,8 @@ func TestCheckErrors(t *testing.T) {
 			name: "gvk found, condition is not an error",
 			input: input{
 				data: data.Object{
-					"APIVersion": "helm.cattle.io/v1",
-					"Kind":       "HelmChart",
+					"apiVersion": "helm.cattle.io/v1",
+					"kind":       "HelmChart",
 				},
 				conditions: []Condition{
 					newCondition("Failed", "False", "", ""),
@@ -156,8 +156,8 @@ func TestCheckErrors(t *testing.T) {
 			name: "load conditions - gvk not found",
 			input: input{
 				data: data.Object{
-					"APIVersion": "helm.cattle.io/v1",
-					"Kind":       "HelmChart",
+					"apiVersion": "helm.cattle.io/v1",
+					"kind":       "HelmChart",
 				},
 				conditions: []Condition{
 					newCondition("Failed", "False", "", ""),
@@ -193,8 +193,8 @@ func TestCheckErrors(t *testing.T) {
 			name: "load conditions - gvk found - condition is only informational",
 			input: input{
 				data: data.Object{
-					"APIVersion": "sample.cattle.io/v1",
-					"Kind":       "Sample",
+					"apiVersion": "sample.cattle.io/v1",
+					"kind":       "Sample",
 				},
 				conditions: []Condition{
 					newCondition("Created", "True", "", ""),
@@ -230,8 +230,8 @@ func TestCheckErrors(t *testing.T) {
 			name: "load conditions - gvk found - is not an error",
 			input: input{
 				data: data.Object{
-					"APIVersion": "sample.cattle.io/v1",
-					"Kind":       "Sample",
+					"apiVersion": "sample.cattle.io/v1",
+					"kind":       "Sample",
 				},
 				conditions: []Condition{
 					newCondition("Failed", "False", "", ""),
@@ -247,6 +247,53 @@ func TestCheckErrors(t *testing.T) {
 					Error: false,
 				},
 			},
+		},
+		{
+			name: "load conditions - gvk found - is error",
+			input: input{
+				data: data.Object{
+					"apiVersion": "sample.cattle.io/v1",
+					"kind":       "Sample",
+				},
+				conditions: []Condition{
+					newCondition("Failed", "True", "", "Sample Failure"),
+				},
+				summary: fleet.Summary{
+					State: "testing",
+					Error: false,
+				},
+			},
+			expected: output{
+				summary: fleet.Summary{
+					State: "testing",
+					Error: true,
+					Message: []string{
+						"Sample Failure",
+					},
+				},
+			},
+		},
+		{
+			name: "load conditions - gvk found - is error but should be ignored",
+			input: input{
+				data: data.Object{
+					"apiVersion": "sample.cattle.io/v1",
+					"kind":       "Sample",
+				},
+				conditions: []Condition{
+					newCondition("Failed", "True", "", ""),
+				},
+				summary: fleet.Summary{
+					State: "testing",
+					Error: false,
+				},
+			},
+			expected: output{
+				summary: fleet.Summary{
+					State: "testing",
+					Error: false,
+				},
+			},
 			loadConditions: func() {
 				os.Setenv(checkGVKErrorMappingEnvVar, `
 					[
@@ -255,7 +302,7 @@ func TestCheckErrors(t *testing.T) {
 							"conditionMappings": [
 								{
 									"type": "Failed",
-									"status": ["True"]
+                                    "status": []
 								}
 							]
 						}
@@ -264,14 +311,14 @@ func TestCheckErrors(t *testing.T) {
 			},
 		},
 		{
-			name: "load conditions - gvk found - is error",
+			name: "load conditions - gvk found - is not error but should be treated as error",
 			input: input{
 				data: data.Object{
-					"APIVersion": "sample.cattle.io/v1",
-					"Kind":       "Sample",
+					"apiVersion": "sample.cattle.io/v1",
+					"kind":       "Sample",
 				},
 				conditions: []Condition{
-					newCondition("Failed", "True", "", "Sample Failure"),
+					newCondition("Foo", "True", "", ""),
 				},
 				summary: fleet.Summary{
 					State: "testing",
@@ -280,11 +327,9 @@ func TestCheckErrors(t *testing.T) {
 			},
 			expected: output{
 				summary: fleet.Summary{
-					State: "testing",
-					Error: true,
-					Message: []string{
-						"Sample Failure",
-					},
+					State:   "testing",
+					Error:   true,
+					Message: []string{""},
 				},
 			},
 			loadConditions: func() {
@@ -294,8 +339,8 @@ func TestCheckErrors(t *testing.T) {
 							"gvk": "sample.cattle.io/v1, Kind=Sample",
 							"conditionMappings": [
 								{
-									"type": "Failed",
-									"status": ["True"]
+									"type": "Foo",
+                                    "status": ["True"]
 								}
 							]
 						}
@@ -307,8 +352,8 @@ func TestCheckErrors(t *testing.T) {
 			name: "fallback conditions",
 			input: input{
 				data: data.Object{
-					"APIVersion": "fallback.cattle.io/v1",
-					"Kind":       "Fallback",
+					"apiVersion": "fallback.cattle.io/v1",
+					"kind":       "Fallback",
 				},
 				conditions: []Condition{
 					newCondition("Failed", "True", "", "Sample Failure"),
@@ -332,8 +377,8 @@ func TestCheckErrors(t *testing.T) {
 			name: "condition has error at reason field",
 			input: input{
 				data: data.Object{
-					"APIVersion": "sample.cattle.io/v1",
-					"Kind":       "Sample",
+					"apiVersion": "sample.cattle.io/v1",
+					"kind":       "Sample",
 				},
 				conditions: []Condition{
 					newCondition("SampleFailed", "True", "Error", "Error in Reason"),
diff --git a/pkg/webhook/webhook_test.go b/pkg/webhook/webhook_test.go
index 9f7c0ede..9f6b4d87 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"