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 </dev/null)
+if [[ -z "$jobs" ]]; then
+ log_error "❌ No jobs found in below JSON:"
+ log_info "$NEEDS_JSON"
exit 1
fi
-jobs_informations=$(echo "$response" | jq '.jobs[:-1]')
-jobs_conclusions=$(echo "$jobs_informations" | jq -r '.[].conclusion')
+results=$(echo "$NEEDS_JSON" | jq -r '.[].result' 2>/dev/null)
-workflow_status="Success"
-if [[ " ${jobs_conclusions[*]} " == *"failure"* ]]; then
- workflow_status="Failure"
-elif [[ " ${jobs_conclusions[*]} " == *"cancelled"* ]]; then
- workflow_status="Cancelled"
-elif [[ " ${jobs_conclusions[*]} " == *"skipped"* ]]; then
- workflow_status="Skipped"
+workflow_status="✅ Success"
+if [[ "${results[*]}" == *"failure"* ]]; then
+ workflow_status="❌ Failure"
+elif [[ "${results[*]}" == *"cancelled"* ]]; then
+ workflow_status="⚠️ Cancelled"
+elif [[ "${results[*]}" == *"skipped"* ]]; then
+ workflow_status="⚠️ Skipped"
fi
log_info "Sending report to the element chat...."
+payload=$(cat <NC-Nightly-${BRANCH_NAME}
Status: ${workflow_status}"
+}
+EOF
+)
+
send_message_to_room_response=$(curl -s -XPOST "$ELEMENT_CHAT_URL/_matrix/client/r0/rooms/%21$ELEMENT_ROOM_ID/send/m.room.message?access_token=$NIGHTLY_CI_USER_TOKEN" \
- -d '
- {
- "msgtype": "m.text",
- "body": "",
- "format": "org.matrix.custom.html",
- "formatted_body": "NC-Nightly-'$BRANCH_NAME'
Status: '$workflow_status'"
- }
- '
+ -d "$payload"
)
if [[ "$send_message_to_room_response" != *"event_id"* ]]; then
@@ -59,4 +84,4 @@ if [[ "$send_message_to_room_response" != *"event_id"* ]]; then
exit 1
fi
-log_success "Notification of the nightly build has been sent to Element chat (OpenProject + Nextcloud)"
+log_success "Notification of the nightly build has been sent to Element chat (OpenProject + Nextcloud)"
\ No newline at end of file
diff --git a/.github/workflows/app-upgrade.yml b/.github/workflows/app-upgrade.yml
new file mode 100644
index 000000000..4c7109e34
--- /dev/null
+++ b/.github/workflows/app-upgrade.yml
@@ -0,0 +1,178 @@
+# SPDX-FileCopyrightText: 2026 Jankari Tech Pvt. Ltd.
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+name: App Upgrade
+
+on:
+ workflow_call:
+ inputs:
+ branch:
+ required: true
+ type: string
+ nextcloud_versions:
+ required: false
+ type: string
+ default: 'master'
+ php_versions:
+ required: false
+ type: string
+ default: '8.3'
+
+jobs:
+ create-matrix:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
+ with:
+ ref: ${{ inputs.branch }}
+
+ - name: Create matrix
+ id: create-matrix
+ env:
+ NEXTCLOUD_VERSIONS: ${{ inputs.nextcloud_versions }}
+ PHP_VERSIONS: ${{ inputs.php_versions }}
+ DEFAULT_PHP_VERSION: '8.3'
+ DEFAULT_DATABASE: 'mysql'
+ run: |
+ MATRIX=$(./.github/scripts/generate-matrix.sh)
+ echo "matrix={\"include\": [$MATRIX]}" >> $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
+ ref: ${{ inputs.branch }}
+
+ - 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/.github/workflows/nighlty-ci-master.yml b/.github/workflows/nighlty-ci-master.yml
index b2a691155..2d0ab0d56 100644
--- a/.github/workflows/nighlty-ci-master.yml
+++ b/.github/workflows/nighlty-ci-master.yml
@@ -15,3 +15,27 @@ jobs:
secrets: inherit
with:
branch: master
+
+ app-upgrade-test:
+ uses: ./.github/workflows/app-upgrade.yml
+ with:
+ branch: master
+ nextcloud_versions: '33'
+
+ notify-nightly-report:
+ needs: [builds, app-upgrade-test]
+ if: ${{ always() }}
+ runs-on: ubuntu-22.04
+ steps:
+ - name: checkout
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
+
+ - name: Notify Element (nightly builds)
+ env:
+ ELEMENT_CHAT_URL: ${{ secrets.ELEMENT_CHAT_URL }}
+ ELEMENT_ROOM_ID: ${{ secrets.ELEMENT_ROOM_ID }}
+ NIGHTLY_CI_USER_TOKEN: ${{ secrets.NIGHTLY_CI_USER_TOKEN }}
+ BRANCH_NAME: master
+ NEEDS_JSON: ${{ toJson(needs) }}
+ run: |
+ ./.github/scripts/notify-to-element.sh
\ No newline at end of file
diff --git a/.github/workflows/nighlty-ci-release-2-branch.yml b/.github/workflows/nighlty-ci-release-2-branch.yml
index 159007a07..f3abc9bd7 100644
--- a/.github/workflows/nighlty-ci-release-2-branch.yml
+++ b/.github/workflows/nighlty-ci-release-2-branch.yml
@@ -15,4 +15,28 @@ jobs:
secrets: inherit
with:
branch: release/2.11
- nextcloud_versions: "31 32"
\ No newline at end of file
+ nextcloud_versions: "31 32"
+
+ app-upgrade-test:
+ uses: ./.github/workflows/app-upgrade.yml
+ with:
+ branch: release/2.11
+ nextcloud_versions: "31 32"
+
+ notify-nightly-report:
+ needs: [builds, app-upgrade-test]
+ if: ${{ always() }}
+ runs-on: ubuntu-22.04
+ steps:
+ - name: checkout
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
+
+ - name: Notify Element (nightly builds)
+ env:
+ ELEMENT_CHAT_URL: ${{ secrets.ELEMENT_CHAT_URL }}
+ ELEMENT_ROOM_ID: ${{ secrets.ELEMENT_ROOM_ID }}
+ NIGHTLY_CI_USER_TOKEN: ${{ secrets.NIGHTLY_CI_USER_TOKEN }}
+ BRANCH_NAME: release/2.11
+ NEEDS_JSON: ${{ toJson(needs) }}
+ run: |
+ ./.github/scripts/notify-to-element.sh
\ No newline at end of file
diff --git a/.github/workflows/nighty-ci-release-3-branch.yml b/.github/workflows/nighty-ci-release-3-branch.yml
index 568a1555a..a8e9b99d9 100644
--- a/.github/workflows/nighty-ci-release-3-branch.yml
+++ b/.github/workflows/nighty-ci-release-3-branch.yml
@@ -15,4 +15,28 @@ jobs:
secrets: inherit
with:
branch: release/3.0
- nextcloud_versions: "33"
\ No newline at end of file
+ nextcloud_versions: "33 master"
+
+ app-upgrade-test:
+ uses: ./.github/workflows/app-upgrade.yml
+ with:
+ branch: release/3.0
+ nextcloud_versions: "33"
+
+ notify-nightly-report:
+ needs: [builds, app-upgrade-test]
+ if: ${{ always() }}
+ runs-on: ubuntu-22.04
+ steps:
+ - name: checkout
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
+
+ - name: Notify Element (nightly builds)
+ env:
+ ELEMENT_CHAT_URL: ${{ secrets.ELEMENT_CHAT_URL }}
+ ELEMENT_ROOM_ID: ${{ secrets.ELEMENT_ROOM_ID }}
+ NIGHTLY_CI_USER_TOKEN: ${{ secrets.NIGHTLY_CI_USER_TOKEN }}
+ BRANCH_NAME: release/3.0
+ NEEDS_JSON: ${{ toJson(needs) }}
+ run: |
+ ./.github/scripts/notify-to-element.sh
\ No newline at end of file
diff --git a/.github/workflows/shared_workflow.yml b/.github/workflows/shared_workflow.yml
index 9b3d8d7fa..debe6f3e1 100644
--- a/.github/workflows/shared_workflow.yml
+++ b/.github/workflows/shared_workflow.yml
@@ -326,26 +326,4 @@ jobs:
- name: API Tests
env:
NEXTCLOUD_BASE_URL: http://localhost
- run: make api-test
-
- notify-nightly-report:
- needs:
- - js-lint-unit
- - api-phpunit-tests
- if: ${{ always() && github.event_name == 'schedule' }}
- runs-on: ubuntu-22.04
- steps:
- - name: checkout
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
-
- - name: Notify nightly report to element
- env:
- ELEMENT_CHAT_URL: ${{ secrets.ELEMENT_CHAT_URL }}
- ELEMENT_ROOM_ID: ${{ secrets.ELEMENT_ROOM_ID }}
- NIGHTLY_CI_USER_TOKEN: ${{ secrets.NIGHTLY_CI_USER_TOKEN }}
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- REPO_OWNER: nextcloud
- REPO_NAME: integration_openproject
- RUN_ID: ${{ github.run_id }}
- BRANCH_NAME: ${{ inputs.branch }}
- run: ./.github/scripts/notify-to-element.sh
+ 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 1ddd42031..6a57747a9 100644
--- a/tests/acceptance/features/bootstrap/FeatureContext.php
+++ b/tests/acceptance/features/bootstrap/FeatureContext.php
@@ -272,6 +272,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);