diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 8d2308d884810224d6f9e6aa73a55c8c88e925e9..1ed1a6aadfc3a8ff7bdc1bca90f10536f42ef8d3 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -45,19 +45,40 @@ build-req-image:
     changes:
       - .meta/Dockerfile
 
+.on-file-change:
+  rules:
+    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+      changes:
+        paths:
+          - grib_processor/**/*
+    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
+      when: always
+    - if: $CI_COMMIT_TAG
+      when: always
+
+.on-new-version:
+  rules:
+    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
+      when: never
+    - if: $CI_COMMIT_TAG =~ /\d+\.\d+\.\d+$/
+      when: always
+
 test::lint:
   stage: test
+  extends: .on-file-change
   script:
     - python -m ruff format --check --diff --target-version py38
     - python -m ruff check --diff --target-version py38
 
 test::static-analysis:
   stage: test
+  extends: .on-file-change
   script:
     - python -m mypy grib_processor/
 
 test::unit:
   stage: test
+  extends: .on-file-change
   script:
     - python -m coverage run -m pytest
     - python -m coverage report
@@ -74,9 +95,7 @@ test::unit:
 
 deploy::build:
   stage: deploy
-  rules:
-    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
-    - if: $CI_COMMIT_TAG =~ /\d+\.\d+\.\d+$/
+  extends: .on-new-version
   before_script:
       pip install build
   script:
@@ -89,10 +108,8 @@ deploy::build:
 
 deploy::package::release:
   stage: deploy
-  rules:
-    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
-    - if: $CI_COMMIT_TAG =~ /\d+\.\d+\.\d+$/
+  extends: .on-new-version
   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/*
+    - TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url ${PACKAGE_REGISTRY_URL} ./dist/*