diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..78a8be9587952d9edfd86ba3aa68efd1b69d61e2
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,79 @@
+variables:
+  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
+
+cache:
+  paths:
+    - .cache/pip
+
+default:
+  image: python:latest
+  tags: ["docker", "ssec_shared"] # use shared runners at the SSEC
+  before_script:
+    - python --version ; pip --version # For debugging
+    - pip install -r requirements_dev.txt
+    
+workflow:
+  rules:
+    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+      when: always
+    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
+
+stages:
+  - lint
+  - static-analysis
+  - test
+  - build
+  - deploy
+
+lint-job:
+  stage: lint
+  script:
+    - ruff format --check --diff --target-version py38
+    - ruff check --diff --target-version py38
+
+
+static-analysis-job:
+  stage: static-analysis
+  script:
+    - mypy grib_processor/
+
+test:unit:
+  stage: test
+  script:
+    - coverage run -m pytest
+    - coverage report
+    - coverage xml
+  artifacts:
+    untracked: false
+    when: on_success
+    expire_in: "30 days"
+    reports:
+      coverage_report:
+        coverage_format: cobertura
+        path: ./coverage.xml
+  coverage: /(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/
+
+build:dist:
+  stage: build
+  rules:
+    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+    - if: $CI_COMMIT_TAG =~ /\d+\.\d+\.\d+$/
+  before_script:
+      pip install build
+  script:
+    - python -m build
+  artifacts:
+    expire_in: "1 day"
+    paths:
+      - ./dist/*.whl
+      - ./dist/*.tar.gz
+
+deploy:packages:release:
+  stage: deploy
+  rules:
+    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+    - if: $CI_COMMIT_TAG =~ /\d+\.\d+\.\d+$/
+  before_script:
+      pip install twine
+  script:
+    - TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url ${PACKAGE_REGISTRY_URL}  dist/*