From 40e72ec6826c1ba29296b33649dae11139abe383 Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Wed, 12 Aug 2026 16:28:56 +0200 Subject: [PATCH 1/5] fix(updates): allow development release upgrades --- .../nextcloudnative/app/DesktopAppUpdates.kt | 21 ++++++++++----- .../app/DesktopAppUpdatesTest.kt | 26 ++++++++++++++++--- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdates.kt b/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdates.kt index 754cb2384..034321285 100644 --- a/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdates.kt +++ b/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdates.kt @@ -39,6 +39,13 @@ internal data class DesktopUpdateTarget( val architecture: String, ) +internal fun canUseDirectDesktopUpdates( + buildIdentity: DesktopUpdateBuildIdentity, + target: DesktopUpdateTarget?, +): Boolean = target != null && + buildIdentity.versionCode > 0 && + (!buildIdentity.releaseBuild || buildIdentity.directPackageUpdates) + internal enum class DesktopPackageInstallerOutcome { InstallerHandoffStarted, InstallationCompleted, @@ -157,10 +164,7 @@ internal class DesktopAppUpdater( @Volatile private var cancellationRequested = false fun support(): AppUpdateSupport { - val canUpdate = buildIdentity.releaseBuild && - buildIdentity.directPackageUpdates && - target != null && - buildIdentity.versionCode > 0 + val canUpdate = canUseDirectDesktopUpdates(buildIdentity, target) return AppUpdateSupport( channel = if (canUpdate) { AppDistributionChannel.DirectDesktopPackage @@ -170,12 +174,15 @@ internal class DesktopAppUpdater( currentVersionName = buildIdentity.versionName, currentVersionCode = buildIdentity.versionCode, canCheckDirectUpdates = canUpdate, - explanation = if (canUpdate) { + explanation = if (canUpdate && !buildIdentity.releaseBuild) { + "This development build can update to a newer release from the selected channel. Downloads " + + "are matched to their advertised checksum before using your system installer." + } else if (canUpdate) { "This native package checks the selected release channel, matches downloads to its advertised " + "checksum, and uses your system installer." } else { - "Development, distribution-managed, and unsupported desktop packages are updated through " + - "their distribution workflow." + "Distribution-managed and unsupported desktop packages are updated through their distribution " + + "workflow." }, ) } diff --git a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdatesTest.kt b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdatesTest.kt index 797e6612c..b97eedfe5 100644 --- a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdatesTest.kt +++ b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdatesTest.kt @@ -291,7 +291,7 @@ class DesktopAppUpdatesTest { } @Test - fun onlyPackagedReleaseBuildsOfferDirectNativePackageUpdates() { + fun developmentAndEligibleReleaseBuildsOfferDirectNativePackageUpdates() { val node = Preferences.userRoot().node("desktop-update-test-${UUID.randomUUID()}") val directory = Files.createTempDirectory("desktop-update-support-test").toFile() try { @@ -309,6 +309,19 @@ class DesktopAppUpdatesTest { openInstaller = { DesktopPackageInstallerOutcome.InstallerHandoffStarted }, ) val development = DesktopAppUpdater( + preferences = node, + buildIdentity = DesktopUpdateBuildIdentity( + "development", + 10_000_001, + "0.1.0", + releaseBuild = false, + directPackageUpdates = false, + ), + target = DesktopUpdateTarget("linux", "rpm", "x86_64"), + updateDirectory = directory, + openInstaller = { DesktopPackageInstallerOutcome.InstallerHandoffStarted }, + ) + val unversionedDevelopment = DesktopAppUpdater( preferences = node, buildIdentity = DesktopUpdateBuildIdentity("development", 0, "0.1.0", false, false), target = DesktopUpdateTarget("linux", "rpm", "x86_64"), @@ -320,8 +333,11 @@ class DesktopAppUpdatesTest { assertTrue(release.support().canCheckDirectUpdates) assertTrue(release.support().explanation.contains("checksum")) assertFalse(release.support().explanation.contains("signed", ignoreCase = true)) - assertEquals(AppDistributionChannel.Development, development.support().channel) - assertFalse(development.support().canCheckDirectUpdates) + assertEquals(AppDistributionChannel.DirectDesktopPackage, development.support().channel) + assertTrue(development.support().canCheckDirectUpdates) + assertTrue(development.support().explanation.contains("development build")) + assertEquals(AppDistributionChannel.Development, unversionedDevelopment.support().channel) + assertFalse(unversionedDevelopment.support().canCheckDirectUpdates) val distributionManaged = DesktopAppUpdater( preferences = node, buildIdentity = DesktopUpdateBuildIdentity( @@ -337,7 +353,9 @@ class DesktopAppUpdatesTest { ) assertEquals(AppDistributionChannel.Development, distributionManaged.support().channel) assertFalse(distributionManaged.support().canCheckDirectUpdates) - assertTrue(distributionManaged.support().explanation.contains("distribution-managed")) + assertTrue( + distributionManaged.support().explanation.contains("distribution-managed", ignoreCase = true), + ) assertEquals(6L * 60L * 60L * 1_000L, DESKTOP_APP_UPDATE_CHECK_INTERVAL_MILLIS) val windowsRelease = DesktopAppUpdater( preferences = node, From 6e07ab9142a348661e0d2bc4d98526c771a3b6b9 Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Wed, 12 Aug 2026 16:28:56 +0200 Subject: [PATCH 2/5] docs(changelog): record development release updates --- changes/unreleased/development-build-release-updates.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 changes/unreleased/development-build-release-updates.md diff --git a/changes/unreleased/development-build-release-updates.md b/changes/unreleased/development-build-release-updates.md new file mode 100644 index 000000000..71ee1f92a --- /dev/null +++ b/changes/unreleased/development-build-release-updates.md @@ -0,0 +1,7 @@ +category: fix +issue: 327 +pull: none +platforms: desktop +user-facing: yes + +Development desktop builds can now update to a newer verified release from their selected remote update channel. From 7b6a0aa916cafd61c92b7d3e168fdae0edf5e635 Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Wed, 12 Aug 2026 16:31:07 +0200 Subject: [PATCH 3/5] docs(changelog): link development update PR --- changes/unreleased/development-build-release-updates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/unreleased/development-build-release-updates.md b/changes/unreleased/development-build-release-updates.md index 71ee1f92a..84a92c790 100644 --- a/changes/unreleased/development-build-release-updates.md +++ b/changes/unreleased/development-build-release-updates.md @@ -1,6 +1,6 @@ category: fix issue: 327 -pull: none +pull: 328 platforms: desktop user-facing: yes From 1aafb0fc3c164647320216431eff2e589920a8eb Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Wed, 12 Aug 2026 16:37:08 +0200 Subject: [PATCH 4/5] fix(updates): preserve development update opt-in --- .../nextcloudnative/app/DesktopAppUpdates.kt | 2 +- .../nextcloudnative/app/DesktopAppUpdatesTest.kt | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdates.kt b/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdates.kt index 034321285..0df06a4ac 100644 --- a/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdates.kt +++ b/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdates.kt @@ -44,7 +44,7 @@ internal fun canUseDirectDesktopUpdates( target: DesktopUpdateTarget?, ): Boolean = target != null && buildIdentity.versionCode > 0 && - (!buildIdentity.releaseBuild || buildIdentity.directPackageUpdates) + buildIdentity.directPackageUpdates internal enum class DesktopPackageInstallerOutcome { InstallerHandoffStarted, diff --git a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdatesTest.kt b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdatesTest.kt index b97eedfe5..9af423908 100644 --- a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdatesTest.kt +++ b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdatesTest.kt @@ -315,15 +315,22 @@ class DesktopAppUpdatesTest { 10_000_001, "0.1.0", releaseBuild = false, - directPackageUpdates = false, + directPackageUpdates = true, ), target = DesktopUpdateTarget("linux", "rpm", "x86_64"), updateDirectory = directory, openInstaller = { DesktopPackageInstallerOutcome.InstallerHandoffStarted }, ) + val optedOutDevelopment = DesktopAppUpdater( + preferences = node, + buildIdentity = DesktopUpdateBuildIdentity("development", 10_000_001, "0.1.0", false, false), + target = DesktopUpdateTarget("linux", "rpm", "x86_64"), + updateDirectory = directory, + openInstaller = { DesktopPackageInstallerOutcome.InstallerHandoffStarted }, + ) val unversionedDevelopment = DesktopAppUpdater( preferences = node, - buildIdentity = DesktopUpdateBuildIdentity("development", 0, "0.1.0", false, false), + buildIdentity = DesktopUpdateBuildIdentity("development", 0, "0.1.0", false, true), target = DesktopUpdateTarget("linux", "rpm", "x86_64"), updateDirectory = directory, openInstaller = { DesktopPackageInstallerOutcome.InstallerHandoffStarted }, @@ -336,6 +343,8 @@ class DesktopAppUpdatesTest { assertEquals(AppDistributionChannel.DirectDesktopPackage, development.support().channel) assertTrue(development.support().canCheckDirectUpdates) assertTrue(development.support().explanation.contains("development build")) + assertEquals(AppDistributionChannel.Development, optedOutDevelopment.support().channel) + assertFalse(optedOutDevelopment.support().canCheckDirectUpdates) assertEquals(AppDistributionChannel.Development, unversionedDevelopment.support().channel) assertFalse(unversionedDevelopment.support().canCheckDirectUpdates) val distributionManaged = DesktopAppUpdater( From 5a2e82803de67f2e7ff0a85993de54d20818acd0 Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Wed, 12 Aug 2026 18:18:32 +0200 Subject: [PATCH 5/5] fix(updates): enable CI development upgrades --- .github/workflows/ci.yml | 8 +++++-- tools/test-nightly-release-workflow.sh | 2 ++ .../nextcloudnative/app/DesktopAppUpdates.kt | 22 +++++++++++-------- .../app/DesktopAppUpdatesTest.kt | 3 +++ 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 555d3acb7..f6348abb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -214,10 +214,12 @@ jobs: run: | set -euo pipefail gradle_tasks=() + gradle_options=() if [[ "${RUN_CONTRACT}" == "true" ]]; then gradle_tasks+=(":contractAcquisition:test") fi if [[ "${RUN_DESKTOP}" == "true" ]]; then + gradle_options+=("-PncDirectDesktopPackageUpdates=true") gradle_tasks+=(":ui:desktopTest" ":ui:createDistributable") fi if [[ "${RUN_ANDROID}" == "true" ]]; then @@ -228,7 +230,7 @@ jobs: ) fi printf 'Selected Gradle tasks: %s\n' "${gradle_tasks[*]}" - ./gradlew --no-daemon "${gradle_tasks[@]}" + ./gradlew --no-daemon "${gradle_options[@]}" "${gradle_tasks[@]}" - name: Upload Linux desktop app if: >- @@ -335,7 +337,9 @@ jobs: steps.changes.outputs.windows == 'true' shell: pwsh run: | - .\gradlew.bat --no-daemon :ui:desktopTest :ui:packageMsi + .\gradlew.bat --no-daemon ` + -PncDirectDesktopPackageUpdates=true ` + :ui:desktopTest :ui:packageMsi - name: Verify Windows MSI metadata if: >- diff --git a/tools/test-nightly-release-workflow.sh b/tools/test-nightly-release-workflow.sh index d03610964..37d1dcbaa 100755 --- a/tools/test-nightly-release-workflow.sh +++ b/tools/test-nightly-release-workflow.sh @@ -4,6 +4,7 @@ set -euo pipefail project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" nightly="$project_root/.github/workflows/nightly.yml" prerelease="$project_root/.github/workflows/prerelease.yml" +ci="$project_root/.github/workflows/ci.yml" nightly_notes="$project_root/tools/nightly-release-notes.mjs" promotion="$project_root/tools/promote-app-update-channel.sh" msi_repackager="$project_root/tools/repackage-msi-with-uninstall-cleanup.ps1" @@ -76,6 +77,7 @@ require_text "$nightly" '-PncMacosPackageVersion="${NIGHTLY_DESKTOP_VERSION}"' require_text "$nightly" '-PncDesktopReleaseBuild=true' require_text "$nightly" '-PncDirectDesktopPackageUpdates="${{ matrix.direct_updates }}"' require_text "$nightly" 'direct_updates: "true"' +require_count "$ci" '-PncDirectDesktopPackageUpdates=true' 2 require_text "$nightly" 'name: nextcloud-native-${{ matrix.platform }}' require_text "$nightly" 'name: nextcloud-native-android' require_text "$nightly" 'tools/stage-nightly-assets.sh artifacts dist' diff --git a/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdates.kt b/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdates.kt index 0df06a4ac..86cec1483 100644 --- a/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdates.kt +++ b/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdates.kt @@ -174,15 +174,19 @@ internal class DesktopAppUpdater( currentVersionName = buildIdentity.versionName, currentVersionCode = buildIdentity.versionCode, canCheckDirectUpdates = canUpdate, - explanation = if (canUpdate && !buildIdentity.releaseBuild) { - "This development build can update to a newer release from the selected channel. Downloads " + - "are matched to their advertised checksum before using your system installer." - } else if (canUpdate) { - "This native package checks the selected release channel, matches downloads to its advertised " + - "checksum, and uses your system installer." - } else { - "Distribution-managed and unsupported desktop packages are updated through their distribution " + - "workflow." + explanation = when { + canUpdate && !buildIdentity.releaseBuild -> + "This development build can update to a newer release from the selected channel. Downloads " + + "are matched to their advertised checksum before using your system installer." + canUpdate -> + "This native package checks the selected release channel, matches downloads to its advertised " + + "checksum, and uses your system installer." + !buildIdentity.releaseBuild -> + "This development build cannot check for updates directly. Install a newer development build " + + "or release through the same download or package workflow that provided this build." + else -> + "Distribution-managed and unsupported desktop packages are updated through their distribution " + + "workflow." }, ) } diff --git a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdatesTest.kt b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdatesTest.kt index 9af423908..e4b892286 100644 --- a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdatesTest.kt +++ b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopAppUpdatesTest.kt @@ -345,8 +345,11 @@ class DesktopAppUpdatesTest { assertTrue(development.support().explanation.contains("development build")) assertEquals(AppDistributionChannel.Development, optedOutDevelopment.support().channel) assertFalse(optedOutDevelopment.support().canCheckDirectUpdates) + assertTrue(optedOutDevelopment.support().explanation.contains("development build")) + assertTrue(optedOutDevelopment.support().explanation.contains("cannot check for updates directly")) assertEquals(AppDistributionChannel.Development, unversionedDevelopment.support().channel) assertFalse(unversionedDevelopment.support().canCheckDirectUpdates) + assertTrue(unversionedDevelopment.support().explanation.contains("development build")) val distributionManaged = DesktopAppUpdater( preferences = node, buildIdentity = DesktopUpdateBuildIdentity(