Skip to content

fix(migrations): a migration that ships without a version bump reaches nobody - #3451

Merged
rubenvdlinde merged 1 commit into
developmentfrom
fix/migration-needs-a-version-bump
Sep 5, 2026
Merged

fix(migrations): a migration that ships without a version bump reaches nobody#3451
rubenvdlinde merged 1 commit into
developmentfrom
fix/migration-needs-a-version-bump

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

A migration that ships without a version bump reaches nobody

Found while updating a live instance. development had picked up new migrations
but the app version had not moved, so occ upgrade ran none of them — including
the one that creates the run-lock table #3444 depends on. The feature was simply
absent, with no error anywhere.

Reproduced, not inherited

Throwaway NC 34.0.3 rig on a free port, openregister
2.0.15-unstable.20260905134511 installed from its release tarball, 204
migrations, installed_version equal to <version>.

step result
add Version1Date20260906090000.php (creates a table), leave <version> alone, occ upgrade No upgrade required. exit 0
table created? no (0 rows in information_schema.tables)
row in oc_migrations? no
occ app:update openregister up-to-date or no updates could be found, still no table
change nothing but <version> (2.0.15-unstable… → 2.0.16), occ upgrade Updated <openregister> to 2.0.16
table created, migration recorded? yes

The code was byte-identical across the failing and passing runs. One line of XML
is the entire gate.

And the instrument agreed with the failure:

 >> Executed Migrations:              204
 >> Executed Unavailable Migrations:  204
 >> Available Migrations:             205
 >> New Migrations:                   205
 >> Pending Migrations:               None

Which half is the defect

The version discipline. A PR that adds a migration must move <version> in
the same change, and until now nothing said so or checked it. Measured on the
real history: <version> last moved on 2026-09-03 (85e8321), and development
has added four migrations since, none of which could reach an instance
already on that version.

Waiting for the release bump PR is not enough, because between releases
development is what people deploy — and those bump PRs queue up (#3450 is open
right now).

The fix fits the pattern rather than inventing a third mechanism. sc#396's answer
— derive the gate value from the content it gates — does not transfer here: this
value is a published App Store semver, not an internal signature, so it cannot be
computed. dossiq#1786's answer applies instead: a deliberate bump, in the same
change, now with something mechanical behind it.

The reporting is Nextcloud's, so it is documented rather than worked around.
Three of the five counting fields in occ migrations:status are wrong, and all
three defects are in core:

  • Pending Migrations: NoneMigrationService::describeMigrationStep()
    builds its list under if ($migration->name()), and
    SimpleMigrationStep::name() returns '' by default. None of our 204
    migrations override it
    , so this field reads None for openregister always,
    whatever the database says. It is not a wrong answer about this branch; it is a
    field that has never once been able to answer.
  • New Migrations / Executed Unavailable Migrations
    StatusCommand::getMigrationsInfos() calls array_keys() on
    getAvailableVersions(), which returns a list. Both fields therefore diff
    version strings against the integers 0..n and report the full count on every
    run. That is why the rig showed 204 and 205 with nothing wrong.
  • describeMigrationStep('lastest') — the alias is misspelled in core. Harmless
    only by luck: sortMigrations() falls through to strnatcmp, and Version…
    sorts below lastest.

The only honest pair in that output is Executed against Available. 204 of
205 means one migration has not run, whatever the line below it says. That is now
written down in lib/Migration/NAMING.md, along with the recovery — bump and
occ upgrade, or occ migrations:migrate openregister (verified on the rig: it
ran the pending step and created the table). Both migrations:* commands need
debug: true in config.php or occ answers Command "migrations:status" is not defined.

The mechanical check

scripts/check-migration-version-bump.php fails when a branch adds a file under
lib/Migration/ without moving <version> past its value at the merge base.
Committed, staged and untracked additions all count, and --no-renames is
deliberate: renaming an applied migration makes Nextcloud re-run it, which needs
a bump and a human.

It runs in Merge Hygiene (every push and PR, milliseconds), in
composer check:strict, and as a warning from .githooks/pre-commit.

It exits 2, not 0, when it cannot resolve the base ref or parse info.xml.
A check that cannot see the base has no verdict to give, and a silent pass is
precisely the failure this removes.

Run against the real history that caused the incident (--base=85e8321) it reds
and names all four files.

Tests, proven red

tests/Unit/Migration/MigrationVersionBumpCheckTest.php — 8 cases, each over a
purpose-built git repository, so a green is the check looking and finding
nothing rather than the branch happening to have no migrations: the defect, the
fix, a prerelease bump (2.0.14-unstable.…2.0.15-unstable.…), a control
that edits other files and passes, a lowered version, an untracked migration,
an unresolvable base ref, and a non-repository.

Proven red by mutation, not by assertion:

mutation result
gate always passes 3 failures
untracked files not collected 1 failure
unresolvable base returns 0 1 failure
restored OK (8 tests, 13 assertions)

The sweep

Same shape elsewhere — the fleet gating work on a version string a change can
forget to move:

  • Fixed already, at one layer. The whole-configuration import skip is decided
    by a content hash now, not a version (ImportHandler ~L2147, Added property close-after-click to every NcActionButton #426). That is
    the sc#396 answer, already landed.
  • Still version-only. The per-register and per-schema gates inside
    ImportHandler (L844, L998, L1873, L2872, L3491) compare versions with no
    content escape — the dossiq#1786 shape.
  • A hand-maintained duplicate that has already drifted. Eight Repair steps
    pass a REGISTER_VERSION PHP constant to that gate. Three of the nine
    (descriptor, version) pairs disagree with the descriptor's own info.version:
    ImportCredentialBrokerRegister (const 1.0.0, JSON 1.4.0),
    and both ImportDsarRegisters entries (const 1.2.0/1.1.0, JSON 1.0.0).
    The constant is what gates; the JSON version is read by
    RegisterDescriptorService::inventory() for the admin panel, so the panel and
    the importer can disagree about which version an instance is on. Filed
    separately rather than folded in here — deciding which of those numbers is
    canonical is a design ruling, not a gate.

Verified locally (CI is bottlenecked), by exit code

check exit
composer lint 0
composer check:migration-version 0
composer phpcs 0 (74 files)
composer psalm 0
composer phpstan 0 (1690 files, no errors)
composer test:unit 19,200 tests, 46,881 assertions, 0 failures, 43 skipped
new test file alone OK (8 tests, 13 assertions)
phpmd lib/Migration 0
hydra gates --scope-to-diff (v1.15.1) 0 — 21 applicable gates ran, all green

composer test:unit exits 1 on this host for an environmental reason, not a
failure: PHPUnit's runner warning No code coverage driver available under
failOnWarning. Reproduced on an untouched pre-existing test file
(tests/Unit/SearchControllerTest.php, exit 1, same single warning, 0 failures).
CI has a coverage driver.

Not run locally: the Integration, Database and Service suites (they boot a
Nextcloud server) and Playwright.

Refs #3444

🤖 Generated with Claude Code

…s nobody

Nextcloud reads an app's migration directory only when appinfo/info.xml's
<version> is greater than the installed_version it recorded. Equal versions
mean `occ upgrade` answers "No upgrade required.", exits 0, and opens no
migration file at all. Nothing is logged, nothing fails, and the feature that
needed the table is absent with no error anywhere.

Measured on a throwaway NC 34.0.3 rig running openregister
2.0.15-unstable.20260905134511 from its release tarball: a migration added with
<version> left alone did not run and was not recorded; changing nothing but
<version> and re-running `occ upgrade` ran it. The code was byte-identical
across the two runs.

This is not hypothetical. development carried four migrations added since
<version> last moved on 2026-09-03, one of them the run-lock table #3444
depends on, and an instance updated to that code got none of them.

scripts/check-migration-version-bump.php fails when a branch adds a file under
lib/Migration/ without moving <version> past its value at the merge base. It
runs in Merge Hygiene on every push and PR, in composer check:strict, and as a
warning from .githooks/pre-commit. Run against the real history it reds on
exactly those four files.

It exits 2, not 0, when it cannot resolve the base ref: a check that cannot see
the base has no verdict, and a silent pass is the failure this removes.

occ migrations:status cannot be used for this and is documented as such rather
than worked around. It is Nextcloud's command and three of its five counting
fields are wrong: Pending Migrations reads None for this app always, because
core filters the list on \$migration->name() and SimpleMigrationStep::name()
returns '' for all 204 of ours; New Migrations and Executed Unavailable both
call array_keys() on a list and so diff version strings against 0..n. The only
honest pair is Executed against Available, which is what read 204 of 205 on the
rig while the line below it said nothing was pending.

Refs #3444
@rubenvdlinde
rubenvdlinde merged commit 5c6d249 into development Sep 5, 2026
37 of 38 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/migration-needs-a-version-bump branch September 5, 2026 14:35
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ b06e5e8

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development
Hydra gates

Quality workflow — 2026-09-05 14:42 UTC

Download the full PDF report from the workflow artifacts.

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