diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 59f41ba03e76b2afe0a4fbc48acfb9304adc4aa1..a3ed08b9de90bbbeaf02b0a3adafd8154c6e9cf2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,64 @@
-include:
-  - project: 'quiqqer/stabilization/semantic-release'
-    file: '/ci-templates/.gitlab-ci.yml'
-    ref: dev
+---
+stages:
+- ".pre"
+- test
+- release
+- ".post"
+is_merge_allowed:
+  stage: test
+  script:
+  - 'echo "Error: Merging branch $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME directly into
+    $CI_DEFAULT_BRANCH is not allowed. Please merge branch $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
+    into next first and from there into $CI_DEFAULT_BRANCH."
+
+    '
+  - exit 1
+  rules:
+  - if: $CI_PIPELINE_SOURCE == 'merge_request_event' && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
+      != "next" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH
+validate_required_files:
+  image: bash:5
+  stage: test
+  script:
+  - 'if [ ! -e composer.json ]; then (echo "Error: composer.json does not exist";
+    exit 1); fi'
+  - 'if [ ! -e README.md ]; then (echo "Error: README.md does not exist"; exit 1);
+    fi'
+  - 'if [ ! -e LICENSE ]; then (echo "Error: LICENSE file does not exist"; exit 1);
+    fi'
+  - 'if [ ! -e package.xml ]; then (echo "Error: package.xml does not exist"; exit
+    1); fi'
+  rules:
+  - if: "$CI_PIPELINE_SOURCE == 'merge_request_event' && $CI_MERGE_REQUEST_TARGET_BRANCH_PROTECTED"
+validate_composer:
+  image: composer:2
+  stage: test
+  script:
+  - composer validate --no-check-all --strict composer.json
+  - COMPOSER_TYPE=$(php -r "echo json_decode(file_get_contents('composer.json'))->type;")
+  - 'if [[ $COMPOSER_TYPE != quiqqer-@(module|plugin|template|application) ]] ; then
+    (echo "Error: Invalid type $COMPOSER_TYPE in composer.json. Should be quiqqer-module,
+    quiqqer-plugin, quiqqer-template or quiqqer-application"; exit 1); fi'
+  rules:
+  - if: "$CI_PIPELINE_SOURCE == 'merge_request_event' && $CI_MERGE_REQUEST_TARGET_BRANCH_PROTECTED"
+codestyle_check:
+  image: composer:2
+  stage: test
+  script:
+  - composer global require squizlabs/php_codesniffer=^3
+  - "/tmp/vendor/bin/phpcs -n -p --standard=PSR12 --report=full --no-cache --colors
+    --extensions=php --basepath=$CI_PROJECT_DIR $CI_PROJECT_DIR"
+  rules:
+  - if: "$CI_PIPELINE_SOURCE == 'merge_request_event' && $CI_MERGE_REQUEST_TARGET_BRANCH_PROTECTED"
+  allow_failure: true
+release:
+  image: node:18-slim
+  stage: release
+  before_script:
+  - apt-get update && apt-get install -y --no-install-recommends git-core ca-certificates
+  - npm install -g semantic-release@^21 git+https://dev.quiqqer.com/quiqqer/stabilization/semantic-release.git#dev
+  script:
+  - semantic-release --extends @quiqqer/semantic-release-config
+  rules:
+  - if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"
+  - if: "$CI_COMMIT_BRANCH =~ /^(\\d\\.)?\\d.x$/"