diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3c73ab8bda75d96db01213145e0e6232e5f28b98
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,73 @@
+---
+
+variables:
+  PDM_CACHE_DIR: $CI_PROJECT_DIR/.cache/pdm
+  npm_config_cache: $CI_PROJECT_DIR/.cache/npm
+  PIP_CACHE_DIR: $CI_PROJECT_DIR/.cache/pip
+
+default:
+  cache:
+    paths:
+      - .cache
+
+.python:
+  image: python:3.12
+  before_script:
+    - pip install pdm
+    - apt-get update
+    - apt-get -y install libldap-dev libsasl2-dev
+    - pdm sync --clean
+
+lint-py:
+  extends: .python
+  stage: test
+  script:
+    - >-
+      pdm run
+      ruff
+      check
+      --output-format gitlab
+      --output-file ruff.json
+    - pdm run ruff format --diff
+  artifacts:
+    reports:
+      codequality: ruff.json
+    expire_in: 7 days
+
+build-js:
+  image: node:20-alpine
+  stage: build
+  before_script:
+    - npm ci
+  script:
+    - npm run build
+  artifacts:
+    paths:
+      - schilder2000/static
+
+build-py:
+  extends: .python
+  stage: build
+  needs:
+    - job: build-js
+      artifacts: true
+  script:
+    - pdm build -C without-npm
+  artifacts:
+    paths:
+      - dist
+
+publish:
+  extends: .python
+  stage: deploy
+  needs:
+    - job: build-py
+      artifacts: true
+    - job: lint-py
+      artifacts: false
+  variables:
+    PDM_PUBLISH_REPO: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi
+    PDM_PUBLISH_USERNAME: gitlab-ci-token
+    PDM_PUBLISH_PASSWORD: ${CI_JOB_TOKEN}
+  script:
+    - pdm publish --no-build