From 9e74f5523e3e3f80ff6aa3b13ec2c5b974e2b27a Mon Sep 17 00:00:00 2001 From: Conduction Release Bot Date: Thu, 6 Aug 2026 15:00:55 +0200 Subject: [PATCH] fix(install): write core's config keys with core's type, so apps can be enabled Every app installed through App Versions could be installed but not enabled. `occ app:enable ` died with conflict between new type (mixed) and old type (string) which reads like a schema problem and is not one. It appeared on a completely clean Nextcloud, which is the tell: there was no old schema to conflict with. Since Nextcloud 29 each appconfig row carries a type, and core only accepts a write whose type differs from the stored one when the stored type is VALUE_MIXED. Core writes installed_version, enabled, types and its own remote_/public_ routes through the untyped IConfig::setAppValue(), so those rows are MIXED. The finalizer wrote them with setValueString(), leaving VALUE_STRING rows that core could no longer update, so the very next enable threw. Confirmed by comparing an app installed normally against one installed through App Versions: files had type=2 (MIXED) on those keys, pipelinq had type=4 (STRING). Core's keys now go through setAppValue(), matching core exactly. IConfig::setAppValue is deprecated, but it is the only public API that writes untyped, and matching core's type is the whole point. Config this app owns stays on the typed API. Verified on a clean Nextcloud 31: install pipelinq 0.6.0-dev through App Versions, then `occ app:enable pipelinq` succeeds, and its installed_version/enabled/types rows are type=2 exactly like files. --- lib/Service/Installer/InstallFinalizer.php | 37 +++++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/lib/Service/Installer/InstallFinalizer.php b/lib/Service/Installer/InstallFinalizer.php index 688ad8f..fb7f3ff 100644 --- a/lib/Service/Installer/InstallFinalizer.php +++ b/lib/Service/Installer/InstallFinalizer.php @@ -23,6 +23,7 @@ use OCP\BackgroundJob\IJob; use OCP\BackgroundJob\IJobList; use OCP\IAppConfig; +use OCP\IConfig; use OCP\Migration\IOutput; use OCP\Server; use Psr\Log\LoggerInterface; @@ -42,6 +43,13 @@ class InstallFinalizer { public function __construct( private IAppConfig $appConfig, + /** + * Only for the handful of keys Nextcloud core owns and writes itself. + * IConfig::setAppValue() is deprecated, but it is the sole public way + * to store a value untyped (VALUE_MIXED), and matching core's own type + * is the entire point. See the comment on the writes below. + */ + private IConfig $config, private IAppManager $appManager, private IJobList $jobList, private LoggerInterface $logger, @@ -117,18 +125,35 @@ public function finalize(string $appPath, array $info, string $enabled, ?IOutput $installedVersion = $infoVersion !== '' ? $infoVersion : $this->appManager->getAppVersion($appId, false); - $this->appConfig->setValueString($appId, 'installed_version', $installedVersion); - $this->appConfig->setValueString($appId, 'enabled', $enabled); + /** + * setAppValue, not setValueString, for the keys core owns. + * + * Since Nextcloud 29 every appconfig row carries a type, and core only + * tolerates a write whose type differs from the stored one when the + * stored type is VALUE_MIXED. Core writes installed_version, enabled + * and its own remote_/public_ routes through the untyped + * IConfig::setAppValue(), so they land as MIXED. Writing them as + * VALUE_STRING here left a typed row that core could no longer update, + * and the next `occ app:enable` died with + * + * conflict between new type (mixed) and old type (string) + * + * That made every app installed through App Versions impossible to + * enable, which is most of the point of the app. Config we own stays + * on the typed API; only core's keys use core's semantics. + */ + $this->config->setAppValue($appId, 'installed_version', $installedVersion); + $this->config->setAppValue($appId, 'enabled', $enabled); /** @var array $remote */ $remote = (array)($info['remote'] ?? []); foreach ($remote as $name => $path) { - $this->appConfig->setValueString('core', 'remote_' . $name, $appId . '/' . $path); + $this->config->setAppValue('core', 'remote_' . $name, $appId . '/' . $path); } /** @var array $public */ $public = (array)($info['public'] ?? []); foreach ($public as $name => $path) { - $this->appConfig->setValueString('core', 'public_' . $name, $appId . '/' . $path); + $this->config->setAppValue('core', 'public_' . $name, $appId . '/' . $path); } $this->persistAppTypes($appId); @@ -161,7 +186,9 @@ private function persistAppTypes(string $appId): void { if (is_array($appInfo) && isset($appInfo['types']) && is_array($appInfo['types'])) { $types = implode(',', array_map('strval', $appInfo['types'])); } - $this->appConfig->setValueString($appId, 'types', $types); + // Core-owned key as well: OC_App writes app types untyped, so a typed + // row here would collide the same way installed_version did. + $this->config->setAppValue($appId, 'types', $types); } private static function includeAppScript(string $script): void {