Repository: argoproj/argo-cd
Pull request #23096 (2025-05-22 03:52:31)

chore(deps): bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc from 1.35.0 to 1.36.0
Bumps [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc](https://github.com/open-telemetry/opentelemetry-go) from 1.35.0 to 1.36.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md">go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc's changelog</a>.</em></p>
<blockquote>
<h2>[1.36.0/0.58.0/0.12.0] 2025-05-20</h2>
<h3>Added</h3>
<ul>
<li>Add exponential histogram support in <code>go.opentelemetry.io/otel/exporters/prometheus</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6421">#6421</a>)</li>
<li>The <code>go.opentelemetry.io/otel/semconv/v1.31.0</code> package.
The package contains semantic conventions from the <code>v1.31.0</code> version of the OpenTelemetry Semantic Conventions.
See the <a href="https://github.com/open-telemetry/opentelemetry-go/blob/main/semconv/v1.31.0/MIGRATION.md">migration documentation</a> for information on how to upgrade from <code>go.opentelemetry.io/otel/semconv/v1.30.0</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6479">#6479</a>)</li>
<li>Add <code>Recording</code>, <code>Scope</code>, and <code>Record</code> types in <code>go.opentelemetry.io/otel/log/logtest</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6507">#6507</a>)</li>
<li>Add <code>WithHTTPClient</code> option to configure the <code>http.Client</code> used by <code>go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6751">#6751</a>)</li>
<li>Add <code>WithHTTPClient</code> option to configure the <code>http.Client</code> used by <code>go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6752">#6752</a>)</li>
<li>Add <code>WithHTTPClient</code> option to configure the <code>http.Client</code> used by <code>go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6688">#6688</a>)</li>
<li>Add <code>ValuesGetter</code> in <code>go.opentelemetry.io/otel/propagation</code>, a <code>TextMapCarrier</code> that supports retrieving multiple values for a single key. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5973">#5973</a>)</li>
<li>Add <code>Values</code> method to <code>HeaderCarrier</code> to implement the new <code>ValuesGetter</code> interface in <code>go.opentelemetry.io/otel/propagation</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5973">#5973</a>)</li>
<li>Update <code>Baggage</code> in <code>go.opentelemetry.io/otel/propagation</code> to retrieve multiple values for a key when the carrier implements <code>ValuesGetter</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/5973">#5973</a>)</li>
<li>Add <code>AssertEqual</code> function in <code>go.opentelemetry.io/otel/log/logtest</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6662">#6662</a>)</li>
<li>The <code>go.opentelemetry.io/otel/semconv/v1.32.0</code> package.
The package contains semantic conventions from the <code>v1.32.0</code> version of the OpenTelemetry Semantic Conventions.
See the <a href="https://github.com/open-telemetry/opentelemetry-go/blob/main/semconv/v1.32.0/MIGRATION.md">migration documentation</a> for information on how to upgrade from <code>go.opentelemetry.io/otel/semconv/v1.31.0</code>(<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6782">#6782</a>)</li>
<li>Add <code>Transform</code> option in <code>go.opentelemetry.io/otel/log/logtest</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6794">#6794</a>)</li>
<li>Add <code>Desc</code> option in <code>go.opentelemetry.io/otel/log/logtest</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6796">#6796</a>)</li>
</ul>
<h3>Removed</h3>
<ul>
<li>Drop support for [Go 1.22]. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6381">#6381</a>, <a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6418">#6418</a>)</li>
<li>Remove <code>Resource</code> field from <code>EnabledParameters</code> in <code>go.opentelemetry.io/otel/sdk/log</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6494">#6494</a>)</li>
<li>Remove <code>RecordFactory</code> type from <code>go.opentelemetry.io/otel/log/logtest</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6492">#6492</a>)</li>
<li>Remove <code>ScopeRecords</code>, <code>EmittedRecord</code>, and <code>RecordFactory</code> types from <code>go.opentelemetry.io/otel/log/logtest</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6507">#6507</a>)</li>
<li>Remove <code>AssertRecordEqual</code> function in <code>go.opentelemetry.io/otel/log/logtest</code>, use <code>AssertEqual</code> instead. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6662">#6662</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>⚠️ Update <code>github.com/prometheus/client_golang</code> to <code>v1.21.1</code>, which changes the <code>NameValidationScheme</code> to <code>UTF8Validation</code>.
This allows metrics names to keep original delimiters (e.g. <code>.</code>), rather than replacing with underscores.
This can be reverted by setting <code>github.com/prometheus/common/model.NameValidationScheme</code> to <code>LegacyValidation</code> in <code>github.com/prometheus/common/model</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6433">#6433</a>)</li>
<li>Initialize map with <code>len(keys)</code> in <code>NewAllowKeysFilter</code> and <code>NewDenyKeysFilter</code> to avoid unnecessary allocations in <code>go.opentelemetry.io/otel/attribute</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6455">#6455</a>)</li>
<li><code>go.opentelemetry.io/otel/log/logtest</code> is now a separate Go module. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6465">#6465</a>)</li>
<li><code>go.opentelemetry.io/otel/sdk/log/logtest</code> is now a separate Go module. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6466">#6466</a>)</li>
<li><code>Recorder</code> in <code>go.opentelemetry.io/otel/log/logtest</code> no longer separately stores records emitted by loggers with the same instrumentation scope. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6507">#6507</a>)</li>
<li>Improve performance of <code>BatchProcessor</code> in <code>go.opentelemetry.io/otel/sdk/log</code> by not exporting when exporter cannot accept more. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6569">#6569</a>, <a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6641">#6641</a>)</li>
</ul>
<h3>Deprecated</h3>
<ul>
<li>Deprecate support for <code>model.LegacyValidation</code> for <code>go.opentelemetry.io/otel/exporters/prometheus</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6449">#6449</a>)</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Stop percent encoding header environment variables in <code>go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc</code> and <code>go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6392">#6392</a>)</li>
<li>Ensure the <code>noopSpan.tracerProvider</code> method is not inlined in <code>go.opentelemetry.io/otel/trace</code> so the <code>go.opentelemetry.io/auto</code> instrumentation can instrument non-recording spans. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6456">#6456</a>)</li>
<li>Use a <code>sync.Pool</code> instead of allocating <code>metricdata.ResourceMetrics</code> in <code>go.opentelemetry.io/otel/exporters/prometheus</code>. (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6472">#6472</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/a85ae98dcedc0761078518a715dea53e519b4846"><code>a85ae98</code></a> Release v1.36.0/v0.58.0/v0.12.0 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6793">#6793</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/3f85c350380b2720a919fd156da321b488f3b746"><code>3f85c35</code></a> logtest: add Desc (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6796">#6796</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/d2fff768dc438b4bb0a4b0252cd0249c8ece9289"><code>d2fff76</code></a> chore(deps): update module github.com/uudashr/iface to v1.4.0 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6798">#6798</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/2d4c9dc115bf8fd047f6248b489802a6f5c22d1d"><code>2d4c9dc</code></a> Add <code>semconv/v1.32.0</code> (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6782">#6782</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/fe523bd15a09be9db1f2752d1cbbcf4e50762ab3"><code>fe523bd</code></a> logtest: add Transform (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6794">#6794</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/e57879908fbd71f0fe64c4905c579d5827a34779"><code>e578799</code></a> fix(deps): update googleapis to 55703ea (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6795">#6795</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/af189e5a37c17de074d27f83921007756ce5ce51"><code>af189e5</code></a> chore(deps): update module github.com/golangci/misspell to v0.7.0 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6792">#6792</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/836a2ce69db65ff7f106eea7967d1fe6d5b491b2"><code>836a2ce</code></a> chore(deps): update module github.com/alecthomas/chroma/v2 to v2.18.0 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6790">#6790</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/93f8f13abe5acedf02b7eed1f585c96e48fa35c0"><code>93f8f13</code></a> chore(deps): update github/codeql-action action to v3.28.18 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6789">#6789</a>)</li>
<li><a href="https://github.com/open-telemetry/opentelemetry-go/commit/1556ff2aeb34bd7cfd2ef035fd7fa2f5942e71f9"><code>1556ff2</code></a> chore(deps): update codecov/codecov-action action to v5.4.3 (<a href="https://redirect.github.com/open-telemetry/opentelemetry-go/issues/6785">#6785</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/open-telemetry/opentelemetry-go/compare/v1.35.0...v1.36.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc&package-manager=go_modules&previous-version=1.35.0&new-version=1.36.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/argoproj/argo-cd/pull/23096?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 59.96%. Comparing base [(`4febded`)](https://app.codecov.io/gh/argoproj/argo-cd/commit/4febded784fd5c0639fff9acbfb240cfe58c344a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj) to head [(`ab1bd58`)](https://app.codecov.io/gh/argoproj/argo-cd/commit/ab1bd58029d744a997185449b5b2f1ddb2b056d7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master   #23096      +/-   ##
==========================================
- Coverage   59.99%   59.96%   -0.03%     
==========================================
  Files         343      343              
  Lines       57822    57822              
==========================================
- Hits        34690    34673      -17     
- Misses      20354    20368      +14     
- Partials     2778     2781       +3     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/argoproj/argo-cd/pull/23096?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=argoproj).


Patch:
diff --git a/.github/workflows/ci-build.yaml b/.github/workflows/ci-build.yaml
index 08df0d063..e1afc4a76 100644
--- a/.github/workflows/ci-build.yaml
+++ b/.github/workflows/ci-build.yaml
@@ -494,7 +494,7 @@ jobs:
           git config --global user.email "john.doe@example.com"
       - name: Pull Docker image required for tests
         run: |
-          docker pull ghcr.io/dexidp/dex:v2.41.1
+          docker pull ghcr.io/dexidp/dex:v2.43.0
           docker pull argoproj/argo-cd-ci-builder:v1.0.0
           docker pull redis:7.2.7-alpine
       - name: Create target directory for binaries in the build-process
diff --git a/cmd/argocd-application-controller/commands/argocd_application_controller.go b/cmd/argocd-application-controller/commands/argocd_application_controller.go
index 49a71f746..d9f3e2d9c 100644
--- a/cmd/argocd-application-controller/commands/argocd_application_controller.go
+++ b/cmd/argocd-application-controller/commands/argocd_application_controller.go
@@ -68,6 +68,7 @@ func NewCommand() *cobra.Command {
 		selfHealBackoffTimeoutSeconds    int
 		selfHealBackoffFactor            int
 		selfHealBackoffCapSeconds        int
+		selfHealBackoffCooldownSeconds   int
 		syncTimeout                      int
 		statusProcessors                 int
 		operationProcessors              int
@@ -201,6 +202,7 @@ func NewCommand() *cobra.Command {
 				time.Duration(appResyncJitter)*time.Second,
 				time.Duration(selfHealTimeoutSeconds)*time.Second,
 				selfHealBackoff,
+				time.Duration(selfHealBackoffCooldownSeconds)*time.Second,
 				time.Duration(syncTimeout)*time.Second,
 				time.Duration(repoErrorGracePeriod)*time.Second,
 				metricsPort,
@@ -272,6 +274,7 @@ func NewCommand() *cobra.Command {
 	command.Flags().IntVar(&selfHealBackoffTimeoutSeconds, "self-heal-backoff-timeout-seconds", env.ParseNumFromEnv("ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_TIMEOUT_SECONDS", 2, 0, math.MaxInt32), "Specifies initial timeout of exponential backoff between self heal attempts")
 	command.Flags().IntVar(&selfHealBackoffFactor, "self-heal-backoff-factor", env.ParseNumFromEnv("ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_FACTOR", 3, 0, math.MaxInt32), "Specifies factor of exponential timeout between application self heal attempts")
 	command.Flags().IntVar(&selfHealBackoffCapSeconds, "self-heal-backoff-cap-seconds", env.ParseNumFromEnv("ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_CAP_SECONDS", 300, 0, math.MaxInt32), "Specifies max timeout of exponential backoff between application self heal attempts")
+	command.Flags().IntVar(&selfHealBackoffCooldownSeconds, "self-heal-backoff-cooldown-seconds", env.ParseNumFromEnv("ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_COOLDOWN_SECONDS", 330, 0, math.MaxInt32), "Specifies period of time the app needs to stay synced before the self heal backoff can reset")
 	command.Flags().IntVar(&syncTimeout, "sync-timeout", env.ParseNumFromEnv("ARGOCD_APPLICATION_CONTROLLER_SYNC_TIMEOUT", 0, 0, math.MaxInt32), "Specifies the timeout after which a sync would be terminated. 0 means no timeout (default 0).")
 	command.Flags().Int64Var(&kubectlParallelismLimit, "kubectl-parallelism-limit", env.ParseInt64FromEnv("ARGOCD_APPLICATION_CONTROLLER_KUBECTL_PARALLELISM_LIMIT", 20, 0, math.MaxInt64), "Number of allowed concurrent kubectl fork/execs. Any value less than 1 means no limit.")
 	command.Flags().BoolVar(&repoServerPlaintext, "repo-server-plaintext", env.ParseBoolFromEnv("ARGOCD_APPLICATION_CONTROLLER_REPO_SERVER_PLAINTEXT", false), "Disable TLS on connections to repo server")
diff --git a/controller/appcontroller.go b/controller/appcontroller.go
index 8a020f20c..a82ecac76 100644
--- a/controller/appcontroller.go
+++ b/controller/appcontroller.go
@@ -41,6 +41,7 @@ import (
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/tools/cache"
 	"k8s.io/client-go/util/workqueue"
+	"k8s.io/utils/ptr"
 
 	commitclient "github.com/argoproj/argo-cd/v3/commitserver/apiclient"
 	"github.com/argoproj/argo-cd/v3/common"
@@ -125,6 +126,7 @@ type ApplicationController struct {
 	statusRefreshJitter           time.Duration
 	selfHealTimeout               time.Duration
 	selfHealBackOff               *wait.Backoff
+	selfHealBackoffCooldown       time.Duration
 	syncTimeout                   time.Duration
 	db                            db.ArgoDB
 	settingsMgr                   *settings_util.SettingsManager
@@ -160,6 +162,7 @@ func NewApplicationController(
 	appResyncJitter time.Duration,
 	selfHealTimeout time.Duration,
 	selfHealBackoff *wait.Backoff,
+	selfHealBackoffCooldown time.Duration,
 	syncTimeout time.Duration,
 	repoErrorGracePeriod time.Duration,
 	metricsPort int,
@@ -206,6 +209,7 @@ func NewApplicationController(
 		settingsMgr:                       settingsMgr,
 		selfHealTimeout:                   selfHealTimeout,
 		selfHealBackOff:                   selfHealBackoff,
+		selfHealBackoffCooldown:           selfHealBackoffCooldown,
 		syncTimeout:                       syncTimeout,
 		clusterSharding:                   clusterSharding,
 		projByNameCache:                   sync.Map{},
@@ -2240,17 +2244,22 @@ func (ctrl *ApplicationController) shouldSelfHeal(app *appv1.Application, alread
 		return true, time.Duration(0)
 	}
 
-	// Reset counter if the prior sync was successful OR if the revision has changed
-	if !alreadyAttempted || app.Status.Sync.Status == appv1.SyncStatusCodeSynced {
+	var timeSinceOperation *time.Duration
+	if app.Status.OperationState.FinishedAt != nil {
+		timeSinceOperation = ptr.To(time.Since(app.Status.OperationState.FinishedAt.Time))
+	}
+
+	// Reset counter if the prior sync was successful and the cooldown period is over OR if the revision has changed
+	if !alreadyAttempted || (timeSinceOperation != nil && *timeSinceOperation >= ctrl.selfHealBackoffCooldown && app.Status.Sync.Status == appv1.SyncStatusCodeSynced) {
 		app.Status.OperationState.Operation.Sync.SelfHealAttemptsCount = 0
 	}
 
 	var retryAfter time.Duration
 	if ctrl.selfHealBackOff == nil {
-		if app.Status.OperationState.FinishedAt == nil {
+		if timeSinceOperation == nil {
 			retryAfter = ctrl.selfHealTimeout
 		} else {
-			retryAfter = ctrl.selfHealTimeout - time.Since(app.Status.OperationState.FinishedAt.Time)
+			retryAfter = ctrl.selfHealTimeout - *timeSinceOperation
 		}
 	} else {
 		backOff := *ctrl.selfHealBackOff
@@ -2260,10 +2269,11 @@ func (ctrl *ApplicationController) shouldSelfHeal(app *appv1.Application, alread
 		for i := 0; i < steps; i++ {
 			delay = backOff.Step()
 		}
-		if app.Status.OperationState.FinishedAt == nil {
+
+		if timeSinceOperation == nil {
 			retryAfter = delay
 		} else {
-			retryAfter = delay - time.Since(app.Status.OperationState.FinishedAt.Time)
+			retryAfter = delay - *timeSinceOperation
 		}
 	}
 	return retryAfter <= 0, retryAfter
diff --git a/docs/operator-manual/health.md b/docs/operator-manual/health.md
index 468f13407..cdaf053ea 100644
--- a/docs/operator-manual/health.md
+++ b/docs/operator-manual/health.md
@@ -168,7 +168,33 @@ To test the implemented custom health checks, run `go test -v ./util/lua/`.
 
 The [PR#1139](https://github.com/argoproj/argo-cd/pull/1139) is an example of Cert Manager CRDs custom health check.
 
-Please note that bundled health checks with wildcards are not supported.
+#### Wildcard Support for Built-in Health Checks
+
+You can use a single health check for multiple resources by using a wildcard in the group or kind directory names.
+
+The `_` character behaves like a `*` wildcard. For example, consider the following directory structure:
+
+```
+argo-cd
+|-- resource_customizations
+|    |-- _.group.io               # CRD group
+|    |    |-- _                   # Resource kind
+|    |    |    |-- health.lua     # Health check
+```
+
+Any resource with a group that ends with `.group.io` will use the health check in `health.lua`.
+
+Wildcard checks are only evaluated if there is no specific check for the resource.
+
+If multiple wildcard checks match, the first one in the directory structure is used.
+
+We use the [doublestar](https://github.com/bmatcuk/doublestar) glob library to match the wildcard checks. We currently
+only treat a path as a wildcard if it contains a `_` character, but this may change in the future.
+
+!!!important "Avoid Massive Scripts"
+
+    Avoid writing massive scripts to handle multiple resources. They'll get hard to read and maintain. Instead, just
+    duplicate the relevant parts in resource-specific scripts.
 
 ## Overriding Go-Based Health Checks
 
diff --git a/docs/operator-manual/server-commands/argocd-application-controller.md b/docs/operator-manual/server-commands/argocd-application-controller.md
index 852c62456..29c6b95d7 100644
--- a/docs/operator-manual/server-commands/argocd-application-controller.md
+++ b/docs/operator-manual/server-commands/argocd-application-controller.md
@@ -71,6 +71,7 @@ argocd-application-controller [flags]
       --repo-server-timeout-seconds int                           Repo server RPC call timeout seconds. (default 60)
       --request-timeout string                                    The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0")
       --self-heal-backoff-cap-seconds int                         Specifies max timeout of exponential backoff between application self heal attempts (default 300)
+      --self-heal-backoff-cooldown-seconds int                    Specifies period of time the app needs to stay synced before the self heal backoff can reset (default 330)
       --self-heal-backoff-factor int                              Specifies factor of exponential timeout between application self heal attempts (default 3)
       --self-heal-backoff-timeout-seconds int                     Specifies initial timeout of exponential backoff between self heal attempts (default 2)
       --self-heal-timeout-seconds int                             Specifies timeout between application self heal attempts
diff --git a/go.mod b/go.mod
index f0b5c9b7c..ea4174b11 100644
--- a/go.mod
+++ b/go.mod
@@ -84,9 +84,9 @@ require (
 	github.com/yuin/gopher-lua v1.1.1
 	gitlab.com/gitlab-org/api/client-go v0.116.0
 	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0
-	go.opentelemetry.io/otel v1.35.0
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
-	go.opentelemetry.io/otel/sdk v1.35.0
+	go.opentelemetry.io/otel v1.36.0
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0
+	go.opentelemetry.io/otel/sdk v1.36.0
 	go.uber.org/automaxprocs v1.6.0
 	golang.org/x/crypto v0.38.0
 	golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
@@ -95,7 +95,7 @@ require (
 	golang.org/x/sync v0.14.0
 	golang.org/x/term v0.32.0
 	golang.org/x/time v0.11.0
-	google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a
+	google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237
 	google.golang.org/grpc v1.72.1
 	google.golang.org/protobuf v1.36.6
 	gopkg.in/yaml.v2 v2.4.0
@@ -158,6 +158,7 @@ require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/blang/semver/v4 v4.0.0 // indirect
 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
+	github.com/cenkalti/backoff/v5 v5.0.2 // indirect
 	github.com/chai2010/gettext-go v1.0.2 // indirect
 	github.com/cloudflare/circl v1.6.1 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
@@ -201,7 +202,7 @@ require (
 	github.com/gosimple/unidecode v1.0.1 // indirect
 	github.com/gregdel/pushover v1.3.1 // indirect
 	github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-version v1.7.0 // indirect
 	github.com/huandu/xstrings v1.5.0 // indirect
@@ -263,10 +264,10 @@ require (
 	go.mongodb.org/mongo-driver v1.17.1 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
-	go.opentelemetry.io/otel/metric v1.35.0 // indirect
-	go.opentelemetry.io/otel/trace v1.35.0 // indirect
-	go.opentelemetry.io/proto/otlp v1.5.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
+	go.opentelemetry.io/otel/metric v1.36.0 // indirect
+	go.opentelemetry.io/otel/trace v1.36.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.6.0 // indirect
 	golang.org/x/mod v0.22.0 // indirect
 	golang.org/x/sys v0.33.0 // indirect
 	golang.org/x/text v0.25.0 // indirect
@@ -276,7 +277,7 @@ require (
 	gomodules.xyz/notify v0.1.1 // indirect
 	google.golang.org/api v0.223.0 // indirect
 	google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
diff --git a/go.sum b/go.sum
index ef8680da4..88b8b257a 100644
--- a/go.sum
+++ b/go.sum
@@ -181,6 +181,8 @@ github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QH
 github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
+github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -475,8 +477,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQ
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
 github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
 github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
@@ -832,22 +834,22 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
-go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
-go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
-go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
-go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
-go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
-go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
+go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
+go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
+go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
+go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
+go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
 go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
 go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
-go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
-go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
-go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
-go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
+go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
+go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
+go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
+go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
 go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
@@ -1216,10 +1218,10 @@ google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6D
 google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
 google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
 google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
-google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
+google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
diff --git a/manifests/base/application-controller-deployment/argocd-application-controller-deployment.yaml b/manifests/base/application-controller-deployment/argocd-application-controller-deployment.yaml
index ea40f9c9a..bf65aed35 100644
--- a/manifests/base/application-controller-deployment/argocd-application-controller-deployment.yaml
+++ b/manifests/base/application-controller-deployment/argocd-application-controller-deployment.yaml
@@ -121,6 +121,12 @@ spec:
               name: argocd-cmd-params-cm
               key: controller.self.heal.backoff.cap.seconds
               optional: true
+        - name: ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_COOLDOWN_SECONDS
+          valueFrom:
+            configMapKeyRef:
+              name: argocd-cmd-params-cm
+              key: controller.self.heal.backoff.cooldown.seconds
+              optional: true
         - name: ARGOCD_APPLICATION_CONTROLLER_SYNC_TIMEOUT
           valueFrom:
             configMapKeyRef:
diff --git a/manifests/base/application-controller/argocd-application-controller-statefulset.yaml b/manifests/base/application-controller/argocd-application-controller-statefulset.yaml
index a595b75f8..e33d3917f 100644
--- a/manifests/base/application-controller/argocd-application-controller-statefulset.yaml
+++ b/manifests/base/application-controller/argocd-application-controller-statefulset.yaml
@@ -124,6 +124,12 @@ spec:
               name: argocd-cmd-params-cm
               key: controller.self.heal.backoff.cap.seconds
               optional: true
+        - name: ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_COOLDOWN_SECONDS
+          valueFrom:
+            configMapKeyRef:
+              name: argocd-cmd-params-cm
+              key: controller.self.heal.backoff.cooldown.seconds
+              optional: true
         - name: ARGOCD_APPLICATION_CONTROLLER_SYNC_TIMEOUT
           valueFrom:
             configMapKeyRef:
diff --git a/manifests/base/dex/argocd-dex-server-deployment.yaml b/manifests/base/dex/argocd-dex-server-deployment.yaml
index 0ef89cd75..1c1203b63 100644
--- a/manifests/base/dex/argocd-dex-server-deployment.yaml
+++ b/manifests/base/dex/argocd-dex-server-deployment.yaml
@@ -37,7 +37,7 @@ spec:
             type: RuntimeDefault
       containers:
       - name: dex
-        image: ghcr.io/dexidp/dex:v2.41.1
+        image: ghcr.io/dexidp/dex:v2.43.0
         imagePullPolicy: Always
         command: [/shared/argocd-dex, rundex]
         env:
diff --git a/manifests/core-install-with-hydrator.yaml b/manifests/core-install-with-hydrator.yaml
index cc4cdef63..1feb53fcf 100644
--- a/manifests/core-install-with-hydrator.yaml
+++ b/manifests/core-install-with-hydrator.yaml
@@ -25456,6 +25456,12 @@ spec:
               key: controller.self.heal.backoff.cap.seconds
               name: argocd-cmd-params-cm
               optional: true
+        - name: ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_COOLDOWN_SECONDS
+          valueFrom:
+            configMapKeyRef:
+              key: controller.self.heal.backoff.cooldown.seconds
+              name: argocd-cmd-params-cm
+              optional: true
         - name: ARGOCD_APPLICATION_CONTROLLER_SYNC_TIMEOUT
           valueFrom:
             configMapKeyRef:
diff --git a/manifests/core-install.yaml b/manifests/core-install.yaml
index 9dea88c51..b38872875 100644
--- a/manifests/core-install.yaml
+++ b/manifests/core-install.yaml
@@ -25290,6 +25290,12 @@ spec:
               key: controller.self.heal.backoff.cap.seconds
               name: argocd-cmd-params-cm
               optional: true
+        - name: ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_COOLDOWN_SECONDS
+          valueFrom:
+            configMapKeyRef:
+              key: controller.self.heal.backoff.cooldown.seconds
+              name: argocd-cmd-params-cm
+              optional: true
         - name: ARGOCD_APPLICATION_CONTROLLER_SYNC_TIMEOUT
           valueFrom:
             configMapKeyRef:
diff --git a/manifests/ha/install-with-hydrator.yaml b/manifests/ha/install-with-hydrator.yaml
index 468841fa5..d8020d1d6 100644
--- a/manifests/ha/install-with-hydrator.yaml
+++ b/manifests/ha/install-with-hydrator.yaml
@@ -26301,7 +26301,7 @@ spec:
               key: dexserver.disable.tls
               name: argocd-cmd-params-cm
               optional: true
-        image: ghcr.io/dexidp/dex:v2.41.1
+        image: ghcr.io/dexidp/dex:v2.43.0
         imagePullPolicy: Always
         name: dex
         ports:
@@ -27501,6 +27501,12 @@ spec:
               key: controller.self.heal.backoff.cap.seconds
               name: argocd-cmd-params-cm
               optional: true
+        - name: ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_COOLDOWN_SECONDS
+          valueFrom:
+            configMapKeyRef:
+              key: controller.self.heal.backoff.cooldown.seconds
+              name: argocd-cmd-params-cm
+              optional: true
         - name: ARGOCD_APPLICATION_CONTROLLER_SYNC_TIMEOUT
           valueFrom:
             configMapKeyRef:
diff --git a/manifests/ha/install.yaml b/manifests/ha/install.yaml
index f7880bb48..801d81972 100644
--- a/manifests/ha/install.yaml
+++ b/manifests/ha/install.yaml
@@ -26137,7 +26137,7 @@ spec:
               key: dexserver.disable.tls
               name: argocd-cmd-params-cm
               optional: true
-        image: ghcr.io/dexidp/dex:v2.41.1
+        image: ghcr.io/dexidp/dex:v2.43.0
         imagePullPolicy: Always
         name: dex
         ports:
@@ -27337,6 +27337,12 @@ spec:
               key: controller.self.heal.backoff.cap.seconds
               name: argocd-cmd-params-cm
               optional: true
+        - name: ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_COOLDOWN_SECONDS
+          valueFrom:
+            configMapKeyRef:
+              key: controller.self.heal.backoff.cooldown.seconds
+              name: argocd-cmd-params-cm
+              optional: true
         - name: ARGOCD_APPLICATION_CONTROLLER_SYNC_TIMEOUT
           valueFrom:
             configMapKeyRef:
diff --git a/manifests/ha/namespace-install-with-hydrator.yaml b/manifests/ha/namespace-install-with-hydrator.yaml
index 5ce09327c..e3f092880 100644
--- a/manifests/ha/namespace-install-with-hydrator.yaml
+++ b/manifests/ha/namespace-install-with-hydrator.yaml
@@ -2110,7 +2110,7 @@ spec:
               key: dexserver.disable.tls
               name: argocd-cmd-params-cm
               optional: true
-        image: ghcr.io/dexidp/dex:v2.41.1
+        image: ghcr.io/dexidp/dex:v2.43.0
         imagePullPolicy: Always
         name: dex
         ports:
@@ -3310,6 +3310,12 @@ spec:
               key: controller.self.heal.backoff.cap.seconds
               name: argocd-cmd-params-cm
               optional: true
+        - name: ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_COOLDOWN_SECONDS
+          valueFrom:
+            configMapKeyRef:
+              key: controller.self.heal.backoff.cooldown.seconds
+              name: argocd-cmd-params-cm
+              optional: true
         - name: ARGOCD_APPLICATION_CONTROLLER_SYNC_TIMEOUT
           valueFrom:
             configMapKeyRef:
diff --git a/manifests/ha/namespace-install.yaml b/manifests/ha/namespace-install.yaml
index 6cdf81e20..c22cffa53 100644
--- a/manifests/ha/namespace-install.yaml
+++ b/manifests/ha/namespace-install.yaml
@@ -1946,7 +1946,7 @@ spec:
               key: dexserver.disable.tls
               name: argocd-cmd-params-cm
               optional: true
-        image: ghcr.io/dexidp/dex:v2.41.1
+        image: ghcr.io/dexidp/dex:v2.43.0
         imagePullPolicy: Always
         name: dex
         ports:
@@ -3146,6 +3146,12 @@ spec:
               key: controller.self.heal.backoff.cap.seconds
               name: argocd-cmd-params-cm
               optional: true
+        - name: ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_COOLDOWN_SECONDS
+          valueFrom:
+            configMapKeyRef:
+              key: controller.self.heal.backoff.cooldown.seconds
+              name: argocd-cmd-params-cm
+              optional: true
         - name: ARGOCD_APPLICATION_CONTROLLER_SYNC_TIMEOUT
           valueFrom:
             configMapKeyRef:
diff --git a/manifests/install-with-hydrator.yaml b/manifests/install-with-hydrator.yaml
index 90ffd7004..3970649c6 100644
--- a/manifests/install-with-hydrator.yaml
+++ b/manifests/install-with-hydrator.yaml
@@ -25395,7 +25395,7 @@ spec:
               key: dexserver.disable.tls
               name: argocd-cmd-params-cm
               optional: true
-        image: ghcr.io/dexidp/dex:v2.41.1
+        image: ghcr.io/dexidp/dex:v2.43.0
         imagePullPolicy: Always
         name: dex
         ports:
@@ -26545,6 +26545,12 @@ spec:
               key: controller.self.heal.backoff.cap.seconds
               name: argocd-cmd-params-cm
               optional: true
+        - name: ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_COOLDOWN_SECONDS
+          valueFrom:
+            configMapKeyRef:
+              key: controller.self.heal.backoff.cooldown.seconds
+              name: argocd-cmd-params-cm
+              optional: true
         - name: ARGOCD_APPLICATION_CONTROLLER_SYNC_TIMEOUT
           valueFrom:
             configMapKeyRef:
diff --git a/manifests/install.yaml b/manifests/install.yaml
index a2e82b3a5..8c89a3ab3 100644
--- a/manifests/install.yaml
+++ b/manifests/install.yaml
@@ -25229,7 +25229,7 @@ spec:
               key: dexserver.disable.tls
               name: argocd-cmd-params-cm
               optional: true
-        image: ghcr.io/dexidp/dex:v2.41.1
+        image: ghcr.io/dexidp/dex:v2.43.0
         imagePullPolicy: Always
         name: dex
         ports:
@@ -26379,6 +26379,12 @@ spec:
               key: controller.self.heal.backoff.cap.seconds
               name: argocd-cmd-params-cm
               optional: true
+        - name: ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_COOLDOWN_SECONDS
+          valueFrom:
+            configMapKeyRef:
+              key: controller.self.heal.backoff.cooldown.seconds
+              name: argocd-cmd-params-cm
+              optional: true
         - name: ARGOCD_APPLICATION_CONTROLLER_SYNC_TIMEOUT
           valueFrom:
             configMapKeyRef:
diff --git a/manifests/namespace-install-with-hydrator.yaml b/manifests/namespace-install-with-hydrator.yaml
index 0dd6a1147..a86a9f8cd 100644
--- a/manifests/namespace-install-with-hydrator.yaml
+++ b/manifests/namespace-install-with-hydrator.yaml
@@ -1204,7 +1204,7 @@ spec:
               key: dexserver.disable.tls
               name: argocd-cmd-params-cm
               optional: true
-        image: ghcr.io/dexidp/dex:v2.41.1
+        image: ghcr.io/dexidp/dex:v2.43.0
         imagePullPolicy: Always
         name: dex
         ports:
@@ -2354,6 +2354,12 @@ spec:
               key: controller.self.heal.backoff.cap.seconds
               name: argocd-cmd-params-cm
               optional: true
+        - name: ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_COOLDOWN_SECONDS
+          valueFrom:
+            configMapKeyRef:
+              key: controller.self.heal.backoff.cooldown.seconds
+              name: argocd-cmd-params-cm
+              optional: true
         - name: ARGOCD_APPLICATION_CONTROLLER_SYNC_TIMEOUT
           valueFrom:
             configMapKeyRef:
diff --git a/manifests/namespace-install.yaml b/manifests/namespace-install.yaml
index 85aee13ab..752b64b10 100644
--- a/manifests/namespace-install.yaml
+++ b/manifests/namespace-install.yaml
@@ -1038,7 +1038,7 @@ spec:
               key: dexserver.disable.tls
               name: argocd-cmd-params-cm
               optional: true
-        image: ghcr.io/dexidp/dex:v2.41.1
+        image: ghcr.io/dexidp/dex:v2.43.0
         imagePullPolicy: Always
         name: dex
         ports:
@@ -2188,6 +2188,12 @@ spec:
               key: controller.self.heal.backoff.cap.seconds
               name: argocd-cmd-params-cm
               optional: true
+        - name: ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_COOLDOWN_SECONDS
+          valueFrom:
+            configMapKeyRef:
+              key: controller.self.heal.backoff.cooldown.seconds
+              name: argocd-cmd-params-cm
+              optional: true
         - name: ARGOCD_APPLICATION_CONTROLLER_SYNC_TIMEOUT
           valueFrom:
             configMapKeyRef:
diff --git a/resource_customizations/_.crossplane.io/_/health.lua b/resource_customizations/_.crossplane.io/_/health.lua
new file mode 100644
index 000000000..c4cd585cf
--- /dev/null
+++ b/resource_customizations/_.crossplane.io/_/health.lua
@@ -0,0 +1,66 @@
+-- Health check copied from here: https://github.com/crossplane/docs/blob/bd701357e9d5eecf529a0b42f23a78850a6d1d87/content/master/guides/crossplane-with-argo-cd.md
+
+health_status = {
+  status = "Progressing",
+  message = "Provisioning ..."
+}
+
+local function contains (table, val)
+  for i, v in ipairs(table) do
+    if v == val then
+      return true
+    end
+  end
+  return false
+end
+
+local has_no_status = {
+  "Composition",
+  "CompositionRevision",
+  "DeploymentRuntimeConfig",
+  "ControllerConfig",
+  "ProviderConfig",
+  "ProviderConfigUsage"
+}
+if obj.status == nil or next(obj.status) == nil and contains(has_no_status, obj.kind) then
+    health_status.status = "Healthy"
+    health_status.message = "Resource is up-to-date."
+  return health_status
+end
+
+if obj.status == nil or next(obj.status) == nil or obj.status.conditions == nil then
+  if obj.kind == "ProviderConfig" and obj.status.users ~= nil then
+    health_status.status = "Healthy"
+    health_status.message = "Resource is in use."
+    return health_status
+  end
+  return health_status
+end
+
+for i, condition in ipairs(obj.status.conditions) do
+  if condition.type == "LastAsyncOperation" then
+    if condition.status == "False" then
+      health_status.status = "Degraded"
+      health_status.message = condition.message
+      return health_status
+    end
+  end
+
+  if condition.type == "Synced" then
+    if condition.status == "False" then
+      health_status.status = "Degraded"
+      health_status.message = condition.message
+      return health_status
+    end
+  end
+
+  if contains({"Ready", "Healthy", "Offered", "Established"}, condition.type) then
+    if condition.status == "True" then
+      health_status.status = "Healthy"
+      health_status.message = "Resource is up-to-date."
+      return health_status
+    end
+  end
+end
+
+return health_status
diff --git a/resource_customizations/_.upbound.io/_/health.lua b/resource_customizations/_.upbound.io/_/health.lua
new file mode 100644
index 000000000..57869c81f
--- /dev/null
+++ b/resource_customizations/_.upbound.io/_/health.lua
@@ -0,0 +1,63 @@
+-- Health check copied from here: https://github.com/crossplane/docs/blob/bd701357e9d5eecf529a0b42f23a78850a6d1d87/content/master/guides/crossplane-with-argo-cd.md
+
+health_status = {
+  status = "Progressing",
+  message = "Provisioning ..."
+}
+
+local function contains (table, val)
+  for i, v in ipairs(table) do
+    if v == val then
+      return true
+    end
+  end
+  return false
+end
+
+local has_no_status = {
+  "ProviderConfig",
+  "ProviderConfigUsage"
+}
+
+if obj.status == nil or next(obj.status) == nil and contains(has_no_status, obj.kind) then
+  health_status.status = "Healthy"
+  health_status.message = "Resource is up-to-date."
+  return health_status
+end
+
+if obj.status == nil or next(obj.status) == nil or obj.status.conditions == nil then
+  if obj.kind == "ProviderConfig" and obj.status.users ~= nil then
+    health_status.status = "Healthy"
+    health_status.message = "Resource is in use."
+    return health_status
+  end
+  return health_status
+end
+
+for i, condition in ipairs(obj.status.conditions) do
+  if condition.type == "LastAsyncOperation" then
+    if condition.status == "False" then
+      health_status.status = "Degraded"
+      health_status.message = condition.message
+      return health_status
+    end
+  end
+
+  if condition.type == "Synced" then
+    if condition.status == "False" then
+      health_status.status = "Degraded"
+      health_status.message = condition.message
+      return health_status
+    end
+  end
+
+  if condition.type == "Ready" then
+    if condition.status == "True" then
+      health_status.status = "Healthy"
+      health_status.message = "Resource is up-to-date."
+      return health_status
+    end
+  end
+end
+
+return health_status
diff --git a/resource_customizations/embed.go b/resource_customizations/embed.go
index 8a4d5316c..8b2157b82 100644
--- a/resource_customizations/embed.go
+++ b/resource_customizations/embed.go
@@ -6,5 +6,5 @@ import (
 
 // Embedded contains embedded resource customization
 //
-//go:embed *
+//go:embed all:*
 var Embedded embed.FS
diff --git a/server/application/application.go b/server/application/application.go
index b9f1d38ce..0a4e52eb5 100644
--- a/server/application/application.go
+++ b/server/application/application.go
@@ -2679,7 +2679,7 @@ func (s *Server) GetApplicationSyncWindows(ctx context.Context, q *application.A
 func (s *Server) inferResourcesStatusHealth(app *v1alpha1.Application) {
 	if app.Status.ResourceHealthSource == v1alpha1.ResourceHealthLocationAppTree {
 		tree := &v1alpha1.ApplicationTree{}
-		if err := s.cache.GetAppResourcesTree(app.Name, tree); err == nil {
+		if err := s.cache.GetAppResourcesTree(app.InstanceName(s.ns), tree); err == nil {
 			healthByKey := map[kube.ResourceKey]*v1alpha1.HealthStatus{}
 			for _, node := range tree.Nodes {
 				if node.Health != nil {
diff --git a/server/applicationset/applicationset.go b/server/applicationset/applicationset.go
index dbf4410d5..cefc34788 100644
--- a/server/applicationset/applicationset.go
+++ b/server/applicationset/applicationset.go
@@ -298,7 +298,7 @@ func (s *Server) updateAppSet(ctx context.Context, appset *v1alpha1.ApplicationS
 			appset.Labels = newAppset.Labels
 			appset.Annotations = newAppset.Annotations
 		}
-
+		appset.Finalizers = newAppset.Finalizers
 		res, err := s.appclientset.ArgoprojV1alpha1().ApplicationSets(s.ns).Update(ctx, appset, metav1.UpdateOptions{})
 		if err == nil {
 			s.logAppSetEvent(ctx, appset, argo.EventReasonResourceUpdated, "updated ApplicationSets spec")
diff --git a/ui/package.json b/ui/package.json
index b0ec7e709..c5dc83324 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -116,7 +116,7 @@
     "sass": "^1.49.9",
     "sass-loader": "^14.2.1",
     "source-map-loader": "^0.2.3",
-    "style-loader": "^0.20.1",
+    "style-loader": "^1",
     "ts-jest": "^29.1.2",
     "ts-node": "10.9.2",
     "typescript": "^4.9.5",
diff --git a/ui/yarn.lock b/ui/yarn.lock
index 57dfb4c70..c8e317a70 100644
--- a/ui/yarn.lock
+++ b/ui/yarn.lock
@@ -2700,7 +2700,7 @@ ajv-formats@^2.1.1:
   dependencies:
     ajv "^8.0.0"
 
-ajv-keywords@^3.1.0, ajv-keywords@^3.5.2:
+ajv-keywords@^3.5.2:
   version "3.5.2"
   resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
   integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
@@ -2712,7 +2712,7 @@ ajv-keywords@^5.0.0:
   dependencies:
     fast-deep-equal "^3.1.3"
 
-ajv@^6.1.0, ajv@^6.12.4, ajv@^6.12.5:
+ajv@^6.12.4, ajv@^6.12.5:
   version "6.12.6"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
   integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -8456,15 +8456,7 @@ scheduler@^0.19.1:
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
 
-schema-utils@^0.4.5:
-  version "0.4.7"
-  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187"
-  integrity sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==
-  dependencies:
-    ajv "^6.1.0"
-    ajv-keywords "^3.1.0"
-
-schema-utils@^2.7.1:
+schema-utils@^2.7.0, schema-utils@^2.7.1:
   version "2.7.1"
   resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
   integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
@@ -9096,13 +9088,13 @@ stubs@^3.0.0:
   resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b"
   integrity sha1-6NK6H6nJBXAwPAMLaQD31fiavls=
 
-style-loader@^0.20.1:
-  version "0.20.3"
-  resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.20.3.tgz#ebef06b89dec491bcb1fdb3452e913a6fd1c10c4"
-  integrity sha512-2I7AVP73MvK33U7B9TKlYZAqdROyMXDYSMvHLX43qy3GCOaJNiV6i0v/sv9idWIaQ42Yn2dNv79Q5mKXbKhAZg==
+style-loader@^1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.3.0.tgz#828b4a3b3b7e7aa5847ce7bae9e874512114249e"
+  integrity sha512-V7TCORko8rs9rIqkSrlMfkqA63DfoGBBJmK1kKGCcSi+BWb4cqz0SRsnp4l6rU5iwOEd0/2ePv68SV22VXon4Q==
   dependencies:
-    loader-utils "^1.1.0"
-    schema-utils "^0.4.5"
+    loader-utils "^2.0.0"
+    schema-utils "^2.7.0"
 
 superagent@^8.1.2:
   version "8.1.2"
diff --git a/util/lua/lua.go b/util/lua/lua.go
index df47709b7..abdc88f84 100644
--- a/util/lua/lua.go
+++ b/util/lua/lua.go
@@ -6,12 +6,17 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"io/fs"
 	"os"
 	"path/filepath"
 	"reflect"
+	"slices"
+	"strings"
+	"sync"
 	"time"
 
 	"github.com/argoproj/gitops-engine/pkg/health"
+	glob "github.com/bmatcuk/doublestar/v4"
 	lua "github.com/yuin/gopher-lua"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime/schema"
@@ -20,7 +25,7 @@ import (
 	applicationpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/application"
 	appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
 	"github.com/argoproj/argo-cd/v3/resource_customizations"
-	"github.com/argoproj/argo-cd/v3/util/glob"
+	argoglob "github.com/argoproj/argo-cd/v3/util/glob"
 )
 
 const (
@@ -31,15 +36,8 @@ const (
 	actionDiscoveryScriptFile = "discovery.lua"
 )
 
-// ScriptDoesNotExistError is an error type for when a built-in script does not exist.
-type ScriptDoesNotExistError struct {
-	// ScriptName is the name of the script that does not exist.
-	ScriptName string
-}
-
-func (e ScriptDoesNotExistError) Error() string {
-	return fmt.Sprintf("built-in script %q does not exist", e.ScriptName)
-}
+// errScriptDoesNotExist is an error type for when a built-in script does not exist.
+var errScriptDoesNotExist = errors.New("built-in script does not exist")
 
 type ResourceHealthOverrides map[string]appv1.ResourceOverride
 
@@ -187,8 +185,16 @@ func (vm VM) GetHealthScript(obj *unstructured.Unstructured) (script string, use
 	// (as built-in scripts are files in folders, named after the GVK, currently there is no wildcard support for them)
 	builtInScript, err := vm.getPredefinedLuaScripts(key, healthScriptFile)
 	if err != nil {
-		var doesNotExist *ScriptDoesNotExistError
-		if errors.As(err, &doesNotExist) {
+		if errors.Is(err, errScriptDoesNotExist) {
+			// Try to find a wildcard built-in health script
+			builtInScript, err = getWildcardBuiltInHealthOverrideLua(key)
+			if err != nil {
+				return "", false, fmt.Errorf("error while fetching built-in health script: %w", err)
+			}
+			if builtInScript != "" {
+				return builtInScript, true, nil
+			}
+
 			// It's okay if no built-in health script exists. Just return an empty string and let the caller handle it.
 			return "", false, nil
 		}
@@ -422,8 +428,7 @@ func (vm VM) GetResourceActionDiscovery(obj *unstructured.Unstructured) ([]strin
 	discoveryKey := key + "/actions/"
 	discoveryScript, err := vm.getPredefinedLuaScripts(discoveryKey, actionDiscoveryScriptFile)
 	if err != nil {
-		var doesNotExistErr *ScriptDoesNotExistError
-		if errors.As(err, &doesNotExistErr) {
+		if errors.Is(err, errScriptDoesNotExist) {
 			// No worries, just return what we have.
 			return discoveryScripts, nil
 		}
@@ -477,7 +482,7 @@ func getWildcardHealthOverrideLua(overrides map[string]appv1.ResourceOverride, g
 	gvkKeyToMatch := GetConfigMapKey(gvk)
 
 	for key, override := range overrides {
-		if glob.Match(key, gvkKeyToMatch) && override.HealthLua != "" {
+		if argoglob.Match(key, gvkKeyToMatch) && override.HealthLua != "" {
 			return override.HealthLua, override.UseOpenLibs
 		}
 	}
@@ -488,13 +493,95 @@ func (vm VM) getPredefinedLuaScripts(objKey string, scriptFile string) (string,
 	data, err := resource_customizations.Embedded.ReadFile(filepath.Join(objKey, scriptFile))
 	if err != nil {
 		if os.IsNotExist(err) {
-			return "", &ScriptDoesNotExistError{ScriptName: objKey}
+			return "", errScriptDoesNotExist
 		}
 		return "", err
 	}
 	return string(data), nil
 }
 
+// globHealthScriptPathsOnce is a sync.Once instance to ensure that the globHealthScriptPaths are only initialized once.
+// The globs come from an embedded filesystem, so it won't change at runtime.
+var globHealthScriptPathsOnce sync.Once
+
+// globHealthScriptPaths is a cache for the glob patterns of directories containing health.lua files. Don't use this
+// directly, use getGlobHealthScriptPaths() instead.
+var globHealthScriptPaths []string
+
+// getGlobHealthScriptPaths returns the paths of the directories containing health.lua files where the path contains a
+// glob pattern. It uses a sync.Once to ensure that the paths are only initialized once.
+func getGlobHealthScriptPaths() ([]string, error) {
+	var err error
+	globHealthScriptPathsOnce.Do(func() {
+		// Walk through the embedded filesystem and get the directory names of all directories containing a health.lua.
+		var patterns []string
+		err = fs.WalkDir(resource_customizations.Embedded, ".", func(path string, d fs.DirEntry, err error) error {
+			if err != nil {
+				return fmt.Errorf("error walking path %q: %w", path, err)
+			}
+
+			// Skip non-directories at the top level
+			if d.IsDir() && filepath.Dir(path) == "." {
+				return nil
+			}
+
+			// Check if the directory contains a health.lua file
+			if filepath.Base(path) != healthScriptFile {
+				return nil
+			}
+
+			groupKindPath := filepath.Dir(path)
+			// Check if the path contains a wildcard. If it doesn't, skip it.
+			if !strings.Contains(groupKindPath, "_") {
+				return nil
+			}
+
+			pattern := strings.ReplaceAll(groupKindPath, "_", "*")
+			// Check that the pattern is valid.
+			if !glob.ValidatePattern(pattern) {
+				return fmt.Errorf("invalid glob pattern %q: %w", pattern, err)
+			}
+
+			patterns = append(patterns, groupKindPath)
+			return nil
+		})
+		if err != nil {
+			return
+		}
+
+		// Sort the patterns to ensure deterministic choice of wildcard directory for a given GK.
+		slices.Sort(patterns)
+
+		globHealthScriptPaths = patterns
+	})
+	if err != nil {
+		return nil, fmt.Errorf("error getting health script glob directories: %w", err)
+	}
+	return globHealthScriptPaths, nil
+}
+
+func getWildcardBuiltInHealthOverrideLua(objKey string) (string, error) {
+	// Check if the GVK matches any of the wildcard directories
+	globs, err := getGlobHealthScriptPaths()
+	if err != nil {
+		return "", fmt.Errorf("error getting health script globs: %w", err)
+	}
+	for _, g := range globs {
+		pattern := strings.ReplaceAll(g, "_", "*")
+		if !glob.PathMatchUnvalidated(pattern, objKey) {
+			continue
+		}
+
+		var script []byte
+		script, err = resource_customizations.Embedded.ReadFile(filepath.Join(g, healthScriptFile))
+		if err != nil {
+			return "", fmt.Errorf("error reading %q file in embedded filesystem: %w", filepath.Join(objKey, healthScriptFile), err)
+		}
+		return string(script), nil
+	}
+	return "", nil
+}
+
 func isValidHealthStatusCode(statusCode health.HealthStatusCode) bool {
 	switch statusCode {
 	case health.HealthStatusUnknown, health.HealthStatusProgressing, health.HealthStatusSuspended, health.HealthStatusHealthy, health.HealthStatusDegraded, health.HealthStatusMissing:


Test patch:
diff --git a/controller/appcontroller_test.go b/controller/appcontroller_test.go
index 12be8228f..8185b5462 100644
--- a/controller/appcontroller_test.go
+++ b/controller/appcontroller_test.go
@@ -172,6 +172,7 @@ func newFakeControllerWithResync(data *fakeData, appResyncPeriod time.Duration,
 		time.Second,
 		time.Minute,
 		nil,
+		time.Minute,
 		0,
 		time.Second*10,
 		common.DefaultPortArgoCDMetrics,
@@ -2642,10 +2643,18 @@ func TestSelfHealExponentialBackoff(t *testing.T) {
 		alreadyAttempted: false,
 		expectedAttempts: 0,
 		syncStatus:       v1alpha1.SyncStatusCodeOutOfSync,
-	}, {
+	}, { // backoff will not reset as finished tme isn't >= cooldown
 		attempts:         6,
-		finishedAt:       nil,
-		expectedDuration: 0,
+		finishedAt:       ptr.To(metav1.Now()),
+		expectedDuration: 120 * time.Second,
+		shouldSelfHeal:   false,
+		alreadyAttempted: true,
+		expectedAttempts: 6,
+		syncStatus:       v1alpha1.SyncStatusCodeSynced,
+	}, { // backoff will reset as finished time is >= cooldown
+		attempts:         40,
+		finishedAt:       &metav1.Time{Time: time.Now().Add(-(1 * time.Minute))},
+		expectedDuration: -60 * time.Second,
 		shouldSelfHeal:   true,
 		alreadyAttempted: true,
 		expectedAttempts: 0,
diff --git a/resource_customizations/_.crossplane.io/_/health_test.yaml b/resource_customizations/_.crossplane.io/_/health_test.yaml
new file mode 100644
index 000000000..7ef0da991
--- /dev/null
+++ b/resource_customizations/_.crossplane.io/_/health_test.yaml
@@ -0,0 +1,5 @@
+tests:
+  - healthStatus:
+      status: Healthy
+      message: "Resource is up-to-date."
+    inputPath: testdata/composition_healthy.yaml
diff --git a/resource_customizations/_.crossplane.io/_/testdata/composition_healthy.yaml b/resource_customizations/_.crossplane.io/_/testdata/composition_healthy.yaml
new file mode 100644
index 000000000..56ae5ac89
--- /dev/null
+++ b/resource_customizations/_.crossplane.io/_/testdata/composition_healthy.yaml
@@ -0,0 +1,25 @@
+# Taken from here May 9, 2025: https://docs.crossplane.io/latest/concepts/compositions/
+apiVersion: apiextensions.crossplane.io/v1
+kind: Composition
+metadata:
+  name: example
+spec:
+  compositeTypeRef:
+    apiVersion: custom-api.example.org/v1alpha1
+    kind: AcmeBucket
+  mode: Pipeline
+  pipeline:
+    - step: patch-and-transform
+      functionRef:
+        name: function-patch-and-transform
+      input:
+        apiVersion: pt.fn.crossplane.io/v1beta1
+        kind: Resources
+        resources:
+          - name: storage-bucket
+            base:
+              apiVersion: s3.aws.upbound.io/v1beta1
+              kind: Bucket
+              spec:
+                forProvider:
+                  region: "us-east-2"
diff --git a/resource_customizations/_.upbound.io/_/health_test.yaml b/resource_customizations/_.upbound.io/_/health_test.yaml
new file mode 100644
index 000000000..a11541d26
--- /dev/null
+++ b/resource_customizations/_.upbound.io/_/health_test.yaml
@@ -0,0 +1,5 @@
+tests:
+  - healthStatus:
+      status: Healthy
+      message: "Resource is up-to-date."
+    inputPath: testdata/providerconfig_healthy.yaml
diff --git a/resource_customizations/_.upbound.io/_/testdata/providerconfig_healthy.yaml b/resource_customizations/_.upbound.io/_/testdata/providerconfig_healthy.yaml
new file mode 100644
index 000000000..5429ac065
--- /dev/null
+++ b/resource_customizations/_.upbound.io/_/testdata/providerconfig_healthy.yaml
@@ -0,0 +1,10 @@
+apiVersion: aws.upbound.io/v1beta1
+kind: ProviderConfig
+metadata:
+  name: irsa-with-role-chaining
+spec:
+  credentials:
+    source: IRSA
+  assumeRoleChain:
+    - roleARN: <roleARN-1>
+    - roleARN: <roleARN-2>
diff --git a/server/applicationset/applicationset_test.go b/server/applicationset/applicationset_test.go
index 46838f3ed..520694411 100644
--- a/server/applicationset/applicationset_test.go
+++ b/server/applicationset/applicationset_test.go
@@ -515,6 +515,7 @@ func TestUpdateAppSet(t *testing.T) {
 			"label-key1": "label-value1",
 			"label-key2": "label-value2",
 		}
+		appset.Finalizers = []string{"finalizer"}
 	})
 
 	newAppSet := newTestAppSet(func(appset *appsv1.ApplicationSet) {
@@ -524,6 +525,7 @@ func TestUpdateAppSet(t *testing.T) {
 		appset.Labels = map[string]string{
 			"label-key1": "label-value1-updated",
 		}
+		appset.Finalizers = []string{"finalizer-updated"}
 	})
 
 	t.Run("Update merge", func(t *testing.T) {
@@ -540,6 +542,7 @@ func TestUpdateAppSet(t *testing.T) {
 			"label-key1": "label-value1-updated",
 			"label-key2": "label-value2",
 		}, updated.Labels)
+		assert.Equal(t, []string{"finalizer-updated"}, updated.Finalizers)
 	})
 
 	t.Run("Update no merge", func(t *testing.T) {
diff --git a/test/remote/Dockerfile b/test/remote/Dockerfile
index a220616b0..6da0a30c9 100644
--- a/test/remote/Dockerfile
+++ b/test/remote/Dockerfile
@@ -1,6 +1,6 @@
 ARG BASE_IMAGE=docker.io/library/ubuntu:24.04@sha256:80dd3c3b9c6cecb9f1667e9290b3bc61b78c2678c02cbdae5f0fea92cc6734ab
 
-FROM docker.io/library/golang:1.24.3@sha256:86b4cff66e04d41821a17cea30c1031ed53e2635e2be99ae0b4a7d69336b5063 AS go
+FROM docker.io/library/golang:1.24.3@sha256:1bcf8844d2464a6485c87646f9da684610758eb1a2df63c8a6e7ca47c64f8655 AS go
 
 RUN go install github.com/mattn/goreman@latest && \
     go install github.com/kisielk/godepgraph@latest
diff --git a/util/lua/lua_test.go b/util/lua/lua_test.go
index aa7842410..9274d279d 100644
--- a/util/lua/lua_test.go
+++ b/util/lua/lua_test.go
@@ -284,8 +284,7 @@ func TestGetResourceActionNoPredefined(t *testing.T) {
 	testObj := StrToUnstructured(objWithNoScriptJSON)
 	vm := VM{}
 	action, err := vm.GetResourceAction(testObj, "test")
-	var expectedErr *ScriptDoesNotExistError
-	require.ErrorAs(t, err, &expectedErr)
+	require.ErrorIs(t, err, errScriptDoesNotExist)
 	assert.Empty(t, action.ActionLua)
 }
 
@@ -868,7 +867,7 @@ return hs`
 	})
 
 	t.Run("Get resource health for */* override with empty health.lua", func(t *testing.T) {
-		testObj := StrToUnstructured(ec2AWSCrossplaneObjJSON)
+		testObj := StrToUnstructured(objWithNoScriptJSON)
 		overrides := getBaseWildcardHealthOverrides
 		status, err := overrides.GetResourceHealth(testObj)
 		require.NoError(t, err)
@@ -954,3 +953,12 @@ func createMockResource(kind string, name string, replicas int) *unstructured.Un
             image: nginx
     `, kind, name, replicas))
 }
+
+func Test_getHealthScriptPaths(t *testing.T) {
+	paths, err := getGlobHealthScriptPaths()
+	require.NoError(t, err)
+
+	// This test will fail any time a glob pattern is added to the health script paths. We don't expect that to happen
+	// often.
+	assert.Equal(t, []string{"_.crossplane.io/_", "_.upbound.io/_"}, paths)
+}