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);