Skip to content

fix(migrations): keep app config when Nextcloud replays changeSchema() - #1192

Merged
saw-jan merged 1 commit into
masterfrom
fix/replay-safe-migrations
Oct 16, 2026
Merged

saw-jan merged 1 commit into
masterfrom
fix/replay-safe-migrations

Conversation

@saw-jan

@saw-jan saw-jan commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Port of #1191 to allow the CI to run
Closes #1191

Problem

On Nextcloud 35, every occ upgrade ends with a database schema check (OC\DB\SchemaChecker, also available as occ db:schema:check). The check replays the changeSchema() of every migration of every app into an in-memory schema. Two migrations of this app write app config inside changeSchema(), so the replay changes the real settings:

  • Version2001Date20221213083550 copies the pre-2.0 keys (oauth_instance_url, client_id, client_secret) into openproject_instance_url, openproject_client_id and openproject_client_secret. Those old keys are long gone, so it copies empty values and wipes the current settings.
  • Version2400Date20230504144300 sets fresh_project_folder_setup to 1, undoing a project folder setup done since.

Nextcloud then treats the integration as not configured: no OpenProject links, no dashboard widget, no OpenProject tab on files. OpenProject's side (the file storage and its project folders) keeps working. This happens on any occ upgrade, including the one that runs when another app is installed or updated.

The check was added in Nextcloud 35.0.0 by nextcloud/server#63351 (not backported to 34): earlier versions run changeSchema() only during the actual migration, so the problem does not exist there.

Steps to reproduce, on Nextcloud 35 with the integration configured (seen with 3.2.0 and with the 35 nightly 3.3.0-20260922):

  1. occ config:app:get integration_openproject openproject_instance_url returns the URL.
  2. occ db:schema:check (or occ upgrade).
  3. The same command returns an empty value, and fresh_project_folder_setup is 1.

Change

Moving the writes to postSchemaChange() is not an option: Nextcloud installs an app's migrations schema-only (Installer::installApp() calls migrate('latest', $previousVersion === '')), so on a fresh install changeSchema() is the only hook that runs, which is presumably also why #894 moved a migration into it. The two migrations are made safe to replay instead:

  • Version2001Date20221213083550 does nothing once none of the old keys exists.
  • Version2400Date20230504144300 sets fresh_project_folder_setup only when the key is not there yet. After that the admin settings keep it up to date (0 after the setup, 1 after a reset), as before.

Testing

The two migrations run against a stubbed IConfig, before and after the change:

Case Before After
Upgrade from pre-2.0 keys keys moved, fresh_project_folder_setup = 1 same
Fresh install (no keys) openproject_instance_url, openproject_client_id, openproject_client_secret created empty, fresh_project_folder_setup = 1 only fresh_project_folder_setup = 1
Replay on a configured instance (fresh_project_folder_setup = 0) URL, client id and secret emptied, fresh_project_folder_setup = 1 unchanged

The "before" replay is exactly what we see on our Nextcloud 35 instances after each occ upgrade, where we have been putting the URL back by script since.

Related Issue or Workpackage

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Tests only (no source changes)

Checklist:

  • Code changes
  • Unit tests added
  • Acceptance tests added
  • Updated CHANGELOG.md file

Nextcloud replays the changeSchema() of every migration to check the
database schema: `occ db:schema:check`, and at the end of every
`occ upgrade` since Nextcloud 35. Two migrations write app config there:

- Version2001Date20221213083550 copies the pre-2.0 keys into the new
  ones. Replayed after those keys are gone, it copies empty values:
  openproject_instance_url, openproject_client_id and
  openproject_client_secret are wiped, and the integration shows as not
  configured.
- Version2400Date20230504144300 sets fresh_project_folder_setup to 1,
  undoing a project folder setup done since.

Moving them to postSchemaChange() is not an option: a fresh install runs
the migrations schema-only, so changeSchema() is all that runs there.
Make both safe to replay instead: the first does nothing once the old
keys are gone, the second sets the key only when it is not there yet.

Signed-off-by: Volodymyr Fesko <vladimir.fesko@gmail.com>
@github-actions

Copy link
Copy Markdown

PHP Code Coverage

Coverage after merging fix/replay-safe-migrations into master will be
69.56%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
integration_openproject/server/apps/integration_openproject/lib
   Capabilities.php0%100%0%0%24, 31–40
   OIDCClientMapper.php0%100%0%0%29, 40–49, 52
   ServerVersion.php0%100%0%0%17, 27–29, 32, 41–42
   TokenEventFactory.php100%100%100%100%
integration_openproject/server/apps/integration_openproject/lib/AppInfo
   Application.php37.04%100%50%36%116–118, 121–135, 138, 140, 142–143, 146, 151–152, 156–159, 161–172, 174, 177, 181, 185–187, 220
integration_openproject/server/apps/integration_openproject/lib/BackgroundJob
   RemoveExpiredDirectUploadTokens.php0%100%0%0%28, 30–32, 41–42
integration_openproject/server/apps/integration_openproject/lib/Controller
   ConfigController.php77.98%100%70.59%78.32%106, 143–144, 146, 148–150, 152–155, 158–159, 161, 186, 190–192, 377–379, 381–383, 432, 512–516, 527, 541–544, 552, 556–559, 601–616, 657, 660–675, 689–695
   DirectDownloadController.php0%100%0%0%33–35, 50–52, 54–61
   DirectUploadController.php71.03%100%100%70.21%117–119, 162–164, 175, 179–182, 184, 194, 201, 217–219, 221–222, 225–230, 233, 235, 245–247, 253–255, 263–265, 280–282, 301, 306, 312
   FilesController.php85.83%100%83.33%85.95%181–182, 246, 255, 272–275, 280–282, 287–289, 301, 303, 306
   OpenProjectAPIController.php81.09%100%82.35%80.98%116, 155, 202–204, 207–214, 216–220, 222, 241, 266, 331, 381, 401, 448, 473–475, 478–482, 484, 66
   OpenProjectController.php96.45%100%80%96.95%241–245
integration_openproject/server/apps/integration_openproject/lib/Dashboard
   OpenProjectWidget.php0%100%0%0%101, 108–109, 111–116, 118–122, 124–126, 129–140, 61–66, 73, 80, 87, 94
integration_openproject/server/apps/integration_openproject/lib/Exception
   OpenprojectAvatarErrorException.php100%100%100%100%
   OpenprojectErrorException.php100%100%100%100%
   OpenprojectFileNotUploadedException.php100%100%100%100%
   OpenprojectGroupfolderSetupConflictException.php100%100%100%100%
   OpenprojectResponseException.php100%100%100%100%
   OpenprojectUnauthorizedUserException.php0%100%0%0%21
integration_openproject/server/apps/integration_openproject/lib/Listener
   BeforeGroupDeletedListener.php100%100%100%100%
   BeforeNodeInsideOpenProjectGroupfilderChangedListener.php0%100%0%0%46–48, 52–55, 57, 59, 62–63, 65, 67–70, 72–75, 77–79
   BeforeUserDeletedListener.php40%100%50%37.50%30, 38, 42–43, 45
   LoadAdditionalScriptsListener.php0%100%0%0%37–38, 46–47, 49, 51–52, 54
   LoadSidebarScript.php65.91%100%100%64.29%105, 77–88, 95, 98
   OpenProjectReferenceListener.php0%100%0%0%45–47, 54–55, 57, 59–60, 62–74
   TermsOfServiceEventListener.php0%100%0%0%41–42, 47–48, 50–51, 53–55, 58–62
   UserChangedListener.php0%100%0%0%34, 41–42, 45–50, 53
   UserLoggedInEventListener.php0%100%0%0%27, 35–36, 39, 42
integration_openproject/server/apps/integration_openproject/lib/Migration
   Version2001Date20221213083550.php0%100%0%0%30, 41–43, 46–54, 56–64, 66–68, 70
   Version20100Date20250820101358.php0%100%0%0%38, 47–53, 56
   Version2310Date20230116153411.php0%100%0%0%29, 32–35, 37–62, 64–65, 67
   Version2400Date20230504144300.php0%100%0%0%30, 41–44, 46
   Version2640Date20240628114301.php0%100%0%0%35, 47–49, 52–53, 56
   Version2900Date20250718065820.php0%100%0%0%22, 33–36, 38–39, 41–42, 45
integration_openproject/server/apps/integration_openproject/lib/Reference
   WorkPackageReferenceProvider.php51.67%100%25%58.33%100–101, 104, 108, 142, 150–151, 159, 37, 44, 51, 58–60, 87, 93–96, 99
integration_openproject/server/apps/integration_openproject/lib/Search
   OpenProjectSearchProvider.php0%100%0%0%100–102, 104–106, 109–110, 112–113, 116–125, 127–131, 52–55, 62, 69, 77, 79, 82, 89–90, 93–97, 99
   OpenProjectSearchResultEntry.php100%100%100%100%
integration_openproject/server/apps/integration_openproject/lib/Service
   DatabaseService.php42.31%100%60%40.43%108–112, 114, 63–76, 78–85
   DirectDownloadService.php100%100%100%100%
   DirectUploadService.php42.86%100%66.67%40%101, 62–65, 67–75, 95
   OauthService.php96.15%100%100%95.65%124, 144
   OpenProjectAPIService.php78.41%100%80%78.29%1005–1006, 1008, 1010–1011,

@saw-jan

saw-jan commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

/backport! to release/3.3

@saw-jan
saw-jan merged commit 743c1b3 into master Oct 16, 2026
10 checks passed
@saw-jan
saw-jan deleted the fix/replay-safe-migrations branch October 16, 2026 04:26
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.

3 participants