Skip to content

[2.x] fix(extension manager): repair major-update detection and constraint relaxation - #4947

Open
karl-bullock wants to merge 2 commits into
flarum:2.xfrom
karl-bullock:fix-core-major-detection
Open

[2.x] fix(extension manager): repair major-update detection and constraint relaxation#4947
karl-bullock wants to merge 2 commits into
flarum:2.xfrom
karl-bullock:fix-core-major-detection

Conversation

@karl-bullock

@karl-bullock karl-bullock commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Affected versions: based on 2.x at ff36902 (Application::VERSION = 2.0.0-rc.6). Both defects are present there.

The extension manager's major-update feature cannot complete, for any version, because of two independent defects in the same path. The second is masked by the first, so fixing either alone is not enough.

1. flarum/core is filtered out of the update check

CheckForUpdatesHandler::handle() skips every package that is not an installed extension:

// Skip if not an extension
if (! $this->extensions->getExtension(Extension::nameToId($mainPackageUpdate['name']))) {
    continue;
}

Extension::nameToId('flarum/core') is flarum-core, and flarum/core is not a flarum-extension package, so getExtension() returns null and core is dropped on every run. Nothing for flarum/core is ever written to updates.installed, so LastUpdateCheck::getNewMajorVersion() returns null and MajorUpdateHandler throws NoNewMajorVersionException before doing any work. Over the API that is a 409 no_new_major_version.

This looks like an oversight rather than a deliberate exclusion, because the surrounding code is written to expect core in that list:

  • the docblock immediately above says the first composer outdated call is made so major updates are visible, and that "That includes flarum/core itself"
  • LastUpdateCheck::getNewMajorVersion() searches updates.installed for a flarum/core entry
  • on the frontend, ControlSectionState.formatCoreUpdate() filters the same list for flarum/core and builds a pseudo-extension from it, MajorUpdater renders coreUpdate['latest-major'], and ExtensionCard has a dedicated isCore branch

All of that is unreachable today.

2. ComposerJson::require('*', '*') never relaxes anything

MajorUpdateHandler::updateComposerJson() sets every direct requirement to * and then pins flarum/core to the new major. The wildcard branch tests the wrong variable:

foreach ($composerJson['require'] as $p => $v) {
    ...
    // Only extensions can all be set to * versioning.
    if (! $this->extensions->getExtension(Extension::nameToId($packageName))) {
        continue;
    }

In that branch $packageName is always the literal *. Extension::nameToId('*') never resolves to an extension, so every iteration continues and no constraint is ever changed. It also emits an Undefined array key 1 warning per require entry, since * has no vendor segment.

The result is that a major update would run composer update with flarum/core pinned to the new major while every extension stayed on its old constraint, which is exactly the unresolvable state the upgrade guide's troubleshooting section tells people to fix by hand.

Why this has not been reported through the UI

Updater.tsx only renders MajorUpdater when isProductionReady(core.package['latest-major']), and that returns true only for stable. With no stable 2.0.0 released yet, the panel does not render for an rc target regardless, so the two defects behind it have not been exercised from the interface. The API endpoint has no such gate, which is where the 409 shows up.

Measured, before and after

On a Flarum 1.8.19 forum with the extension manager enabled, extensions pinned to ^1.8, minimum-stability: beta, PHP 8.4.24, Composer 2.10.2. composer outdated --format json reports flarum/core 1.8.19 -> v2.0.0-rc.5 update-possible in this state.

Before:

packages recorded by check-for-updates   0
flarum/core recorded                     NO
getNewMajorVersion()                     NULL
MajorUpdateHandler                       NoNewMajorVersionException (no_new_major_version)

require('*','*')                         18 warnings, 0 constraints changed

After:

packages recorded by check-for-updates   1
flarum/core recorded                     YES
getNewMajorVersion()                     v2.0.0-rc.5
MajorUpdateHandler (dry run)             completes, composer.json correctly reverted

require('*','*')                         0 warnings, all extensions -> *,
                                         flarum/core -> ^2.0.0, php -> >=8.0 untouched

Scope, stated plainly

The runtime verification above was done against v1.0.8, because that is the code a forum upgrading from 1.8 actually runs, and it is where this has a user-visible consequence today. Both files have the same defect verbatim on 2.x, which is what this patch targets. If you would like the same fix on the 1.x line so it reaches people upgrading from 1.8, I am happy to open that separately.

extensions/package-manager is not listed in monorepo_tests in .github/workflows/backend.yml, so CI does not exercise this extension. Enabling it needs two things first. Its tests/integration/setup.php is the only one of the seventeen extensions here still requiring ../../vendor/autoload.php rather than the php-packages/testing/bootstrap/monorepo.php bootstrap every other one uses. And the fixture in SetupComposer.php pins flarum/core to 1.0.0, which current Composer declines to install because of published security advisories, so composer test:setup fails before any test runs. I hit that on Composer 2.10.2, and this workflow installs composer:v2. Happy to look at both separately if that is useful.

Ubuntu added 2 commits August 17, 2026 02:37
The update check skips any package that is not an installed extension.
flarum/core is not one, so it was dropped on every run, even though
surfacing a new major of it is the stated purpose of the first composer
outdated call, and both LastUpdateCheck::getNewMajorVersion() and the
admin frontend read a flarum/core entry back out of that list.
ComposerJson::require('*', '*') tested Extension::nameToId($packageName),
which in the wildcard branch is always the literal '*'. That never resolves
to an extension, so every iteration continued and no constraint was ever
relaxed, while nameToId() emitted a warning per require entry. Test $p, the
package being iterated, and skip platform requirements that have no vendor
segment.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant