From d4ebb33a614c39ddfe7c9953d86a7f8420016e55 Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Fri, 15 May 2026 15:12:42 +0200 Subject: [PATCH 1/2] fix: enable/disable 'Save Config' whenever auth means are changed (#23780) Signed-off-by: Andre Dietisheim Signed-off-by: Victor Rubezhny co-authored-by: Cursor --- .../gateway/kubeconfig/KubeConfigEntries.kt | 21 ++- .../gateway/kubeconfig/KubeConfigUtils.kt | 24 ++-- .../devtools/gateway/openshift/Cluster.kt | 5 +- .../view/steps/DevSpacesServerStepView.kt | 121 +++++++++++------- .../auth/AbstractAuthenticationStrategy.kt | 2 + .../view/steps/auth/AuthenticationStrategy.kt | 5 + ...ClientCertificateAuthenticationStrategy.kt | 11 ++ ...nShiftCredentialsAuthenticationStrategy.kt | 27 ++++ .../OpenShiftOAuthAuthenticationStrategy.kt | 5 + .../auth/RedHatSSOAuthenticationStrategy.kt | 13 +- .../steps/auth/TokenAuthenticationStrategy.kt | 18 +++ 11 files changed, 175 insertions(+), 77 deletions(-) diff --git a/src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigEntries.kt b/src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigEntries.kt index 641c3fe8..eb0e643d 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigEntries.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigEntries.kt @@ -210,25 +210,22 @@ data class KubeConfigNamedUser( ) } - fun getUserTokenForCluster(clusterName: String, kubeConfig: KubeConfig): String? { - val contextEntry = KubeConfigNamedContext.getByName(clusterName, kubeConfig) ?: return null + fun getUserForCluster(clusterRefName: String, kubeConfig: KubeConfig): KubeConfigUser? { + val contextEntry = KubeConfigNamedContext.getByName(clusterRefName, kubeConfig) ?: return null val userObject = (kubeConfig.users as? List<*>)?.firstOrNull { userObject -> val userMap = userObject as? Map<*, *> ?: return@firstOrNull false val userName = userMap["name"] as? String ?: return@firstOrNull false userName == contextEntry.context.user - } as? Map<*,*> ?: return null - return fromMap(userObject)?.user?.token + } as? Map<*, *> ?: return null + return fromMap(userObject)?.user } + fun getUserTokenForCluster(clusterName: String, kubeConfig: KubeConfig): String? = + getUserForCluster(clusterName, kubeConfig)?.token + fun getUserClientCertForCluster(clusterName: String, kubeConfig: KubeConfig): Pair? { - val contextEntry = KubeConfigNamedContext.getByName(clusterName, kubeConfig) ?: return null - val userObject = (kubeConfig.users as? List<*>)?.firstOrNull { userObject -> - val userMap = userObject as? Map<*, *> ?: return@firstOrNull false - val userName = userMap["name"] as? String ?: return@firstOrNull false - userName == contextEntry.context.user - } as? Map<*,*> ?: return null - val user = fromMap(userObject)?.user - return Pair(user?.clientCertificate, user?.clientKey) + val user = getUserForCluster(clusterName, kubeConfig) ?: return null + return Pair(user.clientCertificate, user.clientKey) } fun isTokenAuth(kubeConfig: KubeConfig): Boolean { diff --git a/src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUtils.kt b/src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUtils.kt index 36de5cdf..87d9ab14 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUtils.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUtils.kt @@ -13,7 +13,6 @@ package com.redhat.devtools.gateway.kubeconfig import com.intellij.openapi.diagnostic.thisLogger import com.intellij.util.EnvironmentUtil -import com.redhat.devtools.gateway.auth.tls.CertificateSource import com.redhat.devtools.gateway.openshift.Cluster import io.kubernetes.client.util.KubeConfig import java.io.File @@ -44,13 +43,10 @@ object KubeConfigUtils { .flatMap { kubeConfig -> kubeConfig.clusters?.mapNotNull { cluster -> val namedCluster = KubeConfigNamedCluster.fromMap(cluster as Map<*, *>) ?: return@mapNotNull null - val token = KubeConfigNamedUser.getUserTokenForCluster(namedCluster.name, kubeConfig) - val clientCert = KubeConfigNamedUser.getUserClientCertForCluster(namedCluster.name, kubeConfig) - val clientCertSource = clientCert?.first - val clientKeySource = clientCert?.second - val cluster = toCluster(namedCluster, token, clientCertSource, clientKeySource) - logger.debug("Parsed cluster: ${cluster.name} at ${cluster.url}") - cluster + val kubeUser = KubeConfigNamedUser.getUserForCluster(namedCluster.name, kubeConfig) + val clusterModel = toCluster(namedCluster, kubeUser) + logger.debug("Parsed cluster: ${clusterModel.name} at ${clusterModel.url}") + clusterModel } ?: emptyList() } .distinctBy { it.id } @@ -87,17 +83,17 @@ object KubeConfigUtils { private fun toCluster( clusterEntry: KubeConfigNamedCluster, - userToken: String?, - clientCertSource: CertificateSource?, - clientKeySource: CertificateSource? + kubeUser: KubeConfigUser? ): Cluster { return Cluster( url = clusterEntry.cluster.server, name = clusterEntry.name, certificateAuthority = clusterEntry.cluster.certificateAuthority, - token = userToken, - clientCert = clientCertSource, - clientKey = clientKeySource + token = kubeUser?.token, + clientCert = kubeUser?.clientCertificate, + clientKey = kubeUser?.clientKey, + basicUsername = kubeUser?.username, + basicPassword = kubeUser?.password, ) } diff --git a/src/main/kotlin/com/redhat/devtools/gateway/openshift/Cluster.kt b/src/main/kotlin/com/redhat/devtools/gateway/openshift/Cluster.kt index 1ee05eec..c36366b9 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/openshift/Cluster.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/openshift/Cluster.kt @@ -21,7 +21,10 @@ data class Cluster( val certificateAuthority: CertificateSource? = null, val token: String? = null, val clientCert: CertificateSource? = null, - val clientKey: CertificateSource? = null + val clientKey: CertificateSource? = null, + /** From kubeconfig user entry when present (basic auth). */ + val basicUsername: String? = null, + val basicPassword: String? = null, ) { init { require(!(token != null && clientCert != null)) { diff --git a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/DevSpacesServerStepView.kt b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/DevSpacesServerStepView.kt index a51144cd..5c6e7c6d 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/DevSpacesServerStepView.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/DevSpacesServerStepView.kt @@ -38,7 +38,6 @@ import com.redhat.devtools.gateway.kubeconfig.KubeConfigUpdate import com.redhat.devtools.gateway.kubeconfig.KubeConfigUtils import com.redhat.devtools.gateway.openshift.Cluster import com.redhat.devtools.gateway.settings.DevSpacesSettings -import com.redhat.devtools.gateway.util.isCancellationException import com.redhat.devtools.gateway.view.steps.auth.* import com.redhat.devtools.gateway.view.ui.Dialogs import com.redhat.devtools.gateway.view.ui.FilteringComboBox @@ -62,24 +61,28 @@ class DevSpacesServerStepView( private lateinit var allClusters: List + /** + * `clusters[].name` referenced by kubeconfig `current-context`; from [KubeConfigUtils.getCurrentClusterName]. + * When the Server combo shows another cluster, the form is dirty vs that default. + */ + private var currentContextClusterName: String? = null + private val settings: ServerSettings = ServerSettings() private lateinit var kubeconfigScope: CoroutineScope private lateinit var kubeconfigMonitor: KubeConfigMonitor - private var saveToKubeconfig: Boolean = false - - private val saveKubeconfigCheckbox = JBCheckBox( + private val saveConfigCheckbox = JBCheckBox( DevSpacesBundle.message("connector.wizard_step.openshift_connection.checkbox.save_configuration") ).apply { isOpaque = false background = null - isSelected = saveToKubeconfig - addActionListener { - saveToKubeconfig = isSelected - } + isSelected = false } + private val saveConfig: Boolean + get() = saveConfigCheckbox.isEnabled && saveConfigCheckbox.isSelected + private val sessionManager = ApplicationManager.getApplication() .getService(RedHatAuthSessionManager::class.java) @@ -100,6 +103,7 @@ class DevSpacesServerStepView( val editor = this.editor.editorComponent as JTextField PasteClipboardMenu.addTo(editor) editor.addKeyListener(createEnterKeyListener()) + editor.document.addDocumentListener(onFieldChanged()) } private val authStrategies: List by lazy { @@ -150,12 +154,6 @@ class DevSpacesServerStepView( private inline fun findStrategy(): T? = authStrategies.firstOrNull { it is T } as? T - private fun getCurrentAuthTokenValue(): CharArray? = - when (currentStrategy?.getAuthMethod()) { - AuthMethod.TOKEN -> (currentStrategy as? TokenAuthenticationStrategy)?.tfToken?.password - else -> null // other tabs don't have a token yet - } - private fun tabPanel(p: JComponent): JComponent = JBUI.Panels.simplePanel(p).apply { isOpaque = true @@ -175,13 +173,10 @@ class DevSpacesServerStepView( addTab(strategy.getTabTitle(), tabPanel(panel)) } - addChangeListener { + addChangeListener { event -> currentStrategy = authStrategies.getOrNull(selectedIndex) - - saveKubeconfigCheckbox.isVisible = - currentStrategy?.getAuthMethod() != AuthMethod.REDHAT_SSO - enableNextButton?.invoke() + enableSaveConfigCheckbox() } } @@ -213,7 +208,7 @@ class DevSpacesServerStepView( } } row { - cell(saveKubeconfigCheckbox) + cell(saveConfigCheckbox) } }.apply { isOpaque = false @@ -256,7 +251,7 @@ class DevSpacesServerStepView( private fun onClusterSelected(event: ItemEvent) { if (event.stateChange == ItemEvent.SELECTED) { (event.item as? Cluster)?.let { selectedCluster -> - if (allClusters.contains(selectedCluster)) { + if ( allClusters.contains(selectedCluster)) { tfCertAuthority.text = selectedCluster.certificateAuthority?.value ?: "" findStrategy()?.tfToken?.apply { text = selectedCluster.token @@ -265,46 +260,84 @@ class DevSpacesServerStepView( tfClientCert.text = selectedCluster.clientCert?.value ?: "" tfClientKey.text = selectedCluster.clientKey?.value ?: "" } - saveKubeconfigCheckbox.isSelected = false + findStrategy()?.applyFromCluster(selectedCluster) + saveConfigCheckbox.isSelected = false } } } - updateSaveKubeconfigCheckboxEnablement() + enableSaveConfigCheckbox() } private fun onFieldChanged(): DocumentListener = object : DocumentListener { override fun insertUpdate(event: DocumentEvent) { enableNextButton?.invoke() - updateSaveKubeconfigCheckboxEnablement() + enableSaveConfigCheckbox() } override fun removeUpdate(e: DocumentEvent) { enableNextButton?.invoke() - updateSaveKubeconfigCheckboxEnablement() + enableSaveConfigCheckbox() } override fun changedUpdate(e: DocumentEvent?) { enableNextButton?.invoke() - updateSaveKubeconfigCheckboxEnablement() + enableSaveConfigCheckbox() } } - private fun updateSaveKubeconfigCheckboxEnablement() { - val cluster = tfServer.selectedItem as? Cluster - val currentToken = getCurrentAuthTokenValue() + /** + * Returns the cluster that is selected. + * When the editor text does not parse as a [Cluster], we must not fall back to the combo's stale `selectedItem`: + * that would keep the old cluster and hide URL edits from [isDirty]. + * + * @return the cluster that is selected or null + */ + private fun getSelectedCluster(): Cluster? { + val parsed = tfServer.editor.item as? Cluster + if (parsed != null) return parsed + val selected = tfServer.selectedItem as? Cluster ?: return null + val text = (tfServer.editor.editorComponent as JTextField).text.trim() + return selected.takeIf { it.toString() == text } + } + + /** + * Returns `true` if the values in the form are dirty and may be saved. + * It is considered dirty if the following values differ from kube config: + * * cluster name or URL/CA/auth + * * selected auth strategy + * * auth strategy values + * + * @see [getKubeConfigFor] + * @see [AuthenticationStrategy.isDirty] + */ + private fun isDirty(): Boolean { + val cluster = getSelectedCluster() ?: return true + val ctxName = currentContextClusterName + if (ctxName != null && cluster.name != ctxName) return true + + val config = getKubeConfigFor(cluster) ?: return true - val tokenChanged = - !cluster?.token.isNullOrBlank() - && currentToken?.isNotEmpty() == true - && !cluster.token.toCharArray().contentEquals(currentToken) + if (cluster.url != config.url) return true - // Only TokenAuthenticationStrategy requires token diff to enable save - val requiresTokenDiff = currentStrategy is TokenAuthenticationStrategy + if (tfCertAuthority.text.trim() != (config.certificateAuthority?.value ?: "").trim()) return true - saveKubeconfigCheckbox.isEnabled = - !allClusters.contains(cluster) - || !requiresTokenDiff - || tokenChanged + return currentStrategy?.isDirty(config) ?: true + } + + /** + * Kubeconfig row for [cluster]. [Cluster.id] is name+URL — id match is exact until URL is edited; + * then we match by name so URL/CA/auth still compare to the saved entry. + */ + private fun getKubeConfigFor(cluster: Cluster): Cluster? = + allClusters.find { it.id == cluster.id } + ?: allClusters.find { it.name == cluster.name } + + private fun enableSaveConfigCheckbox() { + val isDirty = isDirty() + saveConfigCheckbox.isEnabled = isDirty + if (!isDirty) { + saveConfigCheckbox.isSelected = false // uncheck if checkbox is disabled + } } @@ -326,13 +359,14 @@ class DevSpacesServerStepView( } ApplicationManager.getApplication().invokeLater( { + currentContextClusterName = kubeConfigCurrentCluster val previouslySelected = tfServer.selectedItem as? Cluster? setClusters(updatedClusters) setSelectedCluster( (previouslySelected)?.name ?: kubeConfigCurrentCluster, updatedClusters ) - updateSaveKubeconfigCheckboxEnablement() + enableSaveConfigCheckbox() }, ModalityState.stateForComponent(component) ) @@ -345,7 +379,7 @@ class DevSpacesServerStepView( } override fun onNext(): Boolean { - val selectedCluster = tfServer.selectedItem as? Cluster ?: return false + val selectedCluster = getSelectedCluster() ?: return false val server = selectedCluster.url val serverDisplay = server.removePrefix("https://").removePrefix("http://") val strategy = currentStrategy ?: return false @@ -474,7 +508,7 @@ class DevSpacesServerStepView( } private suspend fun saveKubeconfig(cluster: Cluster, token: String, indicator: ProgressIndicator) { - if (!saveToKubeconfig || token.isBlank()) return + if (!saveConfig || token.isBlank()) return try { indicator.text = "Updating Kube config..." @@ -496,7 +530,7 @@ class DevSpacesServerStepView( } private suspend fun saveKubeconfigWithCert(cluster: Cluster, clientCertPem: String, clientKeyPem: String, indicator: ProgressIndicator) { - if (!saveToKubeconfig + if (!saveConfig || clientCertPem.isBlank() || clientKeyPem.isBlank()) return @@ -542,6 +576,7 @@ class DevSpacesServerStepView( tfClientCert.text = toSelect?.clientCert?.value ?: "" tfClientKey.text = toSelect?.clientKey?.value ?: "" } + findStrategy()?.applyFromCluster(toSelect) } private fun startKubeconfigMonitor() { diff --git a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/AbstractAuthenticationStrategy.kt b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/AbstractAuthenticationStrategy.kt index 7052984d..7b164a54 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/AbstractAuthenticationStrategy.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/AbstractAuthenticationStrategy.kt @@ -38,6 +38,8 @@ abstract class AbstractAuthenticationStrategy( return (tfServer as? javax.swing.JComboBox<*>)?.selectedItem != null } + override fun isDirty(saved: Cluster): Boolean = false + /** * Creates a validated API client. */ diff --git a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/AuthenticationStrategy.kt b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/AuthenticationStrategy.kt index e58e3ecf..4c61b748 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/AuthenticationStrategy.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/AuthenticationStrategy.kt @@ -63,4 +63,9 @@ interface AuthenticationStrategy { * Determines if the "Next" button should be enabled for this authentication method. */ fun isNextEnabled(): Boolean + + /** + * Returns `true` if the current values in this strategy differs from the given config + */ + fun isDirty(saved: Cluster): Boolean } diff --git a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/ClientCertificateAuthenticationStrategy.kt b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/ClientCertificateAuthenticationStrategy.kt index ba142010..09682f25 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/ClientCertificateAuthenticationStrategy.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/ClientCertificateAuthenticationStrategy.kt @@ -104,4 +104,15 @@ class ClientCertificateAuthenticationStrategy( isServerSelected() && tfClientCert.text.isNotBlank() && tfClientKey.text.isNotBlank() + + /** + * Dirty vs kubeconfig only once both PEM paths/contents are filled; an empty tab after switching is not dirty. + */ + override fun isDirty(saved: Cluster): Boolean { + val cert = tfClientCert.text.trim() + val key = tfClientKey.text.trim() + if (cert.isEmpty() || key.isEmpty()) return false + return cert != (saved.clientCert?.value ?: "").trim() + || key != (saved.clientKey?.value ?: "").trim() + } } diff --git a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/OpenShiftCredentialsAuthenticationStrategy.kt b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/OpenShiftCredentialsAuthenticationStrategy.kt index b3f9cd9a..ecab11b9 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/OpenShiftCredentialsAuthenticationStrategy.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/OpenShiftCredentialsAuthenticationStrategy.kt @@ -148,8 +148,35 @@ class OpenShiftCredentialsAuthenticationStrategy( devSpacesContext.client = client } + override fun isDirty(saved: Cluster): Boolean { + val username = tfUsername.text.trim() + val password = tfPassword.passwordField.password ?: CharArray(0) + if (username.isEmpty() || password.isEmpty()) return false + return (username == (saved.basicUsername ?: "").trim()) + && !passwordDiffersFromKube(saved.basicPassword, tfPassword.passwordField.password) + } + + private fun passwordDiffersFromKube(kube: String?, current: CharArray = CharArray(0)): Boolean { + val kubeChars = kube?.toCharArray() ?: CharArray(0) + val isKubeBlank = kubeChars.isEmpty() + val isCurrentBlank = current.isEmpty() + + return isKubeBlank != isCurrentBlank + || !kubeChars.contentEquals(current) } + override fun isNextEnabled(): Boolean = isServerSelected() && tfUsername.text.isNotBlank() && tfPassword.passwordField.password?.isNotEmpty() == true + + /** Fills fields from kubeconfig-backed [cluster] when switching selection. */ + fun applyFromCluster(cluster: Cluster?) { + if (cluster == null) { + tfUsername.text = "" + tfPassword.passwordField.text = "" + return + } + tfUsername.text = cluster.basicUsername.orEmpty() + tfPassword.passwordField.text = cluster.basicPassword.orEmpty() + } } diff --git a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/OpenShiftOAuthAuthenticationStrategy.kt b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/OpenShiftOAuthAuthenticationStrategy.kt index b6301555..74d856f2 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/OpenShiftOAuthAuthenticationStrategy.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/OpenShiftOAuthAuthenticationStrategy.kt @@ -103,4 +103,9 @@ class OpenShiftOAuthAuthenticationStrategy( override fun isNextEnabled(): Boolean = isServerSelected() + + /** + * Browser login always yields a new token; there is no pre-login field to diff against kubeconfig. + */ + override fun isDirty(saved: Cluster): Boolean = true } diff --git a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/RedHatSSOAuthenticationStrategy.kt b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/RedHatSSOAuthenticationStrategy.kt index c90b3585..2fb846ce 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/RedHatSSOAuthenticationStrategy.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/RedHatSSOAuthenticationStrategy.kt @@ -49,13 +49,6 @@ class RedHatSSOAuthenticationStrategy( row { label(DevSpacesBundle.message("connector.wizard_step.openshift_connection.text.redhat_sso_info")) } - row { - label( - DevSpacesBundle.message("connector.wizard_step.openshift_connection.text.redhat_sso_token_note") - ).comment( - DevSpacesBundle.message("connector.wizard_step.openshift_connection.text.pipeline_token_comment") - ) - } } override suspend fun authenticate( @@ -106,4 +99,10 @@ class RedHatSSOAuthenticationStrategy( override fun isNextEnabled(): Boolean = isServerSelected() + + /** + * Browser login always yields a new token + */ + override fun isDirty(saved: Cluster): Boolean = true + } diff --git a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/TokenAuthenticationStrategy.kt b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/TokenAuthenticationStrategy.kt index e5242c18..289035f7 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/TokenAuthenticationStrategy.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/view/steps/auth/TokenAuthenticationStrategy.kt @@ -112,6 +112,24 @@ class TokenAuthenticationStrategy( isServerSelected() && tfToken.password?.isNotEmpty() == true + /** + * Dirty vs kubeconfig only once the token field has content; an empty field after switching tabs is not dirty. + */ + override fun isDirty(saved: Cluster): Boolean { + val cur = tfToken.password ?: CharArray(0) + if (cur.isEmpty()) return false + return tokenDiffers(saved.token, cur) + } + + private fun tokenDiffers(savedToken: String?, current: CharArray = CharArray(0)): Boolean { + val saved = savedToken.orEmpty() + val savedBlank = saved.isBlank() + val curEmpty = current.isEmpty() + if (savedBlank && curEmpty) return false + if (savedBlank != curEmpty) return true + return !saved.toCharArray().contentEquals(current) + } + /** * Start monitoring clipboard for tokens. * Should be called during initialization. From 9bc10e8e85cc4022db7337d9c21454640526cc99 Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Fri, 15 May 2026 17:27:42 +0200 Subject: [PATCH 2/2] fix: always rebuild clean, prevent cached Signed-off-by: Andre Dietisheim --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 39d9545a..b3b7ff5c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -111,9 +111,10 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 - # Run tests + # Run tests (clean + disable build cache: avoid stale compileTestKotlin from shared + # remote cache when main Kotlin ABI changes, e.g. NoSuchMethodError on data class ) - name: Run Tests - run: ./gradlew check + run: ./gradlew clean check --no-build-cache # Collect Tests Result of failed tests - name: Collect Tests Result