Skip to content

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

Open
karl-bullock wants to merge 1 commit into
flarum:1.xfrom
karl-bullock:fix-core-major-detection-1x
Open

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

Conversation

@karl-bullock

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

Copy link
Copy Markdown
Contributor

Backport of #4947 to 1.x. Both defects are present on this branch verbatim, and this is the branch where they have a user-visible consequence, because it is the code a forum runs while upgrading from 1.8 to 2.0.

The two defects

1. flarum/core is filtered out of the update check. CheckForUpdatesHandler::handle() skips every package that is not an installed extension. flarum/core is not one, so it is dropped on every run, updates.installed never contains it, LastUpdateCheck::getNewMajorVersion() returns null, and MajorUpdateHandler throws NoNewMajorVersionException before doing any work. Over the API that is a 409 no_new_major_version.

The surrounding code is written to expect core in that list: the docblock above says the first composer outdated call is made so major updates are visible and "That includes flarum/core itself", getNewMajorVersion() searches the list for flarum/core, and on the frontend ControlSectionState.formatCoreUpdate() filters the same list for flarum/core and builds a pseudo-extension from it for MajorUpdater to render.

2. ComposerJson::require('*', '*') never relaxes anything. The wildcard branch tests Util::nameToId($packageName), and there $packageName is always the literal *, which never resolves to an extension. Every iteration continues, so no constraint is changed, and nameToId() emits an Undefined array key 1 warning per require entry. Measured on a forum with extensions pinned to ^1.8: 18 warnings, 0 constraints changed.

The second is masked by the first, so fixing either alone is not enough. With only the detection fix, a major update would run composer update with flarum/core pinned to the new major while every extension stayed on its 1.8 constraint, which cannot resolve.

strpos() is used rather than str_contains() to stay within this branch's >=7.3 || ^8.0 support.

End to end on a real forum

Flarum 1.8.19, extension manager enabled, every extension pinned to ^1.8, flarum/package-manager at ^1.0, minimum-stability: beta, PHP 8.4.24, Composer 2.10.2, MariaDB. The extension's src/ was replaced with this branch, and a genuine MajorUpdate was run with dryRun = false.

Before, on stock 1.x:

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, with this branch:

check-for-updates recorded               flarum/core
getNewMajorVersion()                     v2.0.0-rc.5
composer.json rewritten                  flarum/tags -> *, flarum/core -> ^2.0.0-rc.5
                                         (php platform requirement left alone)
vendor after the update                  flarum/core v2.0.0-rc.5, flarum/tags v2.0.0-rc.5
php flarum migrate                        clean
php flarum info                          Flarum core: 2.0.0-rc.5
GET /                                    200
GET /api, /api/discussions               200

So the forum completed 1.8.19 to 2.0.0-rc.5 through the extension manager's own code path and came back up.

One thing I hit that is not addressed here

After Composer finished writing vendor/, the still-running PHP process fatalled:

Declaration of Symfony\Component\Console\Output\NullOutput::write(...)
must be compatible with Symfony\Component\Console\Output\OutputInterface::write(...)

The update itself had already succeeded on disk, and the forum was healthy once migrations were run from a fresh process. The cause is that ComposerAdapter runs Composer in-process, and a 1.x to 2.x upgrade replaces symfony/console underneath the very process executing it, so the new class no longer matches the interface already loaded. That means the FlarumUpdated event and anything after it in MajorUpdateHandler::handle() do not run.

I have deliberately not touched that here, since it is a separate design question and this PR is already doing two things. Flagging it because these fixes are what make that code path reachable in the first place. Happy to open it separately if it is useful.

Note on reach

This extension does have a workflow on this branch, .github/workflows/flarum-extension-manager-backend.yml, but it sets enable_backend_testing: false, so its suite does not run in CI. I have not proposed changing that here. If you do enable it, note that the fixture in SetupComposer.php pins flarum/core to 1.0.0, which newer Composer refuses to install over published security advisories. This branch pins composer:2.2, which predates that check, so it would not bite today.

Since the last 1.x release of flarum/package-manager was v1.0.8 in March, this only reaches people upgrading from 1.8 if a further 1.0.x is tagged before 2.0.0 stable. That is entirely your call, but it is the reason I opened the backport rather than leaving it at #4947.

One note on the commit message: it uses the scope package manager, taken from the directory name. The extension publishes as flarum/extension-manager and titles itself Extension Manager, so the PR title has been corrected to match. I cannot amend the published commit from here.

…laxation

Two independent defects stop a major update from ever completing.

CheckForUpdatesHandler skipped every 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 Util::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 with no vendor segment.
@karl-bullock
karl-bullock requested a review from a team as a code owner August 17, 2026 04:21
@karl-bullock karl-bullock changed the title [1.x] fix(package manager): repair major-update detection and constraint relaxation [1.x] fix(extension manager): repair major-update detection and constraint relaxation Aug 17, 2026
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