diff --git a/.editorconfig b/.editorconfig
index 4ab7203cb7a3e0d274441b897d59fafab813930d..242a2410e6f60e05aa6af83c0cf69b769c4481b3 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -20,3 +20,6 @@ block_comment_start = /*
 block_comment_end = */
 line_comment = //
 quote_type = single
+
+[*.yml]
+indent_size = 2
\ No newline at end of file
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 02e79e78352e6499e3ff0d946dc505ff6a82fd5a..237e1059b4ba6915938b26d47fb1048738c5c73c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -33,18 +33,18 @@ stages:
   - release
 
 .scripts:
-  mkdir-caches: &mkdir-caches
+  mkdir-caches:
     - mkdir -p $CACHE_DIR
-  mkdir-reports: &mkdir-reports
+  mkdir-reports:
     - mkdir -p $REPORT_DIR
-  install-composer: &install-composer
+  install-composer:
     - make composer-dev
-  configure-studip: &configure-studip
-    - *install-composer
+  configure-studip:
+    - !reference [.scripts, install-composer]
     - cp docker/studip/config_local.php config/config_local.inc.php
     - cp config/config.inc.php.dist config/config.inc.php
-  initialize-studip-database: &initialize-studip-database
-    - *configure-studip
+  initialize-studip-database:
+    - !reference [.scripts, configure-studip]
     - chmod +x .gitlab/scripts/install_db.sh
     - .gitlab/scripts/install_db.sh
     - cli/studip migrate
@@ -57,21 +57,40 @@ stages:
     paths:
       - composer/
     policy: pull
-  npm: &npm-cache
+  npm:
     key:
       files:
         - package-lock.json
     paths:
       - .npm
 
+.services:
+  mariadb:
+    - name: mariadb
+      command: [ "--sql_mode=","--character-set-client=utf8","--character-set-server=utf8","--collation-server=utf8_unicode_ci" ]
+
 .definitions:
-  mariadb-service: &mariadb-service
-   - name: mariadb
-     command: [ "--sql_mode=","--character-set-client=utf8","--character-set-server=utf8","--collation-server=utf8_unicode_ci"]
+  php-changed:
+    - changes:
+      - "**/*.php"
+      - composer.json
+      - composer.lock
+  css-changed:
+    - changes:
+      - "resources/assets/stylesheets/**/*"
+      - "resources/vue/**/*"
+      - package-lock.json
+  js-changed:
+    - changes:
+      - "resources/assets/javascripts/**/*"
+      - "resources/vue/**/*"
+      - package-lock.json
 
 build-composer:
   stage: build
   needs: []
+  rules:
+    - !reference [.definitions, php-changed]
   interruptible: true
   variables:
     COMPOSER_CACHE: $CACHE_DIR/composer-cache
@@ -80,7 +99,7 @@ build-composer:
   script:
     - composer install
   cache:
-    - *composer-cache
+    - !reference [.caches, composer]
     - key: composer-package-cache
       paths:
         - $COMPOSER_CACHE
@@ -89,20 +108,22 @@ build-composer:
 lint-php:
   stage: checks
   needs: [build-composer]
+  rules:
+    - !reference [.definitions, php-changed]
   variables:
     CACHE_LOCATION: $CACHE_DIR/phplint-cache
     PHPLINT_JSON_REPORT: $REPORT_DIR/phplint-report.json
     PHPLINT_CODE_QUALITY_REPORT: $REPORT_DIR/phplint-codequality.json
   interruptible: true
   cache:
-    - *composer-cache
+    - !reference [.caches, composer]
     - key: "$CI_JOB_NAME_SLUG:$CI_COMMIT_REF_SLUG"
       paths:
         - $CACHE_LOCATION
   before_script:
-    - *mkdir-caches
-    - *mkdir-reports
-    - *install-composer
+    - !reference [.scripts, mkdir-caches]
+    - !reference [.scripts, mkdir-reports]
+    - !reference [.scripts, install-composer]
   script:
     - COMPOSER_MEMORY_LIMIT=-1
       composer exec phplint
@@ -118,6 +139,8 @@ lint-php:
 lint-php-8.3:
   image: studip/studip:tests-php8.3
   stage: checks
+  rules:
+    - !reference [.definitions, php-changed]
   needs: [build-composer]
   variables:
     CACHE_LOCATION: $CACHE_DIR/phplint-cache
@@ -125,14 +148,14 @@ lint-php-8.3:
     PHPLINT_CODE_QUALITY_REPORT: $REPORT_DIR/phplint-codequality-8.3.json
   interruptible: true
   cache:
-    - *composer-cache
+    - !reference [.caches, composer]
     - key: "$CI_JOB_NAME_SLUG:$CI_COMMIT_REF_SLUG"
       paths:
         - $CACHE_LOCATION
   before_script:
-    - *mkdir-caches
-    - *mkdir-reports
-    - *install-composer
+    - !reference [.scripts, mkdir-caches]
+    - !reference [.scripts, mkdir-reports]
+    - !reference [.scripts, install-composer]
   script:
     - COMPOSER_MEMORY_LIMIT=-1
       composer exec phplint
@@ -148,6 +171,8 @@ lint-php-8.3:
 lint-js:
   stage: checks
   needs: []
+  rules:
+    - !reference [.definitions, js-changed]
   image: $NODE_IMAGE
   variables:
     CACHE_LOCATION: $CACHE_DIR/eslint-cache
@@ -158,7 +183,7 @@ lint-js:
         - $CACHE_LOCATION
   interruptible: true
   before_script:
-    - *mkdir-reports
+    - !reference [.scripts, mkdir-reports]
     - npm install -g npm@7
     - npm install
       --no-save --no-audit --no-fund
@@ -177,6 +202,8 @@ lint-js:
 lint-css:
   stage: checks
   needs: []
+  rules:
+    - !reference [.definitions, css-changed]
   image: $NODE_IMAGE
   variables:
     CACHE_LOCATION: $CACHE_DIR/stylelint-cache
@@ -187,7 +214,7 @@ lint-css:
       paths:
         - $CACHE_LOCATION
   before_script:
-    - *mkdir-reports
+    - !reference [.scripts, mkdir-reports]
     - npm install
       --no-save --no-audit --no-fund
       --loglevel=error
@@ -208,19 +235,24 @@ lint-css:
 phpstan:
   stage: analyse
   needs: [build-composer]
+  rules:
+    - changes:
+      - "app/controllers/**/*.php"
+      - "lib/**/*.php"
+      - "tests/**/*.php"
   variables:
     CACHE_LOCATION: $CACHE_DIR/phpstan
     PHPSTAN_CODE_QUALITY_REPORT: $REPORT_DIR/phpstan-codequality.json
   interruptible: true
   cache:
-   - *composer-cache
+   - !reference [.caches, composer]
    - key: "$CO_JOB_NAME_SLUG:$CI_COMMIT_REF_SLUG"
      paths:
        - $CACHE_LOCATION
   before_script:
-    - *mkdir-caches
-    - *mkdir-reports
-    - *install-composer
+    - !reference [.scripts, mkdir-caches]
+    - !reference [.scripts, mkdir-reports]
+    - !reference [.scripts, install-composer]
     - 'echo -e "includes:\n    - phpstan.neon.dist\n\nparameters:\n   tmpDir: $PHPSTAN_CACHE_PATH" > phpstan.neon'
   script:
     - php
@@ -238,6 +270,8 @@ phpstan:
 test-unit:
   stage: test
   needs: [lint-php]
+  rules:
+    - !reference [.definitions, php-changed]
   variables:
     PHPUNIT_XML_REPORT: $REPORT_DIR/phpunit-report.xml
   cache:
@@ -246,8 +280,8 @@ test-unit:
   allow_failure: false
   interruptible: true
   before_script:
-    - *mkdir-reports
-    - *configure-studip
+    - !reference [.scripts, mkdir-reports]
+    - !reference [.scripts, configure-studip]
   script:
     - 'composer/bin/codecept
       run unit
@@ -262,13 +296,16 @@ test-unit:
 test-jest:
   stage: test
   needs: [lint-js]
+  rules:
+    - !reference [.definitions, js-changed]
   image: $NODE_IMAGE
   variables:
     JS_TEST_REPORT: $REPORT_DIR/jest.xml
-  cache: *npm-cache
+  cache:
+    - !reference [.caches, npm]
   interruptible: true
   before_script:
-    - *mkdir-reports
+    - !reference [.scripts, mkdir-reports]
     - npm install
   script:
     - JEST_JUNIT_OUTPUT_FILE="$JS_TEST_REPORT" npx jest tests/jest/ --ci --reporters=default --reporters=jest-junit
@@ -279,6 +316,8 @@ test-jest:
 test-functional:
   stage: test
   needs: [lint-php]
+  rules:
+    - !reference [.definitions, php-changed]
   variables:
     FUNCTIONAL_XML_REPORT: $REPORT_DIR/functional-report.xml
     FUNCTIONAL_CODE_QUALITY_REPORT: $REPORT_DIR/functional-codequality.json
@@ -286,12 +325,12 @@ test-functional:
     <<: *composer-cache
     policy: pull
   services:
-    - *mariadb-service
+    - !reference [.services, mariadb]
   allow_failure: false
   interruptible: true
   before_script:
-    - *mkdir-reports
-    - *initialize-studip-database
+    - !reference [.scripts, mkdir-reports]
+    - !reference [.scripts, initialize-studip-database]
   script:
     - 'composer/bin/codecept
       run functional
@@ -306,17 +345,19 @@ test-functional:
 test-jsonapi:
   stage: test
   needs: [lint-php]
+  rules:
+    - !reference [.definitions, php-changed]
   cache:
     <<: *composer-cache
     policy: pull
   services:
-    - *mariadb-service
+    - !reference [.services, mariadb]
   variables:
     JSONAPI_XML_REPORT: $REPORT_DIR/jsonapi-report.xml
   interruptible: true
   before_script:
-    - *mkdir-reports
-    - *initialize-studip-database
+    - !reference [.scripts, mkdir-reports]
+    - !reference [.scripts, initialize-studip-database]
   script:
     - 'composer/bin/codecept
       run jsonapi
@@ -330,9 +371,13 @@ test-jsonapi:
 
 test-assets:
   stage: test
+  rules:
+    - !reference [.definitions, css-changed]
+    - !reference [.definitions, js-changed]
   needs: []
   image: $NODE_IMAGE
-  cache: *npm-cache
+  cache:
+    - !reference [.caches, npm]
   interruptible: true
   before_script:
     - npm install
@@ -352,8 +397,8 @@ test-e2e:
   interruptible: true
   when: manual
   cache:
-    - *composer-cache
-    - *npm-cache
+    - !reference [.caches, composer]
+    - !reference [.caches, npm]
   before_script:
     - mkdir ./bin
     - apt-get update
@@ -372,8 +417,8 @@ test-e2e:
     - php composer-setup.php  --install-dir=./bin --filename=composer
     - export PATH="./bin:$PATH"
     - php -r "unlink('composer-setup.php');"
-    - *mkdir-reports
-    - *initialize-studip-database
+    - !reference [.scripts, mkdir-reports]
+    - !reference [.scripts, initialize-studip-database]
     - ./cli/studip config:set SHOW_TERMS_ON_FIRST_LOGIN 0
     - npm install playwright
     - npm ci