diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..03d0c1831a77b7c6bf808d9e629d220c92cc2c3d
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,81 @@
+stages:
+  - test
+  - build
+
+pages:
+  image: zoo.smok.co/build/build:latest
+  stage: build
+  script:
+    - cd docs
+    - make html
+    - cd ..
+    - mv docs/_build/html public
+  artifacts:
+    paths:
+      - public
+
+.test:
+  stage: test
+  before_script:
+    - pip install --break-system-packages --upgrade pytest setuptools pip coverage nose2
+    - pip install ".[dev,test]"
+  script:
+    - pytest -n 8 -vv --cov=satella
+    - coverage xml
+    - coverage report
+  coverage: /TOTAL.*\s+(\d+\%)/
+
+test_python27:
+  extends: .test
+  image: python:2.7
+
+test_python35:
+  extends: .test
+  image: python:3.5
+
+test_python36:
+  extends: .test
+  image: python:3.6
+
+test_python37:
+  extends: .test
+  image: python:3.7
+
+test_python38:
+  extends: .test
+  image: python:3.8
+
+test_python39:
+  extends: .test
+  image: python:3.9
+
+test_python310:
+  extends: .test
+  image: python:3.10
+
+test_python311:
+  extends: .test
+  image: python:3.11
+
+test_python312:
+  extends: .test
+  image: python:3.12
+
+
+build_python:
+  stage: build
+  before_script:
+    - pip install --break-system-packages --upgrade setuptools pip twine
+  script:
+    - python setup.py bdist_wheel
+    - mv dist/*.whl .
+  artifacts:
+    paths:
+      - "*.whl"
+  after_script:
+    - TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python3 -m twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi *.whl
+  only:
+    - tags
+  except:
+    - branches
+