diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3664823c7d9848b9f90f58ebd112a0c99baeb45d..0fb6f732f7187e1a39d25fb8755da46ddec588a8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,2 +1,253 @@
-# The Docker image that will be used to build your app
 image: studip/studip:tests-php7.2
+
+variables:
+  MYSQL_RANDOM_ROOT_PASSWORD: "true"
+  MYSQL_DATABASE: studip_db
+  MYSQL_USER: studip_user
+  MYSQL_PASSWORD: studip_password
+  MYSQL_HOST: mariadb
+  DEMO_DATA: "true"
+  MAIL_TRANSPORT: debug
+  PHPSTAN_LEVEL: 0
+  # Optimize caching
+  FF_USE_FASTZIP: "true"
+  CACHE_COMPRESSION_LEVEL: "fast"
+  # User faster docker driver
+  DOCKER_DRIVER: overlay2
+
+stages:
+  - checks
+  - analyse
+  - tests
+  - packaging
+  - release
+  - build
+
+.scripts:
+  install-composer:
+    - make composer-dev
+  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:
+    - !reference [.scripts, configure-studip]
+    - chmod +x .gitlab/scripts/install_db.sh
+    - .gitlab/scripts/install_db.sh
+    - cli/studip migrate
+  remove-absolute-path-in-report:
+    - sed -i "s%$PWD/%%" tests/_output/report.xml
+
+.artifacts:
+  common: &common-artifacts
+    when: always
+    expire_in: 1 week
+  tests: &test-artifacts
+    <<: *common-artifacts
+    paths:
+      - tests/_output
+    reports:
+      junit: tests/_output/report.xml
+
+.caches:
+  php: &composer-cache
+    key: "php-$CI_COMMIT_REF_SLUG"
+    paths:
+      - composer/
+      - .caches/phplint-cache
+      - .caches/resultCache.php
+      - .caches/cache/*
+      - .caches/resultCaches/*
+  js: &npm-cache
+    key: "js-$CI_COMMIT_REF_SLUG"
+    paths:
+      - node_modules/
+      - .caches/eslint-cache
+      - .caches/stylelint-cache
+
+lint-php:
+  stage: checks
+  needs: []
+  cache: *composer-cache
+  allow_failure: false
+  interruptible: true
+  before_script:
+    - !reference [.scripts, install-composer]
+    - mkdir -p .reports
+  script:
+    - php -d memory_limit=-1 composer/bin/phplint --xml .reports/phplint-report.xml --cache=.caches/phplint-cache
+  artifacts:
+    <<: *common-artifacts
+    paths:
+      - .reports/phplint-report.xml
+    reports:
+      junit: .reports/phplint-report.xml
+
+lint-js:
+  stage: checks
+  needs: []
+  cache: *npm-cache
+  allow_failure: false
+  interruptible: true
+  before_script:
+    - make npm
+  script:
+    - npm run lint -- --cache --cache-location .caches/eslint-cache --format ./node_modules/eslint-junit/index.js
+  artifacts:
+    <<: *common-artifacts
+    paths:
+      - .reports/eslint-report.xml
+    reports:
+       junit: .reports/eslint-report.xml
+
+lint-css:
+  stage: checks
+  needs: []
+  cache: *npm-cache
+  allow_failure: false
+  interruptible: true
+  before_script:
+    - make npm
+  script:
+    - npm run css-lint -s -- --cache --cache-location .caches/stylelint-cache --custom-formatter node_modules/stylelint-junit-formatter --output-file .reports/stylelint-report.xml
+  artifacts:
+    <<: *common-artifacts
+    paths:
+      - .reports/stylelint-report.xml
+    reports:
+      junit: .reports/stylelint-report.xml
+
+phpstan:
+  stage: analyse
+  needs: [lint-php]
+  allow_failure: true
+  interruptible: true
+  when: manual
+  cache: *composer-cache
+  before_script:
+    - make composer-dev
+    - mkdir .reports -p
+  script:
+    - php composer/bin/phpstan analyse --memory-limit=1G --no-progress --level=$PHPSTAN_LEVEL --error-format=gitlab > .reports/report-phpstan.json
+  artifacts:
+    paths:
+        - .reports/report-phpstan.json
+    expire_in: 14 days
+    when: always
+    reports:
+      codequality: .reports/report-phpstan.json
+
+test-unit:
+  stage: tests
+  needs: [lint-php]
+  cache:
+    <<: *composer-cache
+    policy: pull
+  allow_failure: false
+  interruptible: true
+  before_script:
+    - !reference [.scripts, configure-studip]
+  script:
+    - composer/bin/codecept run unit --xml
+    - !reference [.scripts, remove-absolute-path-in-report]
+  artifacts:
+    <<: *test-artifacts
+
+test-functional:
+  stage: tests
+  needs: [lint-php]
+  cache:
+    <<: *composer-cache
+    policy: pull
+  services:
+    - mariadb
+  allow_failure: false
+  interruptible: true
+  before_script:
+    - !reference [.scripts, initialize-studip-database]
+  script:
+    - composer/bin/codecept run functional --xml
+    - !reference [.scripts, remove-absolute-path-in-report]
+  artifacts:
+   <<: *test-artifacts
+
+test-jsonapi:
+  stage: tests
+  needs: [lint-php]
+  cache:
+    <<: *composer-cache
+    policy: pull
+  services:
+    - mariadb
+  allow_failure: false
+  interruptible: true
+  before_script:
+    - !reference [.scripts, initialize-studip-database]
+  script:
+    - composer/bin/codecept run jsonapi --xml
+    - !reference [.scripts, remove-absolute-path-in-report]
+  artifacts:
+    <<: *test-artifacts
+    when: always
+    expire_in: 1 week
+    paths:
+      - tests/_output
+    reports:
+      junit: tests/_output/report.xml
+
+test-assets:
+  stage: tests
+  needs: []
+  cache: *npm-cache
+  allow_failure: false
+  interruptible: true
+  before_script:
+    - make npm
+  script:
+    - make webpack-dev
+
+packaging:
+  stage: packaging
+  cache: []
+  rules:
+    - if: $CI_COMMIT_TAG
+  before_script:
+    - echo GE_JOB_ID=$CI_JOB_ID >> .packaging.env
+    - mkdir .pkg
+  script:
+    - echo 'Running packaging job'
+    - make build clean-npm
+    - zip -r9 .pkg/studip-$CI_COMMIT_TAG.zip *
+    - tar -czf .pkg/studip-$CI_COMMIT_TAG.tar.gz *
+  artifacts:
+    name: 'Stud.IP-Release-$CI_COMMIT_TAG'
+    paths:
+      - .pkg/studip-$CI_COMMIT_TAG.zip
+      - .pkg/studip-$CI_COMMIT_TAG.tar.gz
+    reports:
+      dotenv: .packaging.env
+    expire_in: never
+
+release:
+  stage: release
+  image: studip/release-cli
+  cache: []
+  rules:
+    - if: $CI_COMMIT_TAG
+  script:
+    - echo 'Running release job'
+  needs:
+    - job: packaging
+      artifacts: true
+  release:
+    name: "$CI_COMMIT_TAG"
+    description: "https://gitlab.studip.de/studip/studip/-/blob/${CI_COMMIT_TAG}/ChangeLog"
+    tag_name: "$CI_COMMIT_TAG"
+    assets:
+      links:
+        - name: "studip-$CI_COMMIT_TAG.zip"
+          url: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/jobs/${GE_JOB_ID}/artifacts/.pkg/studip-$CI_COMMIT_TAG.zip"
+          link_type: package
+        - name: "studip-$CI_COMMIT_TAG.tar.gz"
+          url: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/jobs/${GE_JOB_ID}/artifacts/.pkg/studip-$CI_COMMIT_TAG.tar.gz"
+          link_type: package