diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 93bf3ab10f23e2662d4d721a8a88825b4fe0bc60..5cb81a498a8ea0dd0c38d4a1472afc9427524196 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,5 +1,6 @@
 stages:
-  - github-sync
+  - test
+  - build
   - release
 
 variables:
@@ -7,41 +8,100 @@ variables:
 
 image: registry.duniter.org/docker/python3/duniterpy-builder:0.0.4
 
-.pyenv: &pyenv
-  tags:
-    - redshift-docker-python
-  before_script:
-    - export PYENV_ROOT="$HOME/.pyenv"
-    - export PATH="$PYENV_ROOT/bin:$PATH"
-    - eval "$(pyenv init -)"
-    - pyenv shell $PYENV_PYTHON_VERSION
-
-push_to_github:
-  stage: github-sync
-  variables:
-    GIT_STRATEGY: none
+# SUB-TASKS
+.push_to_github:
   tags:
     - github
-  script:
+  after_script:
+    # remove all files in current repo
     - rm -rf ./*
-    - rm -rf .git
+    - rm -rf .git*
+    # do a mirror clone in current repo
     - git clone --mirror $CI_REPOSITORY_URL .
+    # do config for github push
     - git remote add github $GITHUB_URL_AND_KEY
     - git config --global user.email "contact@duniter.org"
     - git config --global user.name "Duniter"
-    # Job would fail if we don't remove refs about pull requests
-    - bash -c "cat packed-refs | grep -v 'refs/pull' > packed-refs-new; echo 'Removed pull refs.'"
+    # remove refs about merge requests
+    - bash -c "cat packed-refs | grep -v 'refs/merge-requests' > packed-refs-new; echo 'Removed merge-requests refs.'"
     - mv packed-refs-new packed-refs
+    # github push
     - bash -c "git push --force --mirror github 2>&1 | grep -v duniter-gitlab; echo $?"
 
+.pyenv:
+  tags:
+    - redshift-docker-python
+  before_script:
+    - export PYENV_ROOT="$HOME/.pyenv"
+    - export PATH="$PYENV_ROOT/bin:$PATH"
+    - eval "$(pyenv init -)"
+    - pyenv shell $PYENV_PYTHON_VERSION
 
-releases:
-  <<: *pyenv
+.changes:
+  only:
+    changes:
+      - duniterpy/**/*.py
+      - .gitlab-ci.yml
+      - Makefile
+      - requirements_dev.txt
+      - requirements.txt
+      - setup.py
+
+# TASKS
+format:
+  extends:
+    - .pyenv
+    - .changes
+  stage: test
+  script:
+    - pyenv shell 3.6.4  # black install and run needs python 3.6.x minimum
+    - pip install -r requirements_dev.txt
+    - make check-format
+
+check:
+  extends:
+    - .pyenv
+    - .changes
+  stage: test
+  script:
+    - pyenv shell 3.6.4  # black install needs python 3.6.x minimum
+    - pip install -r requirements.txt
+    - pip install -r requirements_dev.txt
+    - make mypy
+    - make pylint
+
+build:
+  extends:
+    - .pyenv
+    - .changes
+  stage: build
+  script:
+    - pip install -r requirements.txt
+    - pip install -r requirements_deploy.txt
+    - make build
+
+release:
+  extends:
+    - .pyenv
+    - .push_to_github
   stage: release
   when: manual
   script:
     - pip install -r requirements.txt
-    - pip install wheel
-    - pip install twine
-    - python setup.py sdist bdist_wheel
-    - twine upload dist/* --username duniter --password $PYPI_PASSWORD
+    - pip install -r requirements_deploy.txt
+    - make build
+    - make deploy PYPI_LOGIN=${PYPI_LOGIN} PYPI_PASSWORD=${PYPI_PASSWORD}
+  only:
+    - tags
+    - master
+
+release_test:
+  extends: .pyenv
+  stage: release
+  when: manual
+  script:
+    - pip install -r requirements.txt
+    - pip install -r requirements_deploy.txt
+    - make build
+    - make deploy_test PYPI_TEST_LOGIN=${PYPI_TEST_LOGIN} PYPI_TEST_PASSWORD=${PYPI_TEST_PASSWORD}
+
diff --git a/mirage/__init__.py b/mirage/__init__.py
index 4b6f094e580474606c9e68cd3689a465fd50f8ea..b99dda008263d54c6b45afef738f61a5350b9b05 100644
--- a/mirage/__init__.py
+++ b/mirage/__init__.py
@@ -1,3 +1,5 @@
 from .node import Node
 from .user import User
 from .block_forge import BlockForge
+
+__version__ = "0.1.15"
diff --git a/release.sh b/release.sh
index d9dbbb7c937d1034ddc24d200bcc316b74e62766..79b51d644a6acff9294c61bba73a94ba4bf30b01 100755
--- a/release.sh
+++ b/release.sh
@@ -1,12 +1,17 @@
 #!/bin/bash
 
 #__version__     = '0.20.1dev9'
-current=`grep -P "version=\"\d+.\d+.\d+(\w*)\"" setup.py | grep -oP "\d+.\d+.\d+(\w*)"`
+current=`grep -P "__version__ = \"\d+.\d+.\d+(\w*)\"" mirage/__init__.py | grep -oP "\d+.\d+.\d+(\w*)"`
 echo "Current version: $current"
 
 if [[ $1 =~ ^[0-9]+.[0-9]+.[0-9]+[0-9a-z]*$ ]]; then
-  sed -i "s/version=\"$current\"/version=\"$1\"/g" setup.py
-  git commit setup.py -m "$1"
+  # update version in mirage
+  sed -i "s/__version__ = \"$current\"/__version__ = \"$1\"/g" mirage/__init__.py
+  # update version in setup.py
+  sed -i "s/version=\"$current\",/version=\"$1\",/" setup.py
+
+  # commit changes and add version tag
+  git commit setup.py mirage/__init__.py -m "$1"
   git tag "$1" -a -m "$1"
 else
   echo "Wrong version format"
diff --git a/requirements_deploy.txt b/requirements_deploy.txt
new file mode 100644
index 0000000000000000000000000000000000000000..ba996edcec3893f32b1aa660f95dc62a1ca4d249
--- /dev/null
+++ b/requirements_deploy.txt
@@ -0,0 +1,3 @@
+setuptools
+wheel
+twine
\ No newline at end of file