diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..53e3ec6a87aab1cf0b6760885c27ade75b484109
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,45 @@
+---
+stages:
+  - test
+  - release
+
+pre-commit:
+  stage: test
+  image: adarnimrod/ci-images:pre-commit
+  variables: &variables
+    XDG_CACHE_HOME: "$CI_PROJECT_DIR/.cache"
+  script:
+    - pre-commit run -a
+  cache: &cache
+    paths:
+      - .cache/
+
+upload:
+  stage: release
+  image: python:3.6
+  before_script:
+    - pip install twine
+  script:
+    - mv "$pypirc" $HOME/.pypirc
+    - python setup.py bdist_wheel
+    - twine upload dist/*
+  variables: *variables
+  cache: *cache
+  rules:
+    - if: $CI_COMMIT_TAG
+  artifacts:
+    paths:
+      - dist/*.whl
+
+release:
+  stage: release
+  image: registry.gitlab.com/gitlab-org/release-cli:latest
+  script:
+    - !!str true
+  rules:
+    - if: $CI_COMMIT_TAG
+  release:
+    name: Release $CI_COMMIT_TAG
+    tag_name: $CI_COMMIT_TAG
+    ref: $CI_COMMIT_TAG
+    description: Release $CI_COMMIT_TAG
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index db64e81a537ad11df0bb9b6af51feba03365a8c4..4d22e874ff12cd5368cf3dc411678b73f1a04acb 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,10 +1,87 @@
--   repo: git://github.com/pre-commit/pre-commit-hooks
-    sha: v0.7.1
-    hooks:
-    -   id: check-added-large-files
-    -   id: check-json
-    -   id: check-xml
-    -   id: check-yaml
-    -   id: check-merge-conflict
-    -   id: flake8
-    -   id: check-symlinks
+# vim:ff=unix ts=2 sw=2 ai expandtab
+---
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v3.4.0
+    hooks:
+      - id: check-executables-have-shebangs
+      - id: check-merge-conflict
+      - id: trailing-whitespace
+
+  - repo: https://github.com/ambv/black
+    rev: 20.8b1
+    hooks:
+      - id: black
+        args:
+          - |
+              --line-length=79
+
+  - repo: https://github.com/Lucas-C/pre-commit-hooks-markup
+    rev: v1.0.1
+    hooks:
+      - id: rst-linter
+
+  - repo: https://github.com/myint/rstcheck.git
+    rev: master
+    hooks:
+      - id: rstcheck
+
+  - repo: https://github.com/adrienverge/yamllint
+    rev: v1.25.0
+    hooks:
+      - id: yamllint
+
+  - repo: https://github.com/amperser/proselint/
+    rev: 0.10.2
+    hooks:
+      - id: proselint
+        types: [plain-text]
+        exclude: LICENSE
+
+  - repo: https://github.com/PyCQA/prospector
+    rev: 1.3.1
+    hooks:
+      - id: prospector
+        args:
+          - |-
+            --max-line-length=79
+          - |-
+            --with-tool=pyroma
+          - |-
+            --with-tool=bandit
+          - |-
+            --without-tool=pep257
+          - |-
+            --doc-warnings
+          - |-
+            --test-warnings
+          - |-
+            --full-pep8
+          - |-
+            --strictness=high
+          - |-
+            --no-autodetect
+        additional_dependencies:
+          - bandit
+          - pyroma
+
+  - repo: https://gitlab.com/pycqa/flake8.git
+    rev: 3.8.4
+    hooks:
+      - id: flake8
+        args:
+          - |-
+            --doctests
+        additional_dependencies:
+          - flake8-bugbear
+
+
+  - repo: https://github.com/Yelp/detect-secrets
+    rev: v0.14.3
+    hooks:
+      - id: detect-secrets
+
+  - repo: https://github.com/mgedmin/check-manifest
+    rev: '0.45'
+    hooks:
+      - id: check-manifest
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index be0c4eab9c9e389760de0661704a714828d1608c..0000000000000000000000000000000000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,26 +0,0 @@
----
-language: python
-python: ["2.7", "3.3", "3.4", "3.5", "3.6"]
-dist: trusty
-sudo: false
-cache:
-  - pip
-matrix:
-  include:
-    - python: "3.5"
-      env: TOXENV=docs
-    - python: "2.7"
-      env: TOXENV=pre-commit
-    - python: "3.5"
-      env: TOXENV=pre-commit
-    - python: "3.5"
-      env: TOXENV=bandit
-
-install:
-    - pip install tox-travis | cat
-
-script:
-  - tox
-
-notifications:
-  email: false
diff --git a/MANIFEST.in b/MANIFEST.in
index 58a23e7ffc4ececd249b6cbf5908774764b7952b..a02ddecf9c3886c8ef1feda126a3276f95950ef5 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,6 @@
 recursive-include eb-prune *.py
 exclude .pre-commit-config.yaml
-exclude .travis.yml
+exclude .gitlab-ci.yml
 include *.rst
 include *.txt
 include VERSION
diff --git a/README.rst b/README.rst
index 8d3f0cc8beae5a35aaaa061902d47b34ba33c2f3..db782703e0229436b8d4f94cae680bfee24c6908 100644
--- a/README.rst
+++ b/README.rst
@@ -1,8 +1,8 @@
 eb-prune
 ########
 
-.. image:: https://travis-ci.org/adarnimrod/eb-prune.svg?branch=master
-    :target: https://travis-ci.org/adarnimrod/eb-prune
+.. image:: https://git.shore.co.il/nimrod/eb-prune/badges/master/pipeline.svg
+    :target: https://git.shore.co.il/nimrod/eb-prune/-/commits/master
 
 A CLI tool to prune old versions of Elastic Beanstalk.
 
@@ -42,16 +42,16 @@ file).
 Testing
 -------
 
-Tests require Python 2.7, Python 3.3 or later and Tox and are run by running
-:code:`tox`. Also, Travis CI is used to test on multiple Python versions for
-every push.
+Various linters are configured with `pre-commit <https://pre-commit.com/>`_.
+Those are run in CI in `GitLab
+<https://git.shore.co.il/nimrod/eb-prune/-/pipelines>`_. To run locally, install
+pre-commit and run it.
 
 Release
 -------
 
-Releases require Python 2.7 or Python 3.3 or later and Tox. To release a new
-version bump the version in the :code:`VERSION` file and run :code:`tox -e
-release`.
+Update the version in :code:`VERSION`, commit, tag and push. GitLab will do the
+rest.
 
 Author
 ------
@@ -59,10 +59,4 @@ Author
 Nimrod Adar, `contact me <nimrod@shore.co.il>`_ or visit my `website
 <https://www.shore.co.il/>`_. Patches are welcome via `git send-email
 <http://git-scm.com/book/en/v2/Git-Commands-Email>`_. The repository is located
-at: https://www.shore.co.il/git/.
-
-TODO
-----
-
-- Release to PyPI on tagged commits from Travis CI.
-- Add tests using moto.
+at: https://git.shore.co.il/explore.
diff --git a/eb_prune/__init__.py b/eb_prune/__init__.py
index 23512e4107edbd0be63228e61658659c8ef0b70d..061a4d8c6399f9630498b174969a80d231c28239 100644
--- a/eb_prune/__init__.py
+++ b/eb_prune/__init__.py
@@ -1,60 +1,75 @@
 #!/usr/bin/env python
-'''Prune older versions of an application in Elastic Beanstalk.'''
-from __future__ import (absolute_import, division, print_function,
-                        unicode_literals)
+"""Prune older versions of an application in Elastic Beanstalk."""
+from __future__ import (
+    absolute_import,
+    division,
+    print_function,
+    unicode_literals,
+)
 from argparse import ArgumentParser
-from botocore import session
+from botocore import session  # pylint: disable=import-error
 
 
 def prune(versions_to_keep, dry_run):
     if dry_run:
-        print('DRY RUN! NOTHING WILL BE REMOVED.')
-    print('Pruning Elastic Beanstalk versions.')
+        print("DRY RUN! NOTHING WILL BE REMOVED.")
+    print("Pruning Elastic Beanstalk versions.")
     aws_session = session.get_session()
-    beanstalk_client = aws_session.create_client('elasticbeanstalk')
+    beanstalk_client = aws_session.create_client("elasticbeanstalk")
     response = beanstalk_client.describe_application_versions()
-    if response['ResponseMetadata']['HTTPStatusCode'] != 200:
-        raise RuntimeError('Failed to describe application versions.')
+    if response["ResponseMetadata"]["HTTPStatusCode"] != 200:
+        raise RuntimeError("Failed to describe application versions.")
     # Get all EB versions.
-    versions = response['ApplicationVersions']
+    versions = response["ApplicationVersions"]
     response = beanstalk_client.describe_environments()
-    if response['ResponseMetadata']['HTTPStatusCode'] != 200:
-        raise RuntimeError('Failed to describe environments.')
+    if response["ResponseMetadata"]["HTTPStatusCode"] != 200:
+        raise RuntimeError("Failed to describe environments.")
     # Remove the currently in-use versions from the list.
-    active_versions = [env['VersionLabel'] for env in response['Environments']]
+    active_versions = [env["VersionLabel"] for env in response["Environments"]]
     previous_versions = filter(
-        lambda x: (not x['VersionLabel'] in active_versions) and
-        x['Status'] == 'UNPROCESSED', versions)
+        lambda x: (not x["VersionLabel"] in active_versions)
+        and x["Status"] == "UNPROCESSED",
+        versions,
+    )
     # Remove the newest versions from the list.
     old_versions = sorted(
-        previous_versions,
-        key=lambda x: x.get('DateCreated'))[:-versions_to_keep]
+        previous_versions, key=lambda x: x.get("DateCreated")
+    )[:-versions_to_keep]
     for version in old_versions:
         if not dry_run:
             response = beanstalk_client.delete_application_version(
-                ApplicationName=version['ApplicationName'],
-                VersionLabel=version['VersionLabel'],
-                DeleteSourceBundle=True)
-            if response['ResponseMetadata']['HTTPStatusCode'] != 200:
-                raise RuntimeError('Failed to delete version {0}.'.format(
-                    version['VersionLabel']))
-        print('Deleted version {0} of {1}.'.format(version['VersionLabel'],
-                                                   version['ApplicationName']))
-    print('Deleted {0} versions.'.format(len(old_versions)))
+                ApplicationName=version["ApplicationName"],
+                VersionLabel=version["VersionLabel"],
+                DeleteSourceBundle=True,
+            )
+            if response["ResponseMetadata"]["HTTPStatusCode"] != 200:
+                raise RuntimeError(
+                    "Failed to delete version {0}.".format(
+                        version["VersionLabel"]
+                    )
+                )
+        print(
+            "Deleted version {0} of {1}.".format(
+                version["VersionLabel"], version["ApplicationName"]
+            )
+        )
+    print("Deleted {0} versions.".format(len(old_versions)))
 
 
 def main():
     parser = ArgumentParser()
-    parser.add_argument('versions_to_keep',
-                        help='The number of versions to keep.',
-                        type=int)
-    parser.add_argument('-d',
-                        '--dry-run',
-                        help='Dry run, do not delete versions.',
-                        action='store_true')
+    parser.add_argument(
+        "versions_to_keep", help="The number of versions to keep.", type=int
+    )
+    parser.add_argument(
+        "-d",
+        "--dry-run",
+        help="Dry run, do not delete versions.",
+        action="store_true",
+    )
     args = parser.parse_args()
     prune(args.versions_to_keep, args.dry_run)
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     main()
diff --git a/setup.py b/setup.py
index 3bfc90f6b2973e1ef24b945362406de36b89ab69..2df357a75a84443056a2bc23200d146797d6b823 100644
--- a/setup.py
+++ b/setup.py
@@ -2,28 +2,33 @@
 from setuptools import setup, find_packages
 
 setup(
-    name='eb-prune',
-    version=open('VERSION', 'r').read(),
-    description='Pruning of Elastic Beanstalk versions.',
-    long_description=open('README.rst', 'r').read(),
-    url='https://www.shore.co.il/git/eb-prune',
-    author='Nimrod Adar',
-    author_email='nimrod@shore.co.il',
-    license='MIT',
+    name="eb-prune",
+    version=open("VERSION", "r").read(),
+    description="Pruning of Elastic Beanstalk versions.",
+    long_description=open("README.rst", "r").read(),
+    url="https://www.shore.co.il/git/eb-prune",
+    author="Nimrod Adar",
+    author_email="nimrod@shore.co.il",
+    license="License :: OSI Approved :: MIT License",
     classifiers=[
-        'Development Status :: 4 - Beta',
-        'Intended Audience :: Developers',
-        'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 2',
-        'Intended Audience :: System Administrators',
-        'Topic :: Utilities',
+        "Development Status :: 4 - Beta",
+        "Intended Audience :: Developers",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 2",
+        "Intended Audience :: System Administrators",
+        "Topic :: Utilities",
     ],
-    keywords='beanstalk AWS',
+    keywords="beanstalk AWS",
     packages=find_packages(),
-    install_requires=['botocore'],
+    install_requires=["botocore"],
     extras_require={
-        'dev': ['tox'], },
+        "dev": ["tox"],
+    },
     entry_points={
-        'console_scripts': [
-            'eb-prune=eb_prune:main'], },
+        "console_scripts": ["eb-prune=eb_prune:main"],
+    },
 )
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index 412bd05df6b8c526dd555f370d490619dcfaa437..0000000000000000000000000000000000000000
--- a/tox.ini
+++ /dev/null
@@ -1,49 +0,0 @@
-[tox]
-envlist = py{2,3}
-
-[travis]
-python =
-    2.7: py2
-    3.3: py3
-    3.4: py3
-    3.5: py3
-    3.6: py3
-
-[testenv]
-basepython =
-    py2: python2
-    py3: python3
-deps =
-    check-manifest
-    flake8
-commands =
-    check-manifest --ignore tox.ini,tests*
-    flake8 .
-    eb-prune --help
-
-[testenv:docs]
-basepython = python
-deps = readme_renderer
-commands = python setup.py check -m -r -s
-
-[testenv:release]
-basepython = python
-whitelist_externals =
-    sh
-deps =
-    twine
-    wheel
-commands =
-    sh -c 'git tag "$(cat VERSION)" && git push --tags'
-    python setup.py bdist_wheel
-    twine upload --skip-existing dist/*.whl
-
-[testenv:bandit]
-basepython = python
-deps = bandit
-commands = bandit --recursive ./ --exclude .tox/,build/,dist/,eb-prune.egg-info
-
-[testenv:pre-commit]
-basepython = python
-deps = pre-commit
-commands = pre-commit run --all-files