Repository: envoyproxy/gateway

Issue:
perf: Set Status LastTransitionTime in the status updater (subscriber) rather than in producer gw api translator (publisher) side
*Description*:
>Describe the issue.

We can avoid publishing the same status multiple times by not setting the LastTransitionTime field in the gateway-api layer. This ensures watchable will be able to deduplicate the identical status conditions and not publish them to status updater.

This also means, the status updater will now need to set the LastTransitionTime for every condition

[optional *Relevant Links*:]
>Any extra documentation required to understand the issue.


Discussion:

Solution patch:
diff --git a/go.mod b/go.mod
index 89d3ee1eb..e79462fd1 100644
--- a/go.mod
+++ b/go.mod
@@ -120,7 +120,7 @@ require (
 	github.com/chai2010/gettext-go v1.0.2 // indirect
 	github.com/cilium/ebpf v0.19.0 // indirect
 	github.com/containerd/cgroups/v3 v3.0.5 // indirect
-	github.com/containerd/containerd v1.7.28 // indirect
+	github.com/containerd/containerd v1.7.29 // indirect
 	github.com/containerd/errdefs v1.0.0 // indirect
 	github.com/containerd/errdefs/pkg v0.3.0 // indirect
 	github.com/containerd/log v0.1.0 // indirect
diff --git a/go.sum b/go.sum
index a9aa95038..caf7ff69c 100644
--- a/go.sum
+++ b/go.sum
@@ -101,8 +101,8 @@ github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv
 github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
 github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
 github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
-github.com/containerd/containerd v1.7.28 h1:Nsgm1AtcmEh4AHAJ4gGlNSaKgXiNccU270Dnf81FQ3c=
-github.com/containerd/containerd v1.7.28/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs=
+github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE=
+github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs=
 github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
 github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
 github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
diff --git a/internal/gatewayapi/status/backend.go b/internal/gatewayapi/status/backend.go
index 16c0139ca..f3973428a 100644
--- a/internal/gatewayapi/status/backend.go
+++ b/internal/gatewayapi/status/backend.go
@@ -14,8 +14,6 @@
 package status
 
 import (
-	"time"
-
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
@@ -33,10 +31,10 @@ func computeBackendAcceptedCondition(be *egv1a1.Backend, accepted bool, msg stri
 	case true:
 		return newCondition(string(egv1a1.BackendReasonAccepted), metav1.ConditionTrue,
 			string(egv1a1.BackendConditionAccepted),
-			"The Backend was accepted", time.Now(), be.Generation)
+			"The Backend was accepted", be.Generation)
 	default:
 		return newCondition(string(egv1a1.BackendReasonInvalid), metav1.ConditionFalse,
 			string(egv1a1.BackendConditionAccepted),
-			msg, time.Now(), be.Generation)
+			msg, be.Generation)
 	}
 }
diff --git a/internal/gatewayapi/status/conditions.go b/internal/gatewayapi/status/conditions.go
index b3320caac..e821f1eaf 100644
--- a/internal/gatewayapi/status/conditions.go
+++ b/internal/gatewayapi/status/conditions.go
@@ -14,7 +14,6 @@
 package status
 
 import (
-	"time"
 	"unicode"
 
 	"github.com/google/go-cmp/cmp"
@@ -42,7 +41,6 @@ func MergeConditions(conditions []metav1.Condition, updates ...metav1.Condition)
 					conditions[j].Reason = updates[i].Reason
 					conditions[j].Message = updates[i].Message
 					conditions[j].ObservedGeneration = updates[i].ObservedGeneration
-					conditions[j].LastTransitionTime = updates[i].LastTransitionTime
 					break
 				}
 			}
@@ -55,13 +53,12 @@ func MergeConditions(conditions []metav1.Condition, updates ...metav1.Condition)
 	return conditions
 }
 
-func newCondition(t string, status metav1.ConditionStatus, reason, msg string, lt time.Time, og int64) metav1.Condition {
+func newCondition(t string, status metav1.ConditionStatus, reason, msg string, og int64) metav1.Condition {
 	return metav1.Condition{
 		Type:               t,
 		Status:             status,
 		Reason:             reason,
 		Message:            truncateConditionMessage(msg),
-		LastTransitionTime: metav1.NewTime(lt),
 		ObservedGeneration: og,
 	}
 }
diff --git a/internal/gatewayapi/status/envoypatchpolicy.go b/internal/gatewayapi/status/envoypatchpolicy.go
index b20fdc53d..0cc068d2e 100644
--- a/internal/gatewayapi/status/envoypatchpolicy.go
+++ b/internal/gatewayapi/status/envoypatchpolicy.go
@@ -7,7 +7,6 @@ package status
 
 import (
 	"strings"
-	"time"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
@@ -30,14 +29,14 @@ func SetProgrammedForEnvoyPatchPolicy(s *gwapiv1.PolicyStatus) {
 	}
 
 	message := "Patches have been successfully applied."
-	cond := newCondition(string(egv1a1.PolicyConditionProgrammed), metav1.ConditionTrue, string(egv1a1.PolicyReasonProgrammed), message, time.Now(), 0)
+	cond := newCondition(string(egv1a1.PolicyConditionProgrammed), metav1.ConditionTrue, string(egv1a1.PolicyReasonProgrammed), message, 0)
 	for i := range s.Ancestors {
 		s.Ancestors[i].Conditions = MergeConditions(s.Ancestors[i].Conditions, cond)
 	}
 }
 
 func SetTranslationErrorForEnvoyPatchPolicy(s *gwapiv1.PolicyStatus, errMsg string) {
-	cond := newCondition(string(egv1a1.PolicyConditionProgrammed), metav1.ConditionFalse, string(egv1a1.PolicyReasonInvalid), errMsg, time.Now(), 0)
+	cond := newCondition(string(egv1a1.PolicyConditionProgrammed), metav1.ConditionFalse, string(egv1a1.PolicyReasonInvalid), errMsg, 0)
 	for i := range s.Ancestors {
 		s.Ancestors[i].Conditions = MergeConditions(s.Ancestors[i].Conditions, cond)
 	}
@@ -45,7 +44,7 @@ func SetTranslationErrorForEnvoyPatchPolicy(s *gwapiv1.PolicyStatus, errMsg stri
 
 func SetResourceNotFoundErrorForEnvoyPatchPolicy(s *gwapiv1.PolicyStatus, notFoundResources []string) {
 	message := "Unable to find xds resources: " + strings.Join(notFoundResources, ",")
-	cond := newCondition(string(egv1a1.PolicyConditionProgrammed), metav1.ConditionFalse, string(egv1a1.PolicyReasonResourceNotFound), message, time.Now(), 0)
+	cond := newCondition(string(egv1a1.PolicyConditionProgrammed), metav1.ConditionFalse, string(egv1a1.PolicyReasonResourceNotFound), message, 0)
 	for i := range s.Ancestors {
 		s.Ancestors[i].Conditions = MergeConditions(s.Ancestors[i].Conditions, cond)
 	}
diff --git a/internal/gatewayapi/status/gateway.go b/internal/gatewayapi/status/gateway.go
index fd4785b33..b42fc1539 100644
--- a/internal/gatewayapi/status/gateway.go
+++ b/internal/gatewayapi/status/gateway.go
@@ -8,7 +8,6 @@ package status
 import (
 	"fmt"
 	"slices"
-	"time"
 
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
@@ -19,14 +18,14 @@ import (
 )
 
 func UpdateGatewayStatusNotAccepted(gw *gwapiv1.Gateway, reason gwapiv1.GatewayConditionReason, msg string) *gwapiv1.Gateway {
-	cond := newCondition(string(gwapiv1.GatewayConditionAccepted), metav1.ConditionFalse, string(reason), msg, time.Now(), gw.Generation)
+	cond := newCondition(string(gwapiv1.GatewayConditionAccepted), metav1.ConditionFalse, string(reason), msg, gw.Generation)
 	gw.Status.Conditions = MergeConditions(gw.Status.Conditions, cond)
 	return gw
 }
 
 func UpdateGatewayStatusAccepted(gw *gwapiv1.Gateway) *gwapiv1.Gateway {
 	cond := newCondition(string(gwapiv1.GatewayConditionAccepted), metav1.ConditionTrue,
-		string(gwapiv1.GatewayReasonAccepted), "The Gateway has been scheduled by Envoy Gateway", time.Now(), gw.Generation)
+		string(gwapiv1.GatewayReasonAccepted), "The Gateway has been scheduled by Envoy Gateway", gw.Generation)
 	gw.Status.Conditions = MergeConditions(gw.Status.Conditions, cond)
 	return gw
 }
@@ -144,7 +143,6 @@ func SetGatewayListenerStatusCondition(gateway *gwapiv1.Gateway, listenerStatusI
 		Reason:             string(reason),
 		Message:            message,
 		ObservedGeneration: gateway.Generation,
-		LastTransitionTime: metav1.NewTime(time.Now()),
 	}
 	gateway.Status.Listeners[listenerStatusIdx].Conditions = MergeConditions(gateway.Status.Listeners[listenerStatusIdx].Conditions, cond)
 }
@@ -162,14 +160,14 @@ func updateGatewayProgrammedCondition(gw *gwapiv1.Gateway, envoyObj client.Objec
 	if len(gw.Status.Addresses) == 0 {
 		gw.Status.Conditions = MergeConditions(gw.Status.Conditions,
 			newCondition(string(gwapiv1.GatewayConditionProgrammed), metav1.ConditionFalse, string(gwapiv1.GatewayReasonAddressNotAssigned),
-				messageAddressNotAssigned, time.Now(), gw.Generation))
+				messageAddressNotAssigned, gw.Generation))
 		return
 	}
 
 	if len(gw.Status.Addresses) > 16 {
 		gw.Status.Conditions = MergeConditions(gw.Status.Conditions,
 			newCondition(string(gwapiv1.GatewayConditionProgrammed), metav1.ConditionFalse, string(gwapiv1.GatewayReasonInvalid),
-				fmt.Sprintf(messageFmtTooManyAddresses, len(gw.Status.Addresses)), time.Now(), gw.Generation))
+				fmt.Sprintf(messageFmtTooManyAddresses, len(gw.Status.Addresses)), gw.Generation))
 
 		// Truncate the addresses to 16
 		// so that the status can be updated successfully.
@@ -183,14 +181,14 @@ func updateGatewayProgrammedCondition(gw *gwapiv1.Gateway, envoyObj client.Objec
 		if obj != nil && obj.Status.AvailableReplicas > 0 {
 			gw.Status.Conditions = MergeConditions(gw.Status.Conditions,
 				newCondition(string(gwapiv1.GatewayConditionProgrammed), metav1.ConditionTrue, string(gwapiv1.GatewayConditionProgrammed),
-					fmt.Sprintf(messageFmtProgrammed, obj.Status.AvailableReplicas, obj.Status.Replicas), time.Now(), gw.Generation))
+					fmt.Sprintf(messageFmtProgrammed, obj.Status.AvailableReplicas, obj.Status.Replicas), gw.Generation))
 			return
 		}
 	case *appsv1.DaemonSet:
 		if obj != nil && obj.Status.NumberAvailable > 0 {
 			gw.Status.Conditions = MergeConditions(gw.Status.Conditions,
 				newCondition(string(gwapiv1.GatewayConditionProgrammed), metav1.ConditionTrue, string(gwapiv1.GatewayConditionProgrammed),
-					fmt.Sprintf(messageFmtProgrammed, obj.Status.NumberAvailable, obj.Status.CurrentNumberScheduled), time.Now(), gw.Generation))
+					fmt.Sprintf(messageFmtProgrammed, obj.Status.NumberAvailable, obj.Status.CurrentNumberScheduled), gw.Generation))
 			return
 		}
 	}
@@ -199,7 +197,7 @@ func updateGatewayProgrammedCondition(gw *gwapiv1.Gateway, envoyObj client.Objec
 	// Envoy DaemonSet, don't mark the Gateway as ready yet.
 	gw.Status.Conditions = MergeConditions(gw.Status.Conditions,
 		newCondition(string(gwapiv1.GatewayConditionProgrammed), metav1.ConditionFalse, string(gwapiv1.GatewayReasonNoResources),
-			messageNoResources, time.Now(), gw.Generation))
+			messageNoResources, gw.Generation))
 }
 
 // GetGatewayListenerStatusConditions returns the status conditions for a specific listener in the gateway status.
diff --git a/internal/gatewayapi/status/gatewayclass.go b/internal/gatewayapi/status/gatewayclass.go
index 6033b2501..28e2e7b6b 100644
--- a/internal/gatewayapi/status/gatewayclass.go
+++ b/internal/gatewayapi/status/gatewayclass.go
@@ -14,8 +14,6 @@
 package status
 
 import (
-	"time"
-
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/sets"
 	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
@@ -56,7 +54,6 @@ func computeGatewayClassAcceptedCondition(gatewayClass *gwapiv1.GatewayClass,
 			Reason:             reason,
 			Message:            msg,
 			ObservedGeneration: gatewayClass.Generation,
-			LastTransitionTime: metav1.NewTime(time.Now()),
 		}
 	default:
 		return metav1.Condition{
@@ -65,7 +62,6 @@ func computeGatewayClassAcceptedCondition(gatewayClass *gwapiv1.GatewayClass,
 			Reason:             reason,
 			Message:            msg,
 			ObservedGeneration: gatewayClass.Generation,
-			LastTransitionTime: metav1.NewTime(time.Now()),
 		}
 	}
 }
diff --git a/internal/gatewayapi/status/policy.go b/internal/gatewayapi/status/policy.go
index 6f3a1c61e..e62bed8fa 100644
--- a/internal/gatewayapi/status/policy.go
+++ b/internal/gatewayapi/status/policy.go
@@ -7,7 +7,6 @@ package status
 
 import (
 	"sort"
-	"time"
 
 	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -104,14 +103,14 @@ func SetConditionForPolicyAncestor(policyStatus *gwapiv1.PolicyStatus, ancestorR
 			}
 
 			// Only create condition and merge if needed
-			cond := newCondition(string(conditionType), status, string(reason), sanitizedMessage, time.Now(), generation)
+			cond := newCondition(string(conditionType), status, string(reason), sanitizedMessage, generation)
 			policyStatus.Ancestors[i].Conditions = MergeConditions(policyStatus.Ancestors[i].Conditions, cond)
 			return
 		}
 	}
 
 	// Add condition for new PolicyAncestorStatus
-	cond := newCondition(string(conditionType), status, string(reason), sanitizedMessage, time.Now(), generation)
+	cond := newCondition(string(conditionType), status, string(reason), sanitizedMessage, generation)
 	policyStatus.Ancestors = append(policyStatus.Ancestors, gwapiv1.PolicyAncestorStatus{
 		AncestorRef:    *ancestorRef,
 		ControllerName: gwapiv1a2.GatewayController(controllerName),
diff --git a/internal/gatewayapi/status/route.go b/internal/gatewayapi/status/route.go
index 7a5c44b19..5dfc4a022 100644
--- a/internal/gatewayapi/status/route.go
+++ b/internal/gatewayapi/status/route.go
@@ -6,8 +6,6 @@
 package status
 
 import (
-	"time"
-
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
 )
@@ -21,7 +19,6 @@ func SetRouteStatusCondition(route *gwapiv1.RouteStatus, routeParentStatusIdx in
 		Reason:             string(reason),
 		Message:            message,
 		ObservedGeneration: routeGeneration,
-		LastTransitionTime: metav1.NewTime(time.Now()),
 	}
 
 	route.Parents[routeParentStatusIdx].Conditions = MergeConditions(route.Parents[routeParentStatusIdx].Conditions, cond)
diff --git a/internal/provider/kubernetes/status.go b/internal/provider/kubernetes/status.go
index bf038249d..b3aec665f 100644
--- a/internal/provider/kubernetes/status.go
+++ b/internal/provider/kubernetes/status.go
@@ -9,6 +9,7 @@ import (
 	"context"
 	"fmt"
 
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -45,7 +46,8 @@ func (r *gatewayAPIReconciler) updateStatusFromSubscriptions(ctx context.Context
 							panic(fmt.Sprintf("unsupported object type %T", obj))
 						}
 						gcCopy := gc.DeepCopy()
-						gcCopy.Status = *update.Value
+						gcCopy.Status = *update.Value.DeepCopy()
+						setLastTransitionTimeInConditions(gcCopy.Status.Conditions, metav1.Now())
 						return gcCopy
 					}),
 				})
@@ -101,13 +103,15 @@ func (r *gatewayAPIReconciler) updateStatusFromSubscriptions(ctx context.Context
 							errChan <- err
 							panic(err)
 						}
+						valCopy := val.DeepCopy()
+						setLastTransitionTimeInConditionsForRouteStatus(&valCopy.RouteStatus, metav1.Now())
 						hCopy := &gwapiv1.HTTPRoute{
 							TypeMeta:   h.TypeMeta,
 							ObjectMeta: h.ObjectMeta,
 							Spec:       h.Spec,
 							Status: gwapiv1.HTTPRouteStatus{
 								RouteStatus: gwapiv1.RouteStatus{
-									Parents: mergeRouteParentStatus(h.Namespace, h.Status.Parents, val.Parents),
+									Parents: mergeRouteParentStatus(h.Namespace, h.Status.Parents, valCopy.Parents),
 								},
 							},
 						}
@@ -139,13 +143,15 @@ func (r *gatewayAPIReconciler) updateStatusFromSubscriptions(ctx context.Context
 							errChan <- err
 							panic(err)
 						}
+						valCopy := val.DeepCopy()
+						setLastTransitionTimeInConditionsForRouteStatus(&valCopy.RouteStatus, metav1.Now())
 						gCopy := &gwapiv1.GRPCRoute{
 							TypeMeta:   g.TypeMeta,
 							ObjectMeta: g.ObjectMeta,
 							Spec:       g.Spec,
 							Status: gwapiv1.GRPCRouteStatus{
 								RouteStatus: gwapiv1.RouteStatus{
-									Parents: mergeRouteParentStatus(g.Namespace, g.Status.Parents, val.Parents),
+									Parents: mergeRouteParentStatus(g.Namespace, g.Status.Parents, valCopy.Parents),
 								},
 							},
 						}
@@ -179,13 +185,15 @@ func (r *gatewayAPIReconciler) updateStatusFromSubscriptions(ctx context.Context
 							errChan <- err
 							panic(err)
 						}
+						valCopy := val.DeepCopy()
+						setLastTransitionTimeInConditionsForRouteStatus(&valCopy.RouteStatus, metav1.Now())
 						tCopy := &gwapiv1a3.TLSRoute{
 							TypeMeta:   t.TypeMeta,
 							ObjectMeta: t.ObjectMeta,
 							Spec:       t.Spec,
 							Status: gwapiv1a2.TLSRouteStatus{
 								RouteStatus: gwapiv1.RouteStatus{
-									Parents: mergeRouteParentStatus(t.Namespace, t.Status.Parents, val.Parents),
+									Parents: mergeRouteParentStatus(t.Namespace, t.Status.Parents, valCopy.Parents),
 								},
 							},
 						}
@@ -219,13 +227,15 @@ func (r *gatewayAPIReconciler) updateStatusFromSubscriptions(ctx context.Context
 							errChan <- err
 							panic(err)
 						}
+						valCopy := val.DeepCopy()
+						setLastTransitionTimeInConditionsForRouteStatus(&valCopy.RouteStatus, metav1.Now())
 						tCopy := &gwapiv1a2.TCPRoute{
 							TypeMeta:   t.TypeMeta,
 							ObjectMeta: t.ObjectMeta,
 							Spec:       t.Spec,
 							Status: gwapiv1a2.TCPRouteStatus{
 								RouteStatus: gwapiv1.RouteStatus{
-									Parents: mergeRouteParentStatus(t.Namespace, t.Status.Parents, val.Parents),
+									Parents: mergeRouteParentStatus(t.Namespace, t.Status.Parents, valCopy.Parents),
 								},
 							},
 						}
@@ -259,13 +269,15 @@ func (r *gatewayAPIReconciler) updateStatusFromSubscriptions(ctx context.Context
 							errChan <- err
 							panic(err)
 						}
+						valCopy := val.DeepCopy()
+						setLastTransitionTimeInConditionsForRouteStatus(&valCopy.RouteStatus, metav1.Now())
 						uCopy := &gwapiv1a2.UDPRoute{
 							TypeMeta:   u.TypeMeta,
 							ObjectMeta: u.ObjectMeta,
 							Spec:       u.Spec,
 							Status: gwapiv1a2.UDPRouteStatus{
 								RouteStatus: gwapiv1.RouteStatus{
-									Parents: mergeRouteParentStatus(u.Namespace, u.Status.Parents, val.Parents),
+									Parents: mergeRouteParentStatus(u.Namespace, u.Status.Parents, valCopy.Parents),
 								},
 							},
 						}
@@ -299,11 +311,13 @@ func (r *gatewayAPIReconciler) updateStatusFromSubscriptions(ctx context.Context
 							errChan <- err
 							panic(err)
 						}
+						valCopy := val.DeepCopy()
+						setLastTransitionTimeInConditionsForPolicyStatus(valCopy, metav1.Now())
 						tCopy := &egv1a1.EnvoyPatchPolicy{
 							TypeMeta:   t.TypeMeta,
 							ObjectMeta: t.ObjectMeta,
 							Spec:       t.Spec,
-							Status:     *val,
+							Status:     *valCopy,
 						}
 						return tCopy
 					}),
@@ -335,11 +349,13 @@ func (r *gatewayAPIReconciler) updateStatusFromSubscriptions(ctx context.Context
 							errChan <- err
 							panic(err)
 						}
+						valCopy := val.DeepCopy()
+						setLastTransitionTimeInConditionsForPolicyStatus(valCopy, metav1.Now())
 						tCopy := &egv1a1.ClientTrafficPolicy{
 							TypeMeta:   t.TypeMeta,
 							ObjectMeta: t.ObjectMeta,
 							Spec:       t.Spec,
-							Status:     *val,
+							Status:     *valCopy,
 						}
 						return tCopy
 					}),
@@ -371,11 +387,13 @@ func (r *gatewayAPIReconciler) updateStatusFromSubscriptions(ctx context.Context
 							errChan <- err
 							panic(err)
 						}
+						valCopy := val.DeepCopy()
+						setLastTransitionTimeInConditionsForPolicyStatus(valCopy, metav1.Now())
 						tCopy := &egv1a1.BackendTrafficPolicy{
 							TypeMeta:   t.TypeMeta,
 							ObjectMeta: t.ObjectMeta,
 							Spec:       t.Spec,
-							Status:     *val,
+							Status:     *valCopy,
 						}
 						return tCopy
 					}),
@@ -407,11 +425,13 @@ func (r *gatewayAPIReconciler) updateStatusFromSubscriptions(ctx context.Context
 							errChan <- err
 							panic(err)
 						}
+						valCopy := val.DeepCopy()
+						setLastTransitionTimeInConditionsForPolicyStatus(valCopy, metav1.Now())
 						tCopy := &egv1a1.SecurityPolicy{
 							TypeMeta:   t.TypeMeta,
 							ObjectMeta: t.ObjectMeta,
 							Spec:       t.Spec,
-							Status:     *val,
+							Status:     *valCopy,
 						}
 						return tCopy
 					}),
@@ -441,11 +461,13 @@ func (r *gatewayAPIReconciler) updateStatusFromSubscriptions(ctx context.Context
 							errChan <- err
 							panic(err)
 						}
+						valCopy := val.DeepCopy()
+						setLastTransitionTimeInConditionsForPolicyStatus(valCopy, metav1.Now())
 						tCopy := &gwapiv1.BackendTLSPolicy{
 							TypeMeta:   t.TypeMeta,
 							ObjectMeta: t.ObjectMeta,
 							Spec:       t.Spec,
-							Status:     *val,
+							Status:     *valCopy,
 						}
 						return tCopy
 					}),
@@ -477,11 +499,13 @@ func (r *gatewayAPIReconciler) updateStatusFromSubscriptions(ctx context.Context
 							errChan <- err
 							panic(err)
 						}
+						valCopy := val.DeepCopy()
+						setLastTransitionTimeInConditionsForPolicyStatus(valCopy, metav1.Now())
 						tCopy := &egv1a1.EnvoyExtensionPolicy{
 							TypeMeta:   t.TypeMeta,
 							ObjectMeta: t.ObjectMeta,
 							Spec:       t.Spec,
-							Status:     *val,
+							Status:     *valCopy,
 						}
 						return tCopy
 					}),
@@ -513,11 +537,13 @@ func (r *gatewayAPIReconciler) updateStatusFromSubscriptions(ctx context.Context
 							errChan <- err
 							panic(err)
 						}
+						valCopy := val.DeepCopy()
+						setLastTransitionTimeInConditions(valCopy.Conditions, metav1.Now())
 						tCopy := &egv1a1.Backend{
 							TypeMeta:   t.TypeMeta,
 							ObjectMeta: t.ObjectMeta,
 							Spec:       t.Spec,
-							Status:     *val,
+							Status:     *valCopy,
 						}
 						return tCopy
 					}),
@@ -554,7 +580,9 @@ func (r *gatewayAPIReconciler) updateStatusFromSubscriptions(ctx context.Context
 								panic(err)
 							}
 							tCopy := t.DeepCopy()
-							tCopy.Object["status"] = *val
+							valCopy := val.DeepCopy()
+							setLastTransitionTimeInConditionsForPolicyStatus(valCopy, metav1.Now())
+							tCopy.Object["status"] = *valCopy
 							return tCopy
 						}),
 					})
@@ -653,10 +681,33 @@ func (r *gatewayAPIReconciler) updateStatusForGateway(ctx context.Context, gtw *
 				panic(fmt.Sprintf("unsupported object type %T", obj))
 			}
 			gCopy := g.DeepCopy()
-			gCopy.Status.Conditions = gtw.Status.Conditions
-			gCopy.Status.Addresses = gtw.Status.Addresses
-			gCopy.Status.Listeners = gtw.Status.Listeners
+			gCopy.Status = *gtw.Status.DeepCopy()
+			setLastTransitionTimeInConditions(gCopy.Status.Conditions, metav1.Now())
+			for i := range gCopy.Status.Listeners {
+				setLastTransitionTimeInConditions(gCopy.Status.Listeners[i].Conditions, metav1.Now())
+			}
 			return gCopy
 		}),
 	})
 }
+
+// setLastTransitionTimeInConditions sets LastTransitionTime to the given time for all conditions in a slice
+func setLastTransitionTimeInConditions(conditions []metav1.Condition, now metav1.Time) {
+	for i := range conditions {
+		conditions[i].LastTransitionTime = now
+	}
+}
+
+// setLastTransitionTimeInConditionsForPolicyStatus sets LastTransitionTime for Policy status conditions
+func setLastTransitionTimeInConditionsForPolicyStatus(status *gwapiv1.PolicyStatus, now metav1.Time) {
+	for i := range status.Ancestors {
+		setLastTransitionTimeInConditions(status.Ancestors[i].Conditions, now)
+	}
+}
+
+// setLastTransitionTimeInConditionsForRouteStatus sets LastTransitionTime for Route status conditions
+func setLastTransitionTimeInConditionsForRouteStatus(status *gwapiv1.RouteStatus, now metav1.Time) {
+	for i := range status.Parents {
+		setLastTransitionTimeInConditions(status.Parents[i].Conditions, now)
+	}
+}
diff --git a/tools/make/lint.mk b/tools/make/lint.mk
index 43db54cc1..fbdc23164 100644
--- a/tools/make/lint.mk
+++ b/tools/make/lint.mk
@@ -101,4 +101,6 @@ lint.markdown:
 .PHONY: lint.dependabot
 lint: lint.dependabot
 lint.dependabot: ## Check if dependabot configuration is valid
-	@npx @bugron/validate-dependabot-yaml .github/dependabot.yml
+	@$(LOG_TARGET)
+	# TODO: re-enable this when https://json.schemastore.org/dependabot-2.0.json is back to normal.
+	@#npx @bugron/validate-dependabot-yaml .github/dependabot.yml



Test patch:
diff --git a/internal/gatewayapi/status/conditions_test.go b/internal/gatewayapi/status/conditions_test.go
index e61e732b8..e43a7c719 100644
--- a/internal/gatewayapi/status/conditions_test.go
+++ b/internal/gatewayapi/status/conditions_test.go
@@ -17,17 +17,12 @@ import (
 	"errors"
 	"strings"
 	"testing"
-	"time"
 
 	"github.com/stretchr/testify/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	utilclock "k8s.io/utils/clock"
-	fakeclock "k8s.io/utils/clock/testing"
 	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
 )
 
-var clock utilclock.Clock = utilclock.RealClock{}
-
 func TestConditionChanged(t *testing.T) {
 	testCases := []struct {
 		name     string
@@ -95,17 +90,6 @@ func TestConditionChanged(t *testing.T) {
 }
 
 func TestMergeConditions(t *testing.T) {
-	// Inject a fake clock and don't forget to reset it
-	fakeClock := fakeclock.NewFakeClock(time.Time{})
-	clock = fakeClock
-	defer func() {
-		clock = utilclock.RealClock{}
-	}()
-
-	start := fakeClock.Now()
-	middle := start.Add(1 * time.Minute)
-	later := start.Add(2 * time.Minute)
-
 	gen := int64(1)
 
 	testCases := []struct {
@@ -117,69 +101,65 @@ func TestMergeConditions(t *testing.T) {
 		{
 			name: "status updated",
 			current: []metav1.Condition{
-				newCondition("available", "false", "Reason", "Message", start, gen),
+				newCondition("available", "false", "Reason", "Message", gen),
 			},
 			updates: []metav1.Condition{
-				newCondition("available", "true", "Reason", "Message", middle, gen),
+				newCondition("available", "true", "Reason", "Message", gen),
 			},
 			expected: []metav1.Condition{
-				newCondition("available", "true", "Reason", "Message", middle, gen),
+				newCondition("available", "true", "Reason", "Message", gen),
 			},
 		},
 		{
 			name: "reason updated",
 			current: []metav1.Condition{
-				newCondition("available", "false", "Reason", "Message", start, gen),
+				newCondition("available", "false", "Reason", "Message", gen),
 			},
 			updates: []metav1.Condition{
-				newCondition("available", "false", "New Reason", "Message", middle, gen),
+				newCondition("available", "false", "New Reason", "Message", gen),
 			},
 			expected: []metav1.Condition{
-				newCondition("available", "false", "New Reason", "Message", middle, gen),
+				newCondition("available", "false", "New Reason", "Message", gen),
 			},
 		},
 		{
 			name: "message updated",
 			current: []metav1.Condition{
-				newCondition("available", "false", "Reason", "Message", start, gen),
+				newCondition("available", "false", "Reason", "Message", gen),
 			},
 			updates: []metav1.Condition{
-				newCondition("available", "false", "Reason", "New Message", middle, gen),
+				newCondition("available", "false", "Reason", "New Message", gen),
 			},
 			expected: []metav1.Condition{
-				newCondition("available", "false", "Reason", "New Message", middle, gen),
+				newCondition("available", "false", "Reason", "New Message", gen),
 			},
 		},
 		{
 			name: "observed generation updated",
 			current: []metav1.Condition{
-				newCondition("available", "false", "Reason", "Message", start, gen),
+				newCondition("available", "false", "Reason", "Message", gen),
 			},
 			updates: []metav1.Condition{
-				newCondition("available", "false", "Reason", "Message", middle, gen+1),
+				newCondition("available", "false", "Reason", "Message", gen+1),
 			},
 			expected: []metav1.Condition{
-				newCondition("available", "false", "Reason", "Message", middle, gen+1),
+				newCondition("available", "false", "Reason", "Message", gen+1),
 			},
 		},
 		{
 			name: "status unchanged",
 			current: []metav1.Condition{
-				newCondition("available", "false", "Reason", "Message", start, gen),
+				newCondition("available", "false", "Reason", "Message", gen),
 			},
 			updates: []metav1.Condition{
-				newCondition("available", "false", "Reason", "Message", middle, gen),
+				newCondition("available", "false", "Reason", "Message", gen),
 			},
 			expected: []metav1.Condition{
-				newCondition("available", "false", "Reason", "Message", start, gen),
+				newCondition("available", "false", "Reason", "Message", gen),
 			},
 		},
 	}
 
-	// Simulate the passage of time between original condition creation
-	// and update processing
-	fakeClock.SetTime(later)
-
 	for _, tc := range testCases {
 		got := MergeConditions(tc.current, tc.updates...)
 		assert.ElementsMatch(t, tc.expected, got, tc.name)
@@ -188,7 +168,7 @@ func TestMergeConditions(t *testing.T) {
 
 func TestMergeConditionsTruncatesMessages(t *testing.T) {
 	longMsg := strings.Repeat("x", conditionMessageMaxLength+5)
-	cond := newCondition("available", metav1.ConditionTrue, "Reason", longMsg, time.Now(), 1)
+	cond := newCondition("available", metav1.ConditionTrue, "Reason", longMsg, 1)
 	conditions := MergeConditions(nil, cond)
 
 	if assert.Len(t, conditions, 1) {