diff --git a/.github/scripts/build-upgradable-app.sh b/.github/scripts/build-upgradable-app.sh
new file mode 100644
index 000000000..4735b7f17
--- /dev/null
+++ b/.github/scripts/build-upgradable-app.sh
@@ -0,0 +1,207 @@
+#!/bin/bash
+# SPDX-FileCopyrightText: 2026 Jankari Tech Pvt. Ltd.
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+# This script is used to build the upgradable integration_openproject app. It performs the following steps:
+# 1. Copy the build files to a separate folder named publish, excluding unnecessary files and directories.
+# 2. Get the current version of the app and update it to a new version by incrementing the major version number.
+# 3. Sign the app files using self-signed certificate.
+# 4. Archive the app into a .tar.gz file.
+# 5. Sign the archive.
+# NOTE: Before running this script, ensure that the Nextcloud instance is running and integration_openproject app is built.
+
+# Required environment variables:
+# 1. NEXTCLOUD_PATH (Absolute path to Nextcloud where occ command is available, e.g. /var/www/html)
+# 2. INTEGRATION_OPENPROJECT_DIR (Absolute path to the directory containing the integration_openproject repository, e.g. /var/www/html/build-app-shared)
+
+set -e -o pipefail
+
+# helper functions
+log_error() {
+ echo -e "\e[31m$1\e[0m"
+}
+
+log_info() {
+ echo -e "\e[37m$1\e[0m"
+}
+
+log_success() {
+ echo -e "\e[32m$1\e[0m"
+}
+
+if [[ -z "$NEXTCLOUD_PATH" ]] || [[ -z "$INTEGRATION_OPENPROJECT_DIR" ]]; then
+ log_error "Missing required environment variables: NEXTCLOUD_PATH, INTEGRATION_OPENPROJECT_DIR"
+ exit 1
+fi
+
+APP_ID=integration_openproject
+cd "$INTEGRATION_OPENPROJECT_DIR"
+
+if [[ ! -d "$INTEGRATION_OPENPROJECT_DIR/$APP_ID" ]]; then
+ log_error "Folder does not exist: $INTEGRATION_OPENPROJECT_DIR/$APP_ID"
+ exit 1
+fi
+
+mkdir -p publish
+
+# copy app files to a separate folder
+log_info "Copying necessary app files to publish directory..."
+rsync -a \
+--exclude=server \
+--exclude=dev \
+--exclude=.git \
+--exclude=appinfo/signature.json \
+--exclude='*.swp' \
+--exclude=build \
+--exclude=.gitignore \
+--exclude=.travis.yml \
+--exclude=.scrutinizer.yml \
+--exclude=CONTRIBUTING.md \
+--exclude=composer.phar \
+--exclude=js/node_modules \
+--exclude=node_modules \
+--exclude=src \
+--exclude=translationfiles \
+--exclude='webpack.*' \
+--exclude=stylelint.config.js \
+--exclude=.eslintrc.js \
+--exclude=.github \
+--exclude=.gitlab-ci.yml \
+--exclude=crowdin.yml \
+--exclude=tools \
+--exclude=.tx \
+--exclude=.l10nignore \
+--exclude=l10n/.tx \
+--exclude=l10n/l10n.pl \
+--exclude=l10n/templates \
+--exclude='l10n/*.sh' \
+--exclude='l10n/[a-z][a-z]' \
+--exclude='l10n/[a-z][a-z]_[A-Z][A-Z]' \
+--exclude=l10n/no-php \
+--exclude=makefile \
+--exclude=screenshots \
+--exclude='phpunit*xml' \
+--exclude=tests \
+--exclude=ci \
+--exclude=vendor/bin \
+$APP_ID publish/
+
+cd publish
+
+# get current version of integration_openproject and update to new version
+current_version=$(php ${NEXTCLOUD_PATH}/occ app:list --output=json | jq -r ".enabled.$APP_ID") || { log_error "Failed to get current version of $APP_ID app."; exit 1; }
+IFS=. read -r a b c <<< "$current_version"
+NEXT_APP_VERSION="$((a+1)).$b.$c"
+
+# Save the new tag to a file for later use in the workflow
+echo "$NEXT_APP_VERSION" > "${APP_ID}_new_version.txt"
+
+# update version in info.xml
+sed -i "s|.*|$NEXT_APP_VERSION|" "integration_openproject/appinfo/info.xml"
+
+#####################
+# Signing the app #
+#####################
+# https://nextcloudappstore.readthedocs.io/en/latest/developer.html#obtaining-a-certificate
+# Check if openssl exists, otherwise install it
+if ! command -v openssl >/dev/null 2>&1; then
+ echo "OpenSSL not found. Installing..."
+ apt update && apt install -y openssl || {
+ echo "Failed to install OpenSSL."
+ exit 1
+ }
+fi
+log_info "Generating app.key and app.crt..."
+openssl req -x509 -newkey rsa:4096 -sha256 -nodes \
+ -keyout app.key \
+ -out app.crt \
+ -days 3650 \
+ -subj "/CN=$APP_ID" \
+ -addext "basicConstraints=CA:FALSE" \
+ -addext "keyUsage=digitalSignature" \
+ -addext "extendedKeyUsage=codeSigning"
+
+if [[ ! -s app.key || ! -s app.crt ]]; then
+ log_error "Failed to generate app signing certificate and key: app.key or app.crt not found."
+ exit 1
+fi
+
+log_info "Adding the generated certificate to Nextcloud's root.crt..."
+nextcloud_root_crt="${NEXTCLOUD_PATH}/resources/codesigning/root.crt"
+if [[ -f ${nextcloud_root_crt} ]]; then
+ echo "" >> ${nextcloud_root_crt}
+ cat app.crt >> ${nextcloud_root_crt}
+else
+ log_error "Nextcloud's root.crt not found at ${nextcloud_root_crt}."
+ exit 1
+fi
+
+# fix permissions for signing
+chown www-data app.key
+chown www-data app.crt
+chown -R www-data $APP_ID
+
+# Sign the app
+# need full path for signing
+log_info "Signing the app files..."
+php ${NEXTCLOUD_PATH}/occ integrity:sign-app \
+ --privateKey=${INTEGRATION_OPENPROJECT_DIR}/publish/app.key \
+ --certificate=${INTEGRATION_OPENPROJECT_DIR}/publish/app.crt \
+ --path=${INTEGRATION_OPENPROJECT_DIR}/publish/$APP_ID || { log_error "Failed to sign app."; exit 1; }
+
+# Archive the app
+tar -czf $APP_ID-$NEXT_APP_VERSION.tar.gz $APP_ID
+if [[ ! -f $APP_ID-$NEXT_APP_VERSION.tar.gz ]]; then
+ log_error "Failed to archive the app. Archive file $APP_ID-$NEXT_APP_VERSION.tar.gz not found."
+ exit 1
+fi
+log_success "App archived into $APP_ID-$NEXT_APP_VERSION.tar.gz."
+
+#####################
+# Sign the archive #
+#####################
+log_info "Signing the app archive..."
+openssl dgst -sha512 -sign app.key $APP_ID-$NEXT_APP_VERSION.tar.gz \
+ | openssl base64 \
+ | tee ${INTEGRATION_OPENPROJECT_DIR}/publish/sign.txt
+
+if [[ ! -s ${INTEGRATION_OPENPROJECT_DIR}/publish/sign.txt ]]; then
+ log_error "Failed to sign the app archive. Signature file sign.txt is empty or not found."
+ exit 1
+else
+ log_success "App archive signed successfully."
+fi
+
+log_success "Upgradable app built successfully."
+
+# prepare apps.json file
+if [[ ! -f ${INTEGRATION_OPENPROJECT_DIR}/publish/${APP_ID}/appinfo/signature.json ]]; then
+ echo "Signature file not found at ${INTEGRATION_OPENPROJECT_DIR}/publish/${APP_ID}/appinfo/signature.json."
+ exit 1
+fi
+certificate=$(jq '.certificate' "${INTEGRATION_OPENPROJECT_DIR}/publish/${APP_ID}/appinfo/signature.json")
+signature=$(tr -d '\n' < "${INTEGRATION_OPENPROJECT_DIR}/publish/sign.txt")
+
+# Create apps.json with the required structure
+cat > apps.json <> $GITHUB_OUTPUT
+ outputs:
+ matrix: ${{ steps.create-matrix.outputs.matrix }}
+
+ upgrade-test:
+ name: Upgrade Testing
+ needs: create-matrix
+ runs-on: ubuntu-latest
+ strategy:
+ matrix: ${{ fromJson(needs.create-matrix.outputs.matrix) }}
+
+ services:
+ nextcloud:
+ image: ghcr.io/juliusknorr/nextcloud-dev-php${{ format('{0}{1}', matrix.phpVersionMajor,matrix.phpVersionMinor) }}:master
+ env:
+ SQL: ${{ matrix.database }}
+ SERVER_BRANCH: ${{ matrix.nextcloudVersion }}
+ NEXTCLOUD_AUTOINSTALL: "Yes"
+ NEXTCLOUD_AUTOINSTALL_APPS: oidc groupfolders user_oidc integration_openproject
+ NEXTCLOUD_AUTOINSTALL_APPS_WAIT_TIME: 60
+ WITH_REDIS: "YES"
+ ports:
+ - 80:80
+ options: --name=nextcloud
+ volumes:
+ - ${{ github.workspace }}/extra-apps:/var/www/html/apps-shared
+ - ${{ github.workspace }}/build-app:/var/www/html/build-app-shared
+
+ database-mysql:
+ image: ghcr.io/nextcloud/continuous-integration-mariadb-10.5:latest
+ env:
+ MYSQL_ROOT_PASSWORD: 'nextcloud'
+ MYSQL_PASSWORD: 'nextcloud'
+ MYSQL_USER: 'nextcloud'
+ MYSQL_DATABASE: 'nextcloud'
+
+ redis:
+ image: ghcr.io/nextcloud/continuous-integration-redis:latest
+ options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
+ with:
+ path: integration_openproject
+
+ - name: Setup PHP ${{ format('{0}.{1}', matrix.phpVersionMajor,matrix.phpVersionMinor) }}
+ uses: shivammathur/setup-php@a4e22b60bbb9c1021113f2860347b0759f66fe5d
+ with:
+ php-version: ${{ format('{0}.{1}', matrix.phpVersionMajor,matrix.phpVersionMinor) }}
+ tools: composer, phpunit
+ extensions: intl, gd, sqlite3
+
+ - name: Wait for Nextcloud server to be ready
+ run: |
+ if ! timeout 5m bash -c '
+ until curl -s -f http://localhost/status.php | grep '"'"'"installed":true'"'"'; do
+ echo "[INFO] Waiting for server to be ready..."
+ sleep 10
+ done
+ '; then
+ echo "[ERROR] Server not ready within 5 minutes."
+ exit 1
+ fi
+
+ # activity app cannot be installed using occ command
+ - name: Setup activity app
+ run: |
+ # fix permissions for folder extra-apps
+ sudo chown $USER:$USER extra-apps
+ cd extra-apps
+ git clone https://github.com/nextcloud/activity.git --depth 1 --branch ${{ matrix.nextcloudVersion }}
+ # Enable activity apps
+ if [ "${{matrix.nextcloudVersion}}" == "master" ]; then
+ # enable app even if it is not compatible with the master branch
+ docker exec nextcloud /bin/bash -c 'occ a:e -f activity'
+ else
+ docker exec nextcloud /bin/bash -c 'occ a:e activity'
+ fi
+
+ - name: Build upgradable integration_openproject app
+ run: |
+ make -C integration_openproject
+ # fix permissions for build-app folder
+ sudo chown -R $USER:$USER build-app
+ cp -R integration_openproject build-app
+ # run the build script inside the nextcloud container
+ docker exec nextcloud /bin/bash -c "\
+ cd build-app-shared && \
+ INTEGRATION_OPENPROJECT_DIR=/var/www/html/build-app-shared \
+ NEXTCLOUD_PATH=/var/www/html \
+ source integration_openproject/.github/scripts/build-upgradable-app.sh"
+ cd build-app/publish
+ echo "NEXT_APP_VERSION=$(cat integration_openproject_new_version.txt)" >> $GITHUB_ENV
+
+ - name: Update integration_openproject app
+ run: |
+ # Start local appstore server
+ docker exec nextcloud /bin/bash -c "cd build-app-shared && php -S localhost:8080 -t publish &"
+
+ # Wait for local appstore server to be ready
+ docker exec nextcloud /bin/bash -c '
+ for i in $(seq 1 5); do
+ if curl -sS http://localhost:8080 > /dev/null; then
+ exit 0
+ fi
+ sleep 2
+ done
+ echo "Failed to run local appstore at localhost:8080"
+ exit 1
+ '
+
+ # Clear appstore cache (force re-fetch)
+ docker exec nextcloud /bin/bash -c "echo '' > /var/www/html/data/appdata_*/appstore/apps.json"
+
+ # Disable share rate limit protection
+ docker exec nextcloud /bin/bash -c "php occ config:system:set ratelimit.protection.enabled --value false --type bool"
+
+ # Configure local appstore server
+ docker exec nextcloud /bin/bash -c "php occ config:system:set appstoreurl --value 'http://localhost:8080'"
+ docker exec nextcloud /bin/bash -c "php occ config:system:set allow_local_remote_servers --value true"
+
+ # Update app
+ docker exec nextcloud /bin/bash -c "php occ app:update --allow-unstable integration_openproject"
+
+ # Verify update
+ if docker exec nextcloud /bin/bash -c "php occ app:list | grep -q 'integration_openproject.*$NEXT_APP_VERSION'"; then
+ echo "App updated successfully to version $NEXT_APP_VERSION"
+ else
+ echo "App not updated to version $NEXT_APP_VERSION"
+ exit 1
+ fi
+
+ - name: API Tests
+ working-directory: integration_openproject
+ env:
+ NEXTCLOUD_BASE_URL: http://localhost
+ run: |
+ make api-test
\ No newline at end of file
diff --git a/tests/acceptance/features/bootstrap/FeatureContext.php b/tests/acceptance/features/bootstrap/FeatureContext.php
index 1c7f59258..e4c85e7a8 100644
--- a/tests/acceptance/features/bootstrap/FeatureContext.php
+++ b/tests/acceptance/features/bootstrap/FeatureContext.php
@@ -270,6 +270,11 @@ private function deleteUserDataFromDocker(string $user): void {
echo "'docker' command not found. Skipping user data deletion.\n";
return;
}
+ // Skip if Nextcloud Docker container does not exist
+ exec("docker ps --format \"{{.Names}}\"", $containers);
+ if (!in_array('nextcloud', $containers)) {
+ return;
+ }
$firstChar = substr($user, 0, 1);
$restChars = substr($user, 1);