diff --git a/release/changelog_since_last_release.rst b/release/changelog_since_last_release.rst
index 73834874f532291d0bc045e469b975de7aa121cd..50cde5c2a26a04337934362db5f0c6f93e1e0c1b 100644
--- a/release/changelog_since_last_release.rst
+++ b/release/changelog_since_last_release.rst
@@ -178,12 +178,14 @@
   * minor
      * Migrate to conda based CI
 * bob.db.asvspoof
-  * v1.0.3 (Sep 22, 2017 14:20)
+  * v1.1.7 (Sep 22, 2017 14:20)
+     * Docs updates
   * minor
      * Migrate to conda based CI
-     * Removed redundant debug_asvspoof2017 script
+     * Removed redundant debug_asvspoof script
 * bob.db.asvspoof2017
   * v1.0.3 (Sep 22, 2017 14:20)
+     * Docs updates                                                                                                                                                              
   * minor
      * Migrate to conda based CI
      * Removed redundant debug_asvspoof2017 script
@@ -361,18 +363,40 @@
      * Created methods modality_separator and modalities
      * Included test case for objects.modality
 * bob.bio.base
-  * v3.3.0 (Sep 22, 2017 13:41)
-     * bob.bio.base!20 Docs url fix
-     * bob.bio.base!19 Python36 & docs fixes
-     * bob.bio.base!18 Removed `Algorithm.read_probe`
-  * major
-     * Add as_array to frame containers
-     * Make the original_directory and annotation_directory a property
-     * Fixed tuple indexing bug in youtube db load function and added tests
-     * Updated docs and tests
-     * Fixed bob.bio.base#11
-     * Added video annotator, updated documentation accordingly
+  * v3.2.0 (Sep 22, 2017 10:55)
+     * bob.bio.base!89 [ci] Fixed issue with matplotlib
+     * bob.bio.base!95 Removed class client from FileListDatabase
+     * bob.bio.base!94 Implemented the plots for the Detection Identification Rate (open-set identification)
+     * bob.bio.base!93 Corrections in the documentation
+     * bob.bio.base!92 Documented the configuration file input for the `verify.py` script
+     * bob.bio.base!90 Implemented a way to use, at the same time, 4 and 5 columns score file in the `evaluate.py` script
+     * bob.bio.base!88 Improved ROC and CMC plots
+     * bob.bio.base!87 Fixed * ZT files are processed even when no ZT processing is wanted
+     * bob.bio.base!81 Droped dependency on Latex
+     * bob.bio.base!86 Implemented five-column score file -* only during concatenation
+     * bob.bio.base!83 Improve suggestion on variable naming convention to match python's standard
+     * bob.bio.base!78 Removed `Algorithm.read_probe` method, since this is already solved via `bob.bio.base`
+     * bob.bio.base!82 Add a function to read features with generators
+  * v3.2.1 (Sep 26, 2017 15:04)
+     * Changed backend in `bob/bio/base/scripts/evaluate.py` to `'pdf'` instead of `'agg'`
+  * v4.2.1 (Mar 31, 2018 )
      * Migrate to conda based CI
+     * Updated docs and tests
+     * Added allow_missing_files option and added tests
+     * Removed write_commands function in grid_search (closes bob.bio.base#71)
+     * Added annotator, updated documentation accordingly
+     * Allow for comment lines in file-lists
+     * Improves verbosity for preprocessing, extraction and enrollment
+     * bob.db.base.Database is deprecated.
+     * Using config file loading mechanism from bob.extension
+     * Fix debug message (closes bob.bio.base#103)
+     * Fixed the exception that is raised when score file is not found
+     * Mentions bob.bio.vein on bob.bio.base docs (closes bob.bio.base#104)
+     * Added metadata in preprocessing, feature extraction, and algorithm
+     * Removed write_commands function in grid_search (closes bob.bio.base#71)
+     * Improve replace directories
+  * patch
+     * small docs building fix
 * bob.bio.gmm
   * v3.1.0 (Sep 22, 2017 14:05)
      * bob.bio.gmm!10 Removed Algorithm.read_probe method, since this is already solved via bob.bio.base
diff --git a/release/release_bob.py b/release/release_bob.py
new file mode 100755
index 0000000000000000000000000000000000000000..56068bd3c51e93d811a3a0d2016a2bed7e819afb
--- /dev/null
+++ b/release/release_bob.py
@@ -0,0 +1,318 @@
+#!/usr/bin/env python
+
+"""
+By using changelog file as an input (can be generated with 'generate_changelog.py' script),
+this script goes through all packages in changelog file (in order listed), tags them correctly
+as per the file, and releases them one by one. A script can also be used to release a single package.
+This script uses python-gitlab package for accessing GitLab's API.
+
+Usage:
+    {0} [-v...] [options] [--] <private_token>
+    {0} -h | --help
+    {0} --version
+
+Arguments:
+    <private_token>  Private token used to access GitLab.
+
+Options:
+    -h --help                     Show this screen.
+    --version                     Show version.
+    -c, --changelog-file STR      A changelog file with all packages to release with their tags, listed in order.
+                                  [default: changelog_since_last_release.rst].
+    -g, --group-name STR          Group name where we are assuming that all packages are located.
+                                  [default: bob].
+    -p, --package STR             If the name of a package is provided, then this package will be found
+                                  in the changelog file and the release will resume from it (if option --resume is set)
+                                  or only this package will be released.
+    -r, --resume                  The overall release will resume from the provided package name.
+    -q, --dry-run                 Only print the actions, but do not execute them.
+
+"""
+
+import sys
+import os
+from docopt import docopt
+import gitlab
+import datetime
+import re
+from distutils.version import StrictVersion as Version
+import numpy
+import time
+
+
+def _insure_correct_package(candidates, group_name, pkg_name):
+    for pkg in candidates:
+        # make sure the name and the group name match exactly
+        if pkg.name == pkg_name and pkg.namespace['name'] == group_name:
+            return pkg
+    raise ValueError('Package "{0}" was not found inside group "{1}"'.format(pkg_name, group_name))
+
+
+# adapted from the same-name function in new_version.py script of bob.extension package
+def _update_readme(readme, version=None):
+    # replace the travis badge in the README.rst with the given version
+    DOC_IMAGE = re.compile(r'\-(stable|(v\d+\.\d+\.\d+([abc]\d+)?))\-')
+    BRANCH_RE = re.compile(r'/(stable|master|(v\d+\.\d+\.\d+([abc]\d+)?))')
+
+    new_readme = []
+    for line in readme.splitlines():
+        if BRANCH_RE.search(line) is not None:
+            if "gitlab" in line:  # gitlab links
+                replacement = "/v%s" % version if version is not None else "/master"
+                line = BRANCH_RE.sub(replacement, line)
+            if "software/bob" in line:  # our doc server
+                if 'master' not in line:  # don't replace 'latest' pointer
+                    replacement = "/v%s" % version if version is not None \
+                        else "/stable"
+                    line = BRANCH_RE.sub(replacement, line)
+        if DOC_IMAGE.search(line) is not None:
+            replacement = '-v%s-' % version if version is not None else '-stable-'
+            line = DOC_IMAGE.sub(replacement, line)
+        new_readme.append(line)
+    return '\n'.join(new_readme)
+
+
+def get_parsed_tag(gitpkg, tag):
+    """
+        An older tag is formatted as 'v2.1.3 (Sep 22, 2017 10:37)', from which we need only v2.1.3
+        The latest tag is either patch, minor, major, or none
+    """
+    m = re.search(r"(v\d.\d.\d)", tag)
+    if m:
+        return m.group(0)
+    # tag = Version(tag)
+
+    # if we bump the version, we need to find the latest released version for this package
+    if 'patch' == tag or 'minor' == tag or 'major' == tag:
+        latest_tag = gitpkg.tags.list(per_page=1, page=1)
+        # if there were no tags yet, assume the first version
+        if not latest_tag:
+            return 'v1.0.0'
+        latest_tag = latest_tag[0]
+        latest_tag_name = latest_tag.name
+        # check that it has expected format v#.#.#
+        # latest_tag_name = Version(latest_tag_name)
+        m = re.match(r"(v\d.\d.\d)", latest_tag_name)
+        if not m:
+            raise ValueError(
+                'The latest tag name {0} in package {1} has unknown format'.format(latest_tag_name, gitpkg.name))
+        # increase the version accordingly
+        if 'major' == tag:  # increment the first number in 'v#.#.#'
+            return latest_tag_name[0] + str(int(latest_tag_name[1]) + 1) + latest_tag_name[2:]
+        if 'minor' == tag:  # increment the second number in 'v#.#.#'
+            return latest_tag_name[:3] + str(int(latest_tag_name[3]) + 1) + latest_tag_name[4:]
+        if 'patch' == tag:  # increment the last number in 'v#.#.#'
+            return latest_tag_name[:-1] + str(int(latest_tag_name[-1]) + 1)
+
+    if 'none' == tag:  # we do nothing in this case
+        return tag
+
+    raise ValueError('Cannot parse changelog tag {0} of the package {1}'.format(tag, gitpkg.name))
+
+
+def update_tag_comments(gitpkg, tag_name, tag_comments_list, dry_run=False):
+    # get tag and update its description
+    tag = gitpkg.tags.get(tag_name)
+    print('Found tag {1}, updating its comments with:'.format(gitpkg.name, tag.name))
+    print(tag_comments_list)
+    if not dry_run:
+        tag.set_release_description('\n'.join(tag_comments_list))
+    return tag
+
+
+def commit_files(gitpkg, files_list, message='Updated files', dry_run=False):
+    data = {
+        'branch': 'master',  # v4
+        'commit_message': message,
+        'actions': []
+    }
+    # add files to update
+    for filename in files_list.keys():
+        update_action = dict(action='update', file_path=filename)
+        # with open(filename, 'r') as f:
+        #     update_action['content'] = f.read()
+        update_action['content'] = files_list[filename]
+        data['actions'].append(update_action)
+
+    print("Committing changes in files: {0}".format(str(files_list.keys())))
+    if not dry_run:
+        gitpkg.commits.create(data)
+
+
+def get_last_nonskip_pipeline(gitpkg, before_last=False):
+    # sleep for 10 seconds to ensure that if a pipeline was just submitted,
+    # we can retrieve it
+    time.sleep(10)
+    if before_last:
+        # take the pipeline before the last
+        return gitpkg.pipelines.list(per_page=2, page=1)[1]
+    else:
+        # otherwise take the last pipeline
+        return gitpkg.pipelines.list(per_page=1, page=1)[0]
+
+
+def just_build_package(gitpkg, dry_run=False):
+    # we assume the last pipeline is with commit [skip ci]
+    # so, we take the pipeline that can be re-built, which the previous to the last one
+    last_pipeline = get_last_nonskip_pipeline(gitpkg, before_last=True)
+
+    # check that the chosen pipeline is the one we are looking for
+    latest_tag_name = gitpkg.tags.list(per_page=1, page=1)[0].name
+    # the pipeline should be the one built for the latest tag, so check if it is the correct choice
+    if last_pipeline.ref != latest_tag_name:
+        raise ValueError('While deploying {0}, found pipeline {1} but it does not match '
+                         'the latest tag {2}'.format(gitpkg.name, last_pipeline.id, latest_tag_name))
+    # the pipeline should have succeeded, otherwise we cannot release
+    if last_pipeline.status != 'success':
+        raise ValueError('While deploying {0}, found pipeline {1} but its status is "{2}" instead '
+                         'of the expected "sucess"'.format(gitpkg.name, last_pipeline.id, last_pipeline.status))
+
+    print("Retrying pipeline {0}".format(last_pipeline.id))
+    if not dry_run:
+        last_pipeline.retry()
+
+
+def wait_for_pipeline_to_finish(gitpkg, tag, dry_run=False):
+    sleep_step = 30
+    max_sleep = 60 * 60  # one hour
+    pipeline = get_last_nonskip_pipeline(gitpkg, before_last=True)
+
+    pipeline_id = pipeline.id
+
+    print('Waiting for the pipeline {0} of package {1} to finish. Do not interrupt.'.format(pipeline_id, gitpkg.name))
+
+    if dry_run:
+        return
+
+    # probe and wait for the pipeline to finish
+    slept_so_far = 0
+    while pipeline.status == 'running' or pipeline.status == 'pending':
+        time.sleep(sleep_step)
+        slept_so_far += sleep_step
+        if slept_so_far > max_sleep:
+            raise ValueError('I cannot wait longer than {0} seconds for '
+                             'pipeline {1} to finish running!'.format(max_sleep, pipeline_id))
+        # probe gitlab to update the status of the pipeline
+        pipeline = gitpkg.pipelines.get(pipeline_id)
+
+    # finished running, now check if it succeeded
+    if pipeline.status != 'success':
+        raise ValueError('Pipeline {0} of project {1} exited with undesired status "{2}". '
+                         'Release is not possible.'.format(pipeline_id, gitpkg.name, pipeline.status))
+
+    print('Pipeline {0} of package {1} succeeded. Continue processing.'.format(pipeline_id, gitpkg.name))
+
+
+def cancel_last_pipeline(gitpkg):
+    pipeline = get_last_nonskip_pipeline(gitpkg)
+    print('Cancelling the last pipeline {0} of project {1}'.format(pipeline.id, gitpkg.name))
+    pipeline.cancel()
+
+
+def release_package(gitpkg, tag_name, tag_comments_list, dry_run=False):
+    # if there is nothing to release, just rebuild the package
+    if tag_name == 'none':
+        print("Since the tag is 'none', we just re-build the last pipeline")
+        return just_build_package(gitpkg)
+
+    # 1. Replace branch tag in Readme to new tag, change version file to new version tag. Add and commit to gitlab
+    version_number = tag_name[1:]  # remove 'v' in front
+    readme_file = gitpkg.files.get(file_path='README.rst', ref='master')
+    readme_content = readme_file.decode().decode()
+    readme_content = _update_readme(readme_content, version=version_number)
+    # commit and push changes
+    commit_files(gitpkg, {'README.rst': readme_content, 'version.txt': version_number},
+                 'Increased stable version to %s' % version_number, dry_run)
+    if not dry_run:
+        # cancel running the pipeline triggered by the last commit
+        cancel_last_pipeline(gitpkg)
+
+    # 2. Tag package with new tag and push
+    print("Creating tag {}".format(tag_name))
+    print("updating tag's comments with:".format(gitpkg.name, tag_name))
+    print(tag_comments_list)
+    if not dry_run:
+        tag = gitpkg.tags.create({'tag_name': tag_name, 'ref': 'master'})
+        # update tag with comments
+        tag.set_release_description('\n'.join(tag_comments_list))
+
+    # 3. Replace branch tag in Readme to master, change version file to beta version tag. Git add, commit, and push.
+    readme_content = _update_readme(readme_content)
+    version_number += 'b0'
+    # commit and push changes
+    commit_files(gitpkg, {'README.rst': readme_content, 'version.txt': version_number},
+                 'Increased latest version to %s [skip ci]' % version_number, dry_run)
+
+
+def parse_and_process_package_changelog(gl, bob_group, pkg_name, package_changelog, dry_run=False):
+    cur_tag = None
+    cur_tag_comments = []
+
+    grpkg = _insure_correct_package(bob_group.projects.list(search=pkg_name), bob_group.name, pkg_name)
+    # so, we need to retrieve the full info from GitLab using correct project id
+    gitpkg = gl.projects.get(id=grpkg.id)
+
+    # we assume that changelog is formatted as structured text
+    # first line is the name of the package
+    for line in package_changelog:
+        if '  *' == line[:3]:  # a tag level
+            # write the comments collected for the previous tag
+            if cur_tag:
+                update_tag_comments(gitpkg, cur_tag, cur_tag_comments, dry_run)
+                cur_tag_comments = []  # reset comments
+
+            # parse the current tag name
+            cur_tag = get_parsed_tag(gitpkg, line[3:].strip())
+
+        else:  # all other lines are assumed to be comments
+            cur_tag_comments.append(line.strip())
+
+    # return the last tag and comments for release
+    return gitpkg, cur_tag, cur_tag_comments
+
+
+def main(private_token, group_name='bob', changelog_file='changelog.rst', dry_run=False, package=None, resume=False):
+    gl = gitlab.Gitlab('https://gitlab.idiap.ch', private_token=private_token, api_version=4)
+    bob_group = gl.groups.list(search=group_name)[0]
+
+    # traverse all packages in the changelog, edit older tags with updated comments,
+    # tag them with a suggested version, then try to release, and
+    # wait until done to proceed to the next package
+    with open(changelog_file) as f:
+        changelog = f.readlines()
+
+    # find the starts of each package's description in the changelog
+    pkgs = numpy.asarray([i for i, line in enumerate(changelog) if line[0] == '*'])
+    pkgs = numpy.concatenate([pkgs, [len(changelog)]])
+    start_idx = 0
+    if package:
+        # get the index where the package first appears in the list
+        start_idx = [i for i, line in enumerate(changelog) if line[1:].strip() == package]
+        if not start_idx:
+            print('Package {0} was not found in the changelog'.format(package))
+            return
+        start_idx = pkgs.tolist().index(start_idx[0])
+
+    for i in range(start_idx, pkgs.shape[0] - 1):
+        cur_package_name = changelog[pkgs[i]][1:].strip()
+        print('\nProcessing package {0}'.format(changelog[pkgs[i]]))
+        gitpkg, tag, tag_comments = parse_and_process_package_changelog(gl, bob_group, cur_package_name,
+                                                                        changelog[pkgs[i] + 1: pkgs[i + 1]], dry_run)
+        # release the package with the found tag and its comments
+        if gitpkg:
+            release_package(gitpkg, tag, tag_comments, dry_run)
+            # now, wait for the pipeline to finish, before we can release the next package
+            wait_for_pipeline_to_finish(gitpkg, tag, dry_run)
+
+        # if package name is provided and resume is not set, process only this package
+        if package == cur_package_name and not resume:
+            break
+
+    print('\nFinished processing changelog')
+
+
+if __name__ == '__main__':
+    arguments = docopt(__doc__.format(sys.argv[0]), version='Changelog 0.0.1')
+    main(arguments['<private_token>'], group_name=arguments['--group-name'],
+         changelog_file=arguments['--changelog-file'], dry_run=arguments['--dry-run'],
+         package=arguments['--package'], resume=arguments['--resume'])