test: add incremental db test

This commit is contained in:
2026-01-10 15:58:22 +02:00
parent a8e158d35b
commit a286bbdfe9
2 changed files with 378 additions and 32 deletions

View File

@@ -0,0 +1,339 @@
# Incremental migration test workflow
#
# This workflow tests that migrations work correctly when upgrading from an older version.
# It first installs the app at v0.14.0 (last version before forum_user_stats -> forum_users rename),
# then upgrades to the current version and verifies all migrations run successfully.
#
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: MIT
name: PHPUnit Incremental Migration
on: pull_request
permissions:
contents: read
concurrency:
group: phpunit-incremental-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
changes:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
src: ${{ steps.changes.outputs.src }}
steps:
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: changes
continue-on-error: true
with:
filters: |
src:
- '.github/workflows/**'
- 'appinfo/**'
- 'lib/**'
- 'tests/**'
- 'composer.json'
- 'composer.lock'
incremental-pgsql:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.src != 'false'
name: Incremental Migration (PostgreSQL)
services:
postgres:
image: ghcr.io/nextcloud/continuous-integration-postgres-16:latest # zizmor: ignore[unpinned-images]
ports:
- 4444:5432/tcp
env:
POSTGRES_USER: root
POSTGRES_PASSWORD: rootpassword
POSTGRES_DB: nextcloud
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
steps:
- name: Checkout app (current)
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
path: app-current
fetch-depth: 0
- name: Detect app ID from appinfo/info.xml
run: |
APP_ID=$(grep -oP '(?<=<id>)[^<]+' app-current/appinfo/info.xml | head -1)
echo "APP_NAME=$APP_ID" >> $GITHUB_ENV
echo "Detected app ID: $APP_ID"
- name: Get supported server versions
id: versions
uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1
with:
filename: app-current/appinfo/info.xml
- name: Save current app for later
run: |
mkdir -p /tmp/app-backup
cp -r app-current /tmp/app-backup/
- name: Checkout server
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
submodules: true
repository: nextcloud/server
ref: ${{ fromJson(steps.versions.outputs.branches)[0] }}
- name: Checkout app at v0.14.0 (pre-rename baseline)
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
ref: v0.14.0
path: apps/${{ env.APP_NAME }}
- name: Set up php 8.3
uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # v2.35.5
with:
php-version: '8.3'
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, pgsql, pdo_pgsql
coverage: none
ini-file: development
ini-values: disable_functions=
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set up dependencies (v0.14.0)
working-directory: apps/${{ env.APP_NAME }}
run: |
composer remove nextcloud/ocp --dev --no-scripts || true
composer i --no-scripts || composer i
- name: Set up Nextcloud and install app at v0.14.0
env:
DB_PORT: 4444
run: |
mkdir data
./occ maintenance:install --verbose --database=pgsql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
echo "::group::Installing app at v0.14.0"
./occ app:enable --force ${{ env.APP_NAME }}
echo "::endgroup::"
echo "::group::Database tables after v0.14.0 install"
./occ db:convert-filecache-bigint --no-interaction || true
PGPASSWORD=rootpassword psql -h 127.0.0.1 -p $DB_PORT -U root -d nextcloud -c "\dt oc_forum_*"
echo "::endgroup::"
- name: Upgrade app to current version
run: |
echo "::group::Replacing app with current version"
rm -rf apps/${{ env.APP_NAME }}
cp -r /tmp/app-backup/app-current apps/${{ env.APP_NAME }}
echo "::endgroup::"
- name: Set up dependencies (current)
working-directory: apps/${{ env.APP_NAME }}
run: |
composer remove nextcloud/ocp --dev --no-scripts || true
composer i
- name: Run migrations to current version
env:
DB_PORT: 4444
run: |
echo "::group::Running upgrade migrations"
# Disable maintenance mode if it was left on
./occ maintenance:mode --off || true
# Disable and re-enable the app to trigger migrations
# This simulates what happens when a user upgrades the app
./occ app:disable ${{ env.APP_NAME }}
./occ app:enable ${{ env.APP_NAME }}
echo "::endgroup::"
echo "::group::Database tables after upgrade"
PGPASSWORD=rootpassword psql -h 127.0.0.1 -p $DB_PORT -U root -d nextcloud -c "\dt oc_forum_*"
echo "::endgroup::"
echo "::group::Checking forum_users table exists"
PGPASSWORD=rootpassword psql -h 127.0.0.1 -p $DB_PORT -U root -d nextcloud -c "SELECT COUNT(*) FROM oc_forum_users" || exit 1
echo "::endgroup::"
- name: Check PHPUnit integration script is defined
id: check_integration
continue-on-error: true
working-directory: apps/${{ env.APP_NAME }}
run: |
composer run --list | grep '^ test:integration ' | wc -l | grep 1
- name: Run Nextcloud
if: steps.check_integration.outcome == 'success'
run: php -S localhost:8080 &
- name: PHPUnit integration
if: steps.check_integration.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:integration
- name: Print logs
if: always()
run: |
cat data/nextcloud.log
incremental-mysql:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.src != 'false'
name: Incremental Migration (MySQL)
services:
mysql:
image: ghcr.io/nextcloud/continuous-integration-mariadb-10.11:latest # zizmor: ignore[unpinned-images]
ports:
- 4444:3306/tcp
env:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: nextcloud
options: --health-cmd="mysqladmin ping" --health-interval 5s --health-timeout 2s --health-retries 5
steps:
- name: Checkout app (current)
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
path: app-current
fetch-depth: 0
- name: Detect app ID from appinfo/info.xml
run: |
APP_ID=$(grep -oP '(?<=<id>)[^<]+' app-current/appinfo/info.xml | head -1)
echo "APP_NAME=$APP_ID" >> $GITHUB_ENV
echo "Detected app ID: $APP_ID"
- name: Get supported server versions
id: versions
uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1
with:
filename: app-current/appinfo/info.xml
- name: Save current app for later
run: |
mkdir -p /tmp/app-backup
cp -r app-current /tmp/app-backup/
- name: Checkout server
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
submodules: true
repository: nextcloud/server
ref: ${{ fromJson(steps.versions.outputs.branches)[0] }}
- name: Checkout app at v0.14.0 (pre-rename baseline)
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
persist-credentials: false
ref: v0.14.0
path: apps/${{ env.APP_NAME }}
- name: Set up php 8.3
uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # v2.35.5
with:
php-version: '8.3'
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, mysql, pdo_mysql
coverage: none
ini-file: development
ini-values: disable_functions=
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set up dependencies (v0.14.0)
working-directory: apps/${{ env.APP_NAME }}
run: |
composer remove nextcloud/ocp --dev --no-scripts || true
composer i --no-scripts || composer i
- name: Set up Nextcloud and install app at v0.14.0
env:
DB_PORT: 4444
run: |
mkdir data
./occ maintenance:install --verbose --database=mysql --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
echo "::group::Installing app at v0.14.0"
./occ app:enable --force ${{ env.APP_NAME }}
echo "::endgroup::"
echo "::group::Database tables after v0.14.0 install"
mysql -h 127.0.0.1 -P $DB_PORT -u root -prootpassword nextcloud -e "SHOW TABLES LIKE 'oc_forum_%'"
echo "::endgroup::"
- name: Upgrade app to current version
run: |
echo "::group::Replacing app with current version"
rm -rf apps/${{ env.APP_NAME }}
cp -r /tmp/app-backup/app-current apps/${{ env.APP_NAME }}
echo "::endgroup::"
- name: Set up dependencies (current)
working-directory: apps/${{ env.APP_NAME }}
run: |
composer remove nextcloud/ocp --dev --no-scripts || true
composer i
- name: Run migrations to current version
env:
DB_PORT: 4444
run: |
echo "::group::Running upgrade migrations"
# Disable maintenance mode if it was left on
./occ maintenance:mode --off || true
# Disable and re-enable the app to trigger migrations
# This simulates what happens when a user upgrades the app
./occ app:disable ${{ env.APP_NAME }}
./occ app:enable ${{ env.APP_NAME }}
echo "::endgroup::"
echo "::group::Database tables after upgrade"
mysql -h 127.0.0.1 -P $DB_PORT -u root -prootpassword nextcloud -e "SHOW TABLES LIKE 'oc_forum_%'"
echo "::endgroup::"
echo "::group::Checking forum_users table exists"
mysql -h 127.0.0.1 -P $DB_PORT -u root -prootpassword nextcloud -e "SELECT COUNT(*) FROM oc_forum_users" || exit 1
echo "::endgroup::"
- name: Check PHPUnit integration script is defined
id: check_integration
continue-on-error: true
working-directory: apps/${{ env.APP_NAME }}
run: |
composer run --list | grep '^ test:integration ' | wc -l | grep 1
- name: Run Nextcloud
if: steps.check_integration.outcome == 'success'
run: php -S localhost:8080 &
- name: PHPUnit integration
if: steps.check_integration.outcome == 'success'
working-directory: apps/${{ env.APP_NAME }}
run: composer run test:integration
- name: Print logs
if: always()
run: |
cat data/nextcloud.log
summary:
permissions:
contents: none
runs-on: ubuntu-latest
needs: [changes, incremental-pgsql, incremental-mysql]
if: always()
name: incremental-migration-summary
steps:
- name: Summary status
run: if ${{ needs.changes.outputs.src != 'false' && (needs.incremental-pgsql.result != 'success' || needs.incremental-mysql.result != 'success') }}; then exit 1; fi

View File

@@ -1146,6 +1146,9 @@ class SeedHelper {
$logger = \OC::$server->get(\Psr\Log\LoggerInterface::class);
$timestamp = time();
// Recover connection state before starting (important for PostgreSQL)
self::recoverConnectionState($db, $logger);
try {
// Check if welcome thread already exists
$qb = $db->getQueryBuilder();
@@ -1197,6 +1200,42 @@ class SeedHelper {
}
});
// Check if slug column still exists BEFORE starting transaction
// (for backwards compatibility with old migrations)
// On PostgreSQL, a failed query inside a transaction aborts the entire transaction,
// so we must check column existence outside the transaction
$hasSlugColumn = true;
try {
$checkQb = $db->getQueryBuilder();
$checkQb->select('slug')->from('forum_posts')->setMaxResults(1);
$checkQb->executeQuery()->closeCursor();
} catch (\Exception $e) {
$hasSlugColumn = false;
// Recover connection state after the failed query (important for PostgreSQL)
self::recoverConnectionState($db, $logger);
}
// Prepare welcome post content
$welcomeContent = $l->t('Welcome to the Nextcloud Forums!') . "\n\n"
. $l->t('This is a community-driven forum built right into your Nextcloud instance. '
. 'Here you can discuss topics, share ideas and collaborate with other users.') . "\n\n"
. '[b]' . $l->t('Features:') . "[/b]\n"
. "[list]\n"
. '[*]' . $l->t('Create and reply to threads') . "\n"
. '[*]' . $l->t('Organize discussions by categories') . "\n"
. '[*]' . $l->t('Use BBCode for rich text formatting') . "\n"
. '[*]' . $l->t('Attach files from your Nextcloud storage') . "\n"
. '[*]' . $l->t('React to posts') . "\n"
. '[*]' . $l->t('Track read/unread threads') . "\n\n"
. "[/list]\n"
. '[b]' . $l->t('BBCode examples:') . "[/b]\n"
. "[list]\n"
. '[*][b]' . $l->t('Bold text') . '[/b] - ' . $l->t('Use %1$stext%2$s', ['[icode][b]', '[/b][/icode]']) . "\n"
. '[*][i]' . $l->t('Italic text') . '[/i] - ' . $l->t('Use %1$stext%2$s', ['[icode][i]', '[/i][/icode]']) . "\n"
. '[*][u]' . $l->t('Underlined text') . '[/u] - ' . $l->t('Use %1$stext%2$s', ['[icode][u]', '[/u][/icode]']) . "\n\n"
. "[/list]\n"
. $l->t('Feel free to start a new discussion or reply to existing threads. Happy posting!');
$db->beginTransaction();
// Create welcome thread
@@ -1219,38 +1258,6 @@ class SeedHelper {
->executeStatement();
$threadId = $qb->getLastInsertId();
// Create welcome post
$welcomeContent = $l->t('Welcome to the Nextcloud Forums!') . "\n\n"
. $l->t('This is a community-driven forum built right into your Nextcloud instance. '
. 'Here you can discuss topics, share ideas and collaborate with other users.') . "\n\n"
. '[b]' . $l->t('Features:') . "[/b]\n"
. "[list]\n"
. '[*]' . $l->t('Create and reply to threads') . "\n"
. '[*]' . $l->t('Organize discussions by categories') . "\n"
. '[*]' . $l->t('Use BBCode for rich text formatting') . "\n"
. '[*]' . $l->t('Attach files from your Nextcloud storage') . "\n"
. '[*]' . $l->t('React to posts') . "\n"
. '[*]' . $l->t('Track read/unread threads') . "\n\n"
. "[/list]\n"
. '[b]' . $l->t('BBCode examples:') . "[/b]\n"
. "[list]\n"
. '[*][b]' . $l->t('Bold text') . '[/b] - ' . $l->t('Use %1$stext%2$s', ['[icode][b]', '[/b][/icode]']) . "\n"
. '[*][i]' . $l->t('Italic text') . '[/i] - ' . $l->t('Use %1$stext%2$s', ['[icode][i]', '[/i][/icode]']) . "\n"
. '[*][u]' . $l->t('Underlined text') . '[/u] - ' . $l->t('Use %1$stext%2$s', ['[icode][u]', '[/u][/icode]']) . "\n\n"
. "[/list]\n"
. $l->t('Feel free to start a new discussion or reply to existing threads. Happy posting!');
// Check if slug column still exists (for backwards compatibility with old migrations)
// Use a query to check column existence since schema introspection APIs vary
$hasSlugColumn = true;
try {
$checkQb = $db->getQueryBuilder();
$checkQb->select('slug')->from('forum_posts')->setMaxResults(1);
$checkQb->executeQuery()->closeCursor();
} catch (\Exception $e) {
$hasSlugColumn = false;
}
// Build post values - slug is optional (removed in Version8)
$qb = $db->getQueryBuilder();
$postValues = [