mwx / nextcloud-app
An app's own update command: reads the store, clears the fetcher's cache, and lets the server's installer take a stable or nightly release
Requires
- php: ^8.2
- symfony/console: ^7.3
Requires (Dev)
None
Suggests
- nextcloud/ocp: The OCP interfaces the command is built on; the server provides them at runtime
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-03 20:44:48 UTC
README
Building blocks a Nextcloud app ships inside itself,
the first of them its own update command.
Owns the mwx\Nextcloud\App\ namespace prefix.
MIT, requires php and symfony/console;
the OCP interfaces come from the server the app runs in (nextcloud/ocp).
composer require mwx/nextcloud-app
The update command
occ app:update --allow-unstable cannot reach a nightly:
the server's app fetcher answers its cached apps.json within its TTL
whatever the flag asks,
and re-filters that already filtered content on a 304 from the store,
so a nightly never appears until the store's ETag changes.
UpdateCommand gives an app <prefix>:update, which
- reads the store's
<appstoreurl>/apps.json, the list the fetcher reads, once, throughIClientService, for the installed version's kind and the newest stable and unstable release above it that this server and PHP are compatible with (StoreRelease,Release); - deletes the fetcher's cache,
apps.jsonin theappstoreapp-data folder, and its failure hold,settings/appstore-fetcher-lastFailure; - lets the server's own installer take the release:
it downloads, checks the certificate and the signature, extracts,
and upgrades (
AppUpdater, over theInstallerseam).
Nothing of the signature checking is done here.
Which release it takes
- no flag: an installed nightly takes the newest release of any kind above it; an installed stable takes the newest stable above it;
--nightly: the newest release of any kind above the installed;--stable: the newest stable above the installed.
"Above" is version_compare, as the installer's own check,
so nothing is ever downgraded,
and a nightly rebuilt under the installed version number is not an update.
The installed version is a nightly where its string carries a pre-release suffix,
or where the store lists that version as a nightly only; stable otherwise.
"Unstable" is what the server means by it: nightlies and pre-releases alike.
The channel decides the installer's $allowUnstable:
true for the nightly channel, false for the stable one.
A release is compatible as the server's fetcher decides it:
the Nextcloud minimum against the server, and required,
so * or <=33 is incompatible;
its maximum not at all, since the fetcher ignores it for apps.json;
and both PHP bounds against PHP, any PHP for *.
A release without isNightly: false is a nightly, as the fetcher reads it.
A range the server cannot parse empties the server's whole list;
here only that release is taken as incompatible.
On a server whose own update channel is beta, daily or git,
the fetcher admits unstable releases whatever $allowUnstable says.
Where the installer offers another version than the channel chooses,
the command fails, naming both, before anything is taken.
Options, output and exit codes
<prefix>:update [--check] [--stable|--nightly] [-o|--output plain|json|json_pretty]
--checkreports and installs nothing. It still clears the fetcher's cache, as every run does, so that the installer's answer is current; the installer then caches the list it fetched under its own flag. After a--nightly --check, on the stable channel, the Apps page may therefore show unstable releases of every app for up to the cache's TTL.--stableand--nightlyexclude each other; both are a usage error.--outputprints the report as plain text, the default, or as JSON.-ois its shortcut; an app whose other commands use-ootherwise turns it off throughoutputShortcut().
The report names the installed version and its kind,
the newest stable and the newest unstable release,
the channel and the release it chooses,
whether the store is enabled (appstoreenabled),
what the server's installer offers, which covers an apps directory
it cannot write to, and the outcome.
A store that cannot be read is reported as unknown,
and the installer is asked all the same.
Where the server's fetcher would not ask the store,
neither is asked:
with appstoreenabled false,
or with has_internet_connection false while appstoreurl is the default.
| Exit | Meaning |
|---|---|
| 0 | updated; under --check, something to take |
| 1 | nothing to take |
| 2 | a failure, or a usage error |
A failure is the installer failing, the store disabled or out of reach as above, the store listing a release the installer does not offer, or the installer offering another release than the one chosen.
In an app
The app subclasses UpdateCommand and names itself and the command:
namespace OCA\MyApp\Command;
use OCA\MyApp\Vendor\mwx\Nextcloud\App\Update\UpdateCommand;
class Update extends UpdateCommand
{
protected function appId(): string
{
return 'myapp';
}
protected function commandName(): string
{
return 'myapp:update';
}
/** Optional; none unless named. */
protected function commandAliases(): array
{
return [ 'myapp:upgrade' ];
}
/** Optional; `-o` unless overridden, null for no shortcut. */
protected function outputShortcut(): ?string
{
return null;
}
}
and registers it in appinfo/info.xml:
<commands>
<command>OCA\MyApp\Command\Update</command>
</commands>
The server's container builds the command through UpdateCommand's
constructor: IClientService, IConfig, IAppManager, IAppDataFactory
and IAppConfig it resolves;
the two optional arguments, an Installer and the server's version,
stay at their defaults there and are a test's to give.
The one internal class
OC\Installer is not in nextcloud/ocp.
ServerInstaller, the adapter behind the package's Installer interface,
is the one class that names it,
as a string resolved from the server's container, never constructed,
so the package's own analysis does not need it.
The app owes the pin:
an integration test against the server it runs on,
asserting that OC\Installer exists and that
isUpdateAvailable( string $appId, bool $allowUnstable ): string|false
and updateAppstoreApp( string $appId, bool $allowUnstable ): bool
keep those signatures,
and its static analysis suppresses the class by name, with that reason.
Shipped scoped
An app installed from the store has no Composer autoloader of its own
at runtime, and two apps on different versions of this package must not clash.
The app therefore ships the package copied into its own namespace by
Strauss:
lib/Vendor/mwx/Nextcloud/App/, under OCA\MyApp\Vendor\mwx\Nextcloud\App\,
which the server's PSR-4 autoloading of lib/ loads.
symfony/console and the OCP interfaces are the server's
and are left out of the copy.
lib/Vendor/ is tracked, so a checkout runs without a build;
CI runs Strauss again and fails on a diff.
Development
In the monorepo,
vendor/bin/phpunit --testsuite nextcloud-app runs this package's tests,
on the doubles of mwx/nextcloud-testing,
against nextcloud/ocp in the root's require-dev.