From c18c4a2a26054fdcbe8e5eee3cb8d5ebc51c68b5 Mon Sep 17 00:00:00 2001
From: Joas Schilling <coding@schilljs.com>
Date: Mon, 16 Jan 2023 17:10:56 +0100
Subject: [PATCH] chore(CI): Update master php testing versions and workflow
 templates

Signed-off-by: Joas Schilling <coding@schilljs.com>
---
 .github/workflows/appstore-build-publish.yml  | 36 +++++-----
 .github/workflows/fixup.yml                   | 12 +++-
 .../workflows/lint-eslint-when-unrelated.yml  | 39 +++++++++++
 .github/workflows/lint-eslint.yml             | 30 ++++++--
 .github/workflows/lint-info-xml.yml           | 12 +++-
 .github/workflows/lint-php-cs.yml             | 10 +--
 .github/workflows/lint-php.yml                | 10 +--
 .github/workflows/lint-stylelint.yml          | 12 ++--
 .github/workflows/node-when-unrelated.yml     | 43 ++++++++++++
 .github/workflows/node.yml                    | 27 ++++++--
 .github/workflows/phpunit-mysql.yml           | 60 +++++++++-------
 .github/workflows/phpunit-pgsql.yml           | 53 ++++++++-------
 .github/workflows/phpunit-sqlite.yml          | 53 ++++++++-------
 .../phpunit-summary-when-unrelated.yml        | 68 +++++++++++++++++++
 .github/workflows/psalm-matrix.yml            | 61 +++++++++++++++++
 .github/workflows/static-analysis.yml         | 26 -------
 composer.json                                 |  3 +-
 17 files changed, 406 insertions(+), 149 deletions(-)
 create mode 100644 .github/workflows/lint-eslint-when-unrelated.yml
 create mode 100644 .github/workflows/node-when-unrelated.yml
 create mode 100644 .github/workflows/phpunit-summary-when-unrelated.yml
 create mode 100644 .github/workflows/psalm-matrix.yml
 delete mode 100644 .github/workflows/static-analysis.yml

diff --git a/.github/workflows/appstore-build-publish.yml b/.github/workflows/appstore-build-publish.yml
index fc04383c..90453eb0 100644
--- a/.github/workflows/appstore-build-publish.yml
+++ b/.github/workflows/appstore-build-publish.yml
@@ -10,7 +10,7 @@ on:
     types: [published]
 
 env:
-  PHP_VERSION: 7.4
+  PHP_VERSION: 8.1
 
 jobs:
   build_and_publish:
@@ -21,42 +21,42 @@ jobs:
 
     steps:
       - name: Check actor permission
-        uses: skjnldsv/check-actor-permission@v2
+        uses: skjnldsv/check-actor-permission@e591dbfe838300c007028e1219ca82cc26e8d7c5 # v2.1
         with:
           require: write
 
       - name: Set app env
         run: |
-          # Split and keep last 
+          # Split and keep last
           echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
           echo "APP_VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV
 
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
         with:
           path: ${{ env.APP_NAME }}
 
       - name: Get appinfo data
         id: appinfo
-        uses: skjnldsv/xpath-action@master
+        uses: skjnldsv/xpath-action@7e6a7c379d0e9abc8acaef43df403ab4fc4f770c # master
         with:
           filename: ${{ env.APP_NAME }}/appinfo/info.xml
           expression: "//info//dependencies//nextcloud/@min-version"
 
       - name: Read package.json node and npm engines version
-        uses: skjnldsv/read-package-engines-version-actions@v1.2
+        uses: skjnldsv/read-package-engines-version-actions@1bdcee71fa343c46b18dc6aceffb4cd1e35209c6 # v1.2
         id: versions
         # Continue if no package.json
         continue-on-error: true
         with:
           path: ${{ env.APP_NAME }}
-          fallbackNode: "^12"
-          fallbackNpm: "^6"
+          fallbackNode: "^16"
+          fallbackNpm: "^7"
 
       - name: Set up node ${{ steps.versions.outputs.nodeVersion }}
         # Skip if no package.json
         if: ${{ steps.versions.outputs.nodeVersion }}
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # v3
         with:
           node-version: ${{ steps.versions.outputs.nodeVersion }}
 
@@ -66,14 +66,16 @@ jobs:
         run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"
 
       - name: Set up php ${{ env.PHP_VERSION }}
-        uses: shivammathur/setup-php@v2
+        uses: shivammathur/setup-php@1a18b2267f80291a81ca1d33e7c851fe09e7dfc4 # v2
         with:
           php-version: ${{ env.PHP_VERSION }}
           coverage: none
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Check composer.json
         id: check_composer
-        uses: andstor/file-existence-action@v1
+        uses: andstor/file-existence-action@20b4d2e596410855db8f9ca21e96fbe18e12930b # v2
         with:
           files: "${{ env.APP_NAME }}/composer.json"
 
@@ -93,15 +95,15 @@ jobs:
 
       - name: Check Krankerl config
         id: krankerl
-        uses: andstor/file-existence-action@v1
+        uses: andstor/file-existence-action@20b4d2e596410855db8f9ca21e96fbe18e12930b # v2
         with:
           files: ${{ env.APP_NAME }}/krankerl.toml
 
       - name: Install Krankerl
         if: steps.krankerl.outputs.files_exists == 'true'
         run: |
-          wget https://github.com/ChristophWurst/krankerl/releases/download/v0.13.0/krankerl_0.13.0_amd64.deb
-          sudo dpkg -i krankerl_0.13.0_amd64.deb
+          wget https://github.com/ChristophWurst/krankerl/releases/download/v0.14.0/krankerl_0.14.0_amd64.deb
+          sudo dpkg -i krankerl_0.14.0_amd64.deb
 
       - name: Package ${{ env.APP_NAME }} ${{ env.APP_VERSION }} with krankerl
         if: steps.krankerl.outputs.files_exists == 'true'
@@ -124,7 +126,7 @@ jobs:
           unzip latest-$NCVERSION.zip
 
       - name: Checkout server master fallback
-        uses: actions/checkout@v3
+        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
         if: ${{ steps.server-checkout.outcome != 'success' }}
         with:
           repository: nextcloud/server
@@ -146,7 +148,7 @@ jobs:
           tar -zcvf ${{ env.APP_NAME }}.tar.gz ${{ env.APP_NAME }}
 
       - name: Attach tarball to github release
-        uses: svenstaro/upload-release-action@v2
+        uses: svenstaro/upload-release-action@133984371c30d34e38222a64855679a414cb7575 # v2
         id: attach_to_release
         with:
           repo_token: ${{ secrets.GITHUB_TOKEN }}
@@ -156,7 +158,7 @@ jobs:
           overwrite: true
 
       - name: Upload app to Nextcloud appstore
-        uses: nextcloud-releases/nextcloud-appstore-push-action@v1
+        uses: nextcloud-releases/nextcloud-appstore-push-action@a011fe619bcf6e77ddebc96f9908e1af4071b9c1 # v1
         with:
           app_name: ${{ env.APP_NAME }}
           appstore_token: ${{ secrets.APPSTORE_TOKEN }}
diff --git a/.github/workflows/fixup.yml b/.github/workflows/fixup.yml
index e2da6329..b9e39207 100644
--- a/.github/workflows/fixup.yml
+++ b/.github/workflows/fixup.yml
@@ -5,13 +5,21 @@
 
 name: Pull request checks
 
-on: pull_request
+on:
+  pull_request:
+    types: [opened, ready_for_review, reopened]
 
 permissions:
   contents: read
 
+concurrency:
+  group: fixup-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+
 jobs:
   commit-message-check:
+    if: github.event.pull_request.draft == false
+
     permissions:
       pull-requests: write
     name: Block fixup and squash commits
@@ -20,6 +28,6 @@ jobs:
 
     steps:
       - name: Run check
-        uses: xt0rted/block-autosquash-commits-action@v2
+        uses: xt0rted/block-autosquash-commits-action@79880c36b4811fe549cfffe20233df88876024e7 # v2
         with:
           repo-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/lint-eslint-when-unrelated.yml b/.github/workflows/lint-eslint-when-unrelated.yml
new file mode 100644
index 00000000..63710eb6
--- /dev/null
+++ b/.github/workflows/lint-eslint-when-unrelated.yml
@@ -0,0 +1,39 @@
+# This workflow is provided via the organization template repository
+#
+# https://github.com/nextcloud/.github
+# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
+#
+# Use lint-eslint together with lint-eslint-when-unrelated to make eslint a required check for GitHub actions
+# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
+
+name: Lint
+
+on:
+  pull_request:
+    paths-ignore:
+      - '.github/workflows/**'
+      - 'src/**'
+      - 'appinfo/info.xml'
+      - 'package.json'
+      - 'package-lock.json'
+      - 'tsconfig.json'
+      - '.eslintrc.*'
+      - '.eslintignore'
+      - '**.js'
+      - '**.ts'
+      - '**.vue'
+
+permissions:
+  contents: read
+
+jobs:
+  lint:
+    permissions:
+      contents: none
+
+    runs-on: ubuntu-latest
+
+    name: eslint
+
+    steps:
+      - run: 'echo "No eslint required"'
diff --git a/.github/workflows/lint-eslint.yml b/.github/workflows/lint-eslint.yml
index c08763ea..628e8fef 100644
--- a/.github/workflows/lint-eslint.yml
+++ b/.github/workflows/lint-eslint.yml
@@ -2,15 +2,31 @@
 #
 # https://github.com/nextcloud/.github
 # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
+#
+# Use lint-eslint together with lint-eslint-when-unrelated to make eslint a required check for GitHub actions
+# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
 
 name: Lint
 
-on: pull_request
+on:
+  pull_request:
+    paths:
+      - '.github/workflows/**'
+      - 'src/**'
+      - 'appinfo/info.xml'
+      - 'package.json'
+      - 'package-lock.json'
+      - 'tsconfig.json'
+      - '.eslintrc.*'
+      - '.eslintignore'
+      - '**.js'
+      - '**.ts'
+      - '**.vue'
 
 permissions:
   contents: read
 
-concurrency: 
+concurrency:
   group: lint-eslint-${{ github.head_ref || github.run_id }}
   cancel-in-progress: true
 
@@ -22,17 +38,17 @@ jobs:
 
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
 
       - name: Read package.json node and npm engines version
-        uses: skjnldsv/read-package-engines-version-actions@v1.2
+        uses: skjnldsv/read-package-engines-version-actions@1bdcee71fa343c46b18dc6aceffb4cd1e35209c6 # v1.2
         id: versions
         with:
-          fallbackNode: '^12'
-          fallbackNpm: '^6'
+          fallbackNode: '^16'
+          fallbackNpm: '^7'
 
       - name: Set up node ${{ steps.versions.outputs.nodeVersion }}
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # v3
         with:
           node-version: ${{ steps.versions.outputs.nodeVersion }}
 
diff --git a/.github/workflows/lint-info-xml.yml b/.github/workflows/lint-info-xml.yml
index cea2a2bc..8f024cfc 100644
--- a/.github/workflows/lint-info-xml.yml
+++ b/.github/workflows/lint-info-xml.yml
@@ -9,9 +9,17 @@ on:
   pull_request:
   push:
     branches:
+      - main
       - master
       - stable*
 
+permissions:
+  contents: read
+
+concurrency:
+  group: lint-info-xml-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+
 jobs:
   xml-linters:
     runs-on: ubuntu-latest
@@ -19,13 +27,13 @@ jobs:
     name: info.xml lint
     steps:
       - name: Checkout
-        uses: actions/checkout@master
+        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
 
       - name: Download schema
         run: wget https://raw.githubusercontent.com/nextcloud/appstore/master/nextcloudappstore/api/v1/release/info.xsd
 
       - name: Lint info.xml
-        uses: ChristophWurst/xmllint-action@v1
+        uses: ChristophWurst/xmllint-action@d18a551aab4728e4af449617638600634d7a48cb # v1
         with:
           xml-file: ./appinfo/info.xml
           xml-schema-file: ./info.xsd
diff --git a/.github/workflows/lint-php-cs.yml b/.github/workflows/lint-php-cs.yml
index 28141020..df490fb2 100644
--- a/.github/workflows/lint-php-cs.yml
+++ b/.github/workflows/lint-php-cs.yml
@@ -10,7 +10,7 @@ on: pull_request
 permissions:
   contents: read
 
-concurrency: 
+concurrency:
   group: lint-php-cs-${{ github.head_ref || github.run_id }}
   cancel-in-progress: true
 
@@ -22,13 +22,15 @@ jobs:
 
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
 
       - name: Set up php
-        uses: shivammathur/setup-php@v2
+        uses: shivammathur/setup-php@1a18b2267f80291a81ca1d33e7c851fe09e7dfc4 # v2
         with:
-          php-version: "7.4"
+          php-version: 8.1
           coverage: none
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Install dependencies
         run: composer i
diff --git a/.github/workflows/lint-php.yml b/.github/workflows/lint-php.yml
index 62476c90..54580af5 100644
--- a/.github/workflows/lint-php.yml
+++ b/.github/workflows/lint-php.yml
@@ -16,7 +16,7 @@ on:
 permissions:
   contents: read
 
-concurrency: 
+concurrency:
   group: lint-php-${{ github.head_ref || github.run_id }}
   cancel-in-progress: true
 
@@ -25,19 +25,21 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        php-versions: ["7.4", "8.0", "8.1"]
+        php-versions: [ "7.4", "8.0", "8.1", "8.2" ]
 
     name: php-lint
 
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
 
       - name: Set up php ${{ matrix.php-versions }}
-        uses: shivammathur/setup-php@v2
+        uses: shivammathur/setup-php@1a18b2267f80291a81ca1d33e7c851fe09e7dfc4 # v2
         with:
           php-version: ${{ matrix.php-versions }}
           coverage: none
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Lint
         run: composer run lint
diff --git a/.github/workflows/lint-stylelint.yml b/.github/workflows/lint-stylelint.yml
index 17b7aebb..6cdf20cc 100644
--- a/.github/workflows/lint-stylelint.yml
+++ b/.github/workflows/lint-stylelint.yml
@@ -10,7 +10,7 @@ on: pull_request
 permissions:
   contents: read
 
-concurrency: 
+concurrency:
   group: lint-stylelint-${{ github.head_ref || github.run_id }}
   cancel-in-progress: true
 
@@ -22,17 +22,17 @@ jobs:
 
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
 
       - name: Read package.json node and npm engines version
-        uses: skjnldsv/read-package-engines-version-actions@v1.2
+        uses: skjnldsv/read-package-engines-version-actions@1bdcee71fa343c46b18dc6aceffb4cd1e35209c6 # v1.2
         id: versions
         with:
-          fallbackNode: '^12'
-          fallbackNpm: '^6'
+          fallbackNode: '^16'
+          fallbackNpm: '^7'
 
       - name: Set up node ${{ steps.versions.outputs.nodeVersion }}
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # v3
         with:
           node-version: ${{ steps.versions.outputs.nodeVersion }}
 
diff --git a/.github/workflows/node-when-unrelated.yml b/.github/workflows/node-when-unrelated.yml
new file mode 100644
index 00000000..db32b0db
--- /dev/null
+++ b/.github/workflows/node-when-unrelated.yml
@@ -0,0 +1,43 @@
+# This workflow is provided via the organization template repository
+#
+# https://github.com/nextcloud/.github
+# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
+#
+# Use node together with node-when-unrelated to make eslint a required check for GitHub actions
+# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
+
+name: Node
+
+on:
+  pull_request:
+    paths-ignore:
+      - '.github/workflows/**'
+      - 'src/**'
+      - 'appinfo/info.xml'
+      - 'package.json'
+      - 'package-lock.json'
+      - 'tsconfig.json'
+      - '**.js'
+      - '**.ts'
+      - '**.vue'
+  push:
+    branches:
+      - main
+      - master
+      - stable*
+
+concurrency:
+  group: node-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+
+jobs:
+  build:
+    permissions:
+      contents: none
+
+    runs-on: ubuntu-latest
+
+    name: node
+    steps:
+      - name: Skip
+        run: 'echo "No JS/TS files changed, skipped Node"'
diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml
index 9150d1f1..e3310644 100644
--- a/.github/workflows/node.yml
+++ b/.github/workflows/node.yml
@@ -7,6 +7,16 @@ name: Node
 
 on:
   pull_request:
+    paths:
+      - '.github/workflows/**'
+      - 'src/**'
+      - 'appinfo/info.xml'
+      - 'package.json'
+      - 'package-lock.json'
+      - 'tsconfig.json'
+      - '**.js'
+      - '**.ts'
+      - '**.vue'
   push:
     branches:
       - main
@@ -16,6 +26,10 @@ on:
 permissions:
   contents: read
 
+concurrency:
+  group: node-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+
 jobs:
   build:
     runs-on: ubuntu-latest
@@ -23,17 +37,17 @@ jobs:
     name: node
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
 
       - name: Read package.json node and npm engines version
-        uses: skjnldsv/read-package-engines-version-actions@v1.2
+        uses: skjnldsv/read-package-engines-version-actions@1bdcee71fa343c46b18dc6aceffb4cd1e35209c6 # v1.2
         id: versions
         with:
-          fallbackNode: '^12'
-          fallbackNpm: '^6'
+          fallbackNode: '^16'
+          fallbackNpm: '^7'
 
       - name: Set up node ${{ steps.versions.outputs.nodeVersion }}
-        uses: actions/setup-node@v3
+        uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # v3
         with:
           node-version: ${{ steps.versions.outputs.nodeVersion }}
 
@@ -47,10 +61,11 @@ jobs:
 
       - name: Check webpack build changes
         run: |
-          bash -c "[[ ! \"`git status --porcelain `\" ]] || exit 1"
+          bash -c "[[ ! \"`git status --porcelain `\" ]] || (echo 'Please recompile and commit the assets, see the section \"Show changes on failure\" for details' && exit 1)"
 
       - name: Show changes on failure
         if: failure()
         run: |
           git status
           git --no-pager diff
+          exit 1 # make it red to grab attention
diff --git a/.github/workflows/phpunit-mysql.yml b/.github/workflows/phpunit-mysql.yml
index 40d2fff5..762865ab 100644
--- a/.github/workflows/phpunit-mysql.yml
+++ b/.github/workflows/phpunit-mysql.yml
@@ -32,19 +32,17 @@ concurrency:
   group: phpunit-mysql-${{ github.head_ref || github.run_id }}
   cancel-in-progress: true
 
-env:
-  # Location of the phpunit.xml and phpunit.integration.xml files
-  PHPUNIT_CONFIG: ./tests/phpunit.xml
-  PHPUNIT_INTEGRATION_CONFIG: ./tests/phpunit.integration.xml
-
 jobs:
   phpunit-mysql:
     runs-on: ubuntu-latest
 
     strategy:
       matrix:
-        php-versions: ['7.4', '8.0', '8.1']
-        server-versions: ['master', 'stable25']
+        php-versions: ['8.0', '8.1']
+        server-versions: ['master']
+        include:
+          - php-versions: '7.4'
+            server-versions: 'stable25'
 
     services:
       mysql:
@@ -67,32 +65,33 @@ jobs:
           echo "SELECT @@sql_mode;" | mysql -h 127.0.0.1 -P 4444 -u root -prootpassword
 
       - name: Checkout server
-        uses: actions/checkout@v3
+        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
         with:
           submodules: true
           repository: nextcloud/server
           ref: ${{ matrix.server-versions }}
 
       - name: Checkout app
-        uses: actions/checkout@v3
+        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
         with:
           path: apps/${{ env.APP_NAME }}
 
       - name: Set up php ${{ matrix.php-versions }}
-        uses: shivammathur/setup-php@v2
+        uses: shivammathur/setup-php@1a18b2267f80291a81ca1d33e7c851fe09e7dfc4 # v2
         with:
           php-version: ${{ matrix.php-versions }}
-          tools: phpunit
           extensions: mbstring, iconv, fileinfo, intl, mysql, pdo_mysql
           coverage: none
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Check composer file existence
         id: check_composer
-        uses: andstor/file-existence-action@v2
+        uses: andstor/file-existence-action@20b4d2e596410855db8f9ca21e96fbe18e12930b # v2
         with:
           files: apps/${{ env.APP_NAME }}/composer.json
 
-      - name: Set up PHPUnit
+      - name: Set up dependencies
         # Only run if phpunit config file exists
         if: steps.check_composer.outputs.files_exists == 'true'
         working-directory: apps/${{ env.APP_NAME }}
@@ -106,34 +105,43 @@ jobs:
           ./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 password
           ./occ app:enable --force ${{ env.APP_NAME }}
 
-      - name: Check PHPUnit config file existence
+      - name: Check PHPUnit script is defined
         id: check_phpunit
-        uses: andstor/file-existence-action@v2
-        with:
-          files: apps/${{ env.APP_NAME }}/${{ env.PHPUNIT_CONFIG }}
+        continue-on-error: true
+        working-directory: apps/${{ env.APP_NAME }}
+        run: |
+          composer run --list | grep "^  test:unit " | wc -l | grep 1
 
       - name: PHPUnit
         # Only run if phpunit config file exists
-        if: steps.check_phpunit.outputs.files_exists == 'true'
+        if: steps.check_phpunit.outcome == 'success'
         working-directory: apps/${{ env.APP_NAME }}
-        run: ./vendor/phpunit/phpunit/phpunit -c ${{ env.PHPUNIT_CONFIG }}
+        run: composer run test:unit
 
-      - name: Check PHPUnit integration config file existence
+      - name: Check PHPUnit integration script is defined
         id: check_integration
-        uses: andstor/file-existence-action@v2
-        with:
-          files: apps/${{ env.APP_NAME }}/${{ env.PHPUNIT_INTEGRATION_CONFIG }}
+        continue-on-error: true
+        working-directory: apps/${{ env.APP_NAME }}
+        run: |
+          composer run --list | grep "^  test:integration " | wc -l | grep 1
 
       - name: Run Nextcloud
         # Only run if phpunit integration config file exists
-        if: steps.check_integration.outputs.files_exists == 'true'
+        if: steps.check_integration.outcome == 'success'
         run: php -S localhost:8080 &
 
       - name: PHPUnit integration
         # Only run if phpunit integration config file exists
-        if: steps.check_integration.outputs.files_exists == 'true'
+        if: steps.check_integration.outcome == 'success'
         working-directory: apps/${{ env.APP_NAME }}
-        run: ./vendor/phpunit/phpunit/phpunit -c ${{ env.PHPUNIT_INTEGRATION_CONFIG }}
+        run: composer run test:integration
+
+      - name: Skipped
+        # Fail the action when neither unit nor integration tests ran
+        if: steps.check_phpunit.outcome == 'failure' && steps.check_integration.outcome == 'failure'
+        run: |
+          echo 'Neither PHPUnit nor PHPUnit integration tests are specified in composer.json scripts'
+          exit 1
 
   summary:
     permissions:
diff --git a/.github/workflows/phpunit-pgsql.yml b/.github/workflows/phpunit-pgsql.yml
index 43bff99b..8ec26389 100644
--- a/.github/workflows/phpunit-pgsql.yml
+++ b/.github/workflows/phpunit-pgsql.yml
@@ -32,11 +32,6 @@ concurrency:
   group: phpunit-pgsql-${{ github.head_ref || github.run_id }}
   cancel-in-progress: true
 
-env:
-  # Location of the phpunit.xml and phpunit.integration.xml files
-  PHPUNIT_CONFIG: ./tests/phpunit.xml
-  PHPUNIT_INTEGRATION_CONFIG: ./tests/phpunit.integration.xml
-
 jobs:
   phpunit-pgsql:
     runs-on: ubuntu-latest
@@ -64,32 +59,33 @@ jobs:
           echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
 
       - name: Checkout server
-        uses: actions/checkout@v3
+        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
         with:
           submodules: true
           repository: nextcloud/server
           ref: ${{ matrix.server-versions }}
 
       - name: Checkout app
-        uses: actions/checkout@v3
+        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
         with:
           path: apps/${{ env.APP_NAME }}
 
       - name: Set up php ${{ matrix.php-versions }}
-        uses: shivammathur/setup-php@v2
+        uses: shivammathur/setup-php@1a18b2267f80291a81ca1d33e7c851fe09e7dfc4 # v2
         with:
           php-version: ${{ matrix.php-versions }}
-          tools: phpunit
           extensions: mbstring, iconv, fileinfo, intl, pgsql, pdo_pgsql
           coverage: none
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Check composer file existence
         id: check_composer
-        uses: andstor/file-existence-action@v2
+        uses: andstor/file-existence-action@20b4d2e596410855db8f9ca21e96fbe18e12930b # v2
         with:
           files: apps/${{ env.APP_NAME }}/composer.json
 
-      - name: Set up PHPUnit
+      - name: Set up dependencies
         # Only run if phpunit config file exists
         if: steps.check_composer.outputs.files_exists == 'true'
         working-directory: apps/${{ env.APP_NAME }}
@@ -103,34 +99,43 @@ jobs:
           ./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 password
           ./occ app:enable --force ${{ env.APP_NAME }}
 
-      - name: Check PHPUnit config file existence
+      - name: Check PHPUnit script is defined
         id: check_phpunit
-        uses: andstor/file-existence-action@v2
-        with:
-          files: apps/${{ env.APP_NAME }}/${{ env.PHPUNIT_CONFIG }}
+        continue-on-error: true
+        working-directory: apps/${{ env.APP_NAME }}
+        run: |
+          composer run --list | grep "^  test:unit " | wc -l | grep 1
 
       - name: PHPUnit
         # Only run if phpunit config file exists
-        if: steps.check_phpunit.outputs.files_exists == 'true'
+        if: steps.check_phpunit.outcome == 'success'
         working-directory: apps/${{ env.APP_NAME }}
-        run: ./vendor/phpunit/phpunit/phpunit -c ${{ env.PHPUNIT_CONFIG }}
+        run: composer run test:unit
 
-      - name: Check PHPUnit integration config file existence
+      - name: Check PHPUnit integration script is defined
         id: check_integration
-        uses: andstor/file-existence-action@v2
-        with:
-          files: apps/${{ env.APP_NAME }}/${{ env.PHPUNIT_INTEGRATION_CONFIG }}
+        continue-on-error: true
+        working-directory: apps/${{ env.APP_NAME }}
+        run: |
+          composer run --list | grep "^  test:integration " | wc -l | grep 1
 
       - name: Run Nextcloud
         # Only run if phpunit integration config file exists
-        if: steps.check_integration.outputs.files_exists == 'true'
+        if: steps.check_integration.outcome == 'success'
         run: php -S localhost:8080 &
 
       - name: PHPUnit integration
         # Only run if phpunit integration config file exists
-        if: steps.check_integration.outputs.files_exists == 'true'
+        if: steps.check_integration.outcome == 'success'
         working-directory: apps/${{ env.APP_NAME }}
-        run: ./vendor/phpunit/phpunit/phpunit -c ${{ env.PHPUNIT_INTEGRATION_CONFIG }}
+        run: composer run test:integration
+
+      - name: Skipped
+        # Fail the action when neither unit nor integration tests ran
+        if: steps.check_phpunit.outcome == 'failure' && steps.check_integration.outcome == 'failure'
+        run: |
+          echo 'Neither PHPUnit nor PHPUnit integration tests are specified in composer.json scripts'
+          exit 1
 
   summary:
     permissions:
diff --git a/.github/workflows/phpunit-sqlite.yml b/.github/workflows/phpunit-sqlite.yml
index 6ed438cd..f1e432af 100644
--- a/.github/workflows/phpunit-sqlite.yml
+++ b/.github/workflows/phpunit-sqlite.yml
@@ -32,11 +32,6 @@ concurrency:
   group: phpunit-sqlite-${{ github.head_ref || github.run_id }}
   cancel-in-progress: true
 
-env:
-  # Location of the phpunit.xml and phpunit.integration.xml files
-  PHPUNIT_CONFIG: ./tests/phpunit.xml
-  PHPUNIT_INTEGRATION_CONFIG: ./tests/phpunit.integration.xml
-
 jobs:
   phpunit-sqlite:
     runs-on: ubuntu-latest
@@ -53,32 +48,33 @@ jobs:
           echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
 
       - name: Checkout server
-        uses: actions/checkout@v3
+        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
         with:
           submodules: true
           repository: nextcloud/server
           ref: ${{ matrix.server-versions }}
 
       - name: Checkout app
-        uses: actions/checkout@v3
+        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
         with:
           path: apps/${{ env.APP_NAME }}
 
       - name: Set up php ${{ matrix.php-versions }}
-        uses: shivammathur/setup-php@v2
+        uses: shivammathur/setup-php@1a18b2267f80291a81ca1d33e7c851fe09e7dfc4 # v2
         with:
           php-version: ${{ matrix.php-versions }}
-          tools: phpunit
           extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite
           coverage: none
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Check composer file existence
         id: check_composer
-        uses: andstor/file-existence-action@v2
+        uses: andstor/file-existence-action@20b4d2e596410855db8f9ca21e96fbe18e12930b # v2
         with:
           files: apps/${{ env.APP_NAME }}/composer.json
 
-      - name: Set up PHPUnit
+      - name: Set up dependencies
         # Only run if phpunit config file exists
         if: steps.check_composer.outputs.files_exists == 'true'
         working-directory: apps/${{ env.APP_NAME }}
@@ -92,34 +88,43 @@ jobs:
           ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass password
           ./occ app:enable --force ${{ env.APP_NAME }}
 
-      - name: Check PHPUnit config file existence
+      - name: Check PHPUnit script is defined
         id: check_phpunit
-        uses: andstor/file-existence-action@v2
-        with:
-          files: apps/${{ env.APP_NAME }}/${{ env.PHPUNIT_CONFIG }}
+        continue-on-error: true
+        working-directory: apps/${{ env.APP_NAME }}
+        run: |
+          composer run --list | grep "^  test:unit " | wc -l | grep 1
 
       - name: PHPUnit
         # Only run if phpunit config file exists
-        if: steps.check_phpunit.outputs.files_exists == 'true'
+        if: steps.check_phpunit.outcome == 'success'
         working-directory: apps/${{ env.APP_NAME }}
-        run: ./vendor/phpunit/phpunit/phpunit -c ${{ env.PHPUNIT_CONFIG }}
+        run: composer run test:unit
 
-      - name: Check PHPUnit integration config file existence
+      - name: Check PHPUnit integration script is defined
         id: check_integration
-        uses: andstor/file-existence-action@v2
-        with:
-          files: apps/${{ env.APP_NAME }}/${{ env.PHPUNIT_INTEGRATION_CONFIG }}
+        continue-on-error: true
+        working-directory: apps/${{ env.APP_NAME }}
+        run: |
+          composer run --list | grep "^  test:integration " | wc -l | grep 1
 
       - name: Run Nextcloud
         # Only run if phpunit integration config file exists
-        if: steps.check_integration.outputs.files_exists == 'true'
+        if: steps.check_integration.outcome == 'success'
         run: php -S localhost:8080 &
 
       - name: PHPUnit integration
         # Only run if phpunit integration config file exists
-        if: steps.check_integration.outputs.files_exists == 'true'
+        if: steps.check_integration.outcome == 'success'
         working-directory: apps/${{ env.APP_NAME }}
-        run: ./vendor/phpunit/phpunit/phpunit -c ${{ env.PHPUNIT_INTEGRATION_CONFIG }}
+        run: composer run test:integration
+
+      - name: Skipped
+        # Fail the action when neither unit nor integration tests ran
+        if: steps.check_phpunit.outcome == 'failure' && steps.check_integration.outcome == 'failure'
+        run: |
+          echo 'Neither PHPUnit nor PHPUnit integration tests are specified in composer.json scripts'
+          exit 1
 
   summary:
     permissions:
diff --git a/.github/workflows/phpunit-summary-when-unrelated.yml b/.github/workflows/phpunit-summary-when-unrelated.yml
new file mode 100644
index 00000000..53ca3096
--- /dev/null
+++ b/.github/workflows/phpunit-summary-when-unrelated.yml
@@ -0,0 +1,68 @@
+# This workflow is provided via the organization template repository
+#
+# https://github.com/nextcloud/.github
+# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
+
+name: PHPUnit
+
+on:
+  pull_request:
+    paths-ignore:
+      - '.github/workflows/**'
+      - 'appinfo/**'
+      - 'lib/**'
+      - 'templates/**'
+      - 'tests/**'
+      - 'vendor/**'
+      - 'vendor-bin/**'
+      - '.php-cs-fixer.dist.php'
+      - 'composer.json'
+      - 'composer.lock'
+
+permissions:
+  contents: read
+
+jobs:
+  summary-mysql:
+    permissions:
+      contents: none
+    runs-on: ubuntu-latest
+
+    name: phpunit-mysql-summary
+
+    steps:
+      - name: Summary status
+        run: 'echo "No PHP files changed, skipped PHPUnit"'
+
+  summary-oci:
+    permissions:
+      contents: none
+    runs-on: ubuntu-latest
+
+    name: phpunit-oci-summary
+
+    steps:
+      - name: Summary status
+        run: 'echo "No PHP files changed, skipped PHPUnit"'
+
+  summary-pgsql:
+    permissions:
+      contents: none
+    runs-on: ubuntu-latest
+
+    name: phpunit-pgsql-summary
+
+    steps:
+      - name: Summary status
+        run: 'echo "No PHP files changed, skipped PHPUnit"'
+
+  summary-sqlite:
+    permissions:
+      contents: none
+    runs-on: ubuntu-latest
+
+    name: phpunit-sqlite-summary
+
+    steps:
+      - name: Summary status
+        run: 'echo "No PHP files changed, skipped PHPUnit"'
diff --git a/.github/workflows/psalm-matrix.yml b/.github/workflows/psalm-matrix.yml
new file mode 100644
index 00000000..1bb33f3b
--- /dev/null
+++ b/.github/workflows/psalm-matrix.yml
@@ -0,0 +1,61 @@
+# This workflow is provided via the organization template repository
+#
+# https://github.com/nextcloud/.github
+# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
+
+name: Static analysis
+
+on:
+  pull_request:
+  push:
+    branches:
+      - master
+      - main
+      - stable*
+
+concurrency:
+  group: psalm-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+
+jobs:
+  static-analysis:
+    runs-on: ubuntu-latest
+    strategy:
+      # do not stop on another job's failure
+      fail-fast: false
+      matrix:
+        ocp-version: [ 'dev-master', 'dev-stable25' ]
+
+    name: Nextcloud ${{ matrix.ocp-version }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+
+      - name: Set up php
+        uses: shivammathur/setup-php@1a18b2267f80291a81ca1d33e7c851fe09e7dfc4 # v2
+        with:
+          php-version: 8.0
+          coverage: none
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Install dependencies
+        run: composer i
+
+      - name: Install dependencies
+        run: composer require --dev nextcloud/ocp:${{ matrix.ocp-version }} --ignore-platform-reqs
+
+      - name: Run coding standards check
+        run: composer run psalm
+
+  summary:
+    runs-on: ubuntu-latest
+    needs: static-analysis
+
+    if: always()
+
+    name: static-psalm-analysis-summary
+
+    steps:
+      - name: Summary status
+        run: if ${{ needs.static-analysis.result != 'success' }}; then exit 1; fi
diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml
deleted file mode 100644
index eb45673e..00000000
--- a/.github/workflows/static-analysis.yml
+++ /dev/null
@@ -1,26 +0,0 @@
-name: Static analysis
-
-on: [pull_request]
-
-jobs:
-  static-psalm-analysis:
-      runs-on: ubuntu-latest
-      strategy:
-          matrix:
-              ocp-version: [ 'dev-master', 'dev-stable25' ]
-      name: Nextcloud ${{ matrix.ocp-version }}
-      steps:
-          - name: Checkout
-            uses: actions/checkout@v3
-          - name: Set up php
-            uses: shivammathur/setup-php@v2
-            with:
-                php-version: 7.4
-                tools: composer:v1
-                coverage: none
-          - name: Install dependencies
-            run: composer i
-          - name: Install dependencies
-            run: composer require --dev christophwurst/nextcloud:${{ matrix.ocp-version }}
-          - name: Run coding standards check
-            run: composer run psalm
diff --git a/composer.json b/composer.json
index ffafa251..216a09cb 100644
--- a/composer.json
+++ b/composer.json
@@ -39,7 +39,8 @@
     "psalm": "psalm --threads=1 --update-baseline",
     "psalm:update-baseline": "psalm --threads=1 --update-baseline",
     "psalm:clear": "psalm --clear-cache && psalm --clear-global-cache",
-    "psalm:fix": "psalm --alter --issues=InvalidReturnType,InvalidNullableReturnType,MissingParamType,InvalidFalsableReturnType"
+    "psalm:fix": "psalm --alter --issues=InvalidReturnType,InvalidNullableReturnType,MissingParamType,InvalidFalsableReturnType",
+    "test:unit": "vendor/bin/phpunit -c tests/phpunit.xml"
   },
   "repositories": [
     {