diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45cba0516..b791c1e08 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,7 @@
 Change log
 ==========
 
-1.22.0 (2018-06-30)
+1.22.0 (2018-07-17)
 -------------------
 
 ### Features
diff --git a/compose/__init__.py b/compose/__init__.py
index a76ca8177..10ae3675f 100644
--- a/compose/__init__.py
+++ b/compose/__init__.py
@@ -1,4 +1,4 @@
 from __future__ import absolute_import
 from __future__ import unicode_literals
 
-__version__ = '1.22.0-rc2'
+__version__ = '1.22.0'
diff --git a/script/release/release.py b/script/release/release.py
index d0545a7e6..23b93a528 100755
--- a/script/release/release.py
+++ b/script/release/release.py
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
 
 import argparse
 import os
+import shutil
 import sys
 import time
 from distutils.core import run_setup
@@ -27,6 +28,7 @@ from release.utils import ScriptError
 from release.utils import update_init_py_version
 from release.utils import update_run_sh_version
 from release.utils import yesno
+from requests.exceptions import HTTPError
 from twine.commands.upload import main as twine_upload
 
 
@@ -125,13 +127,60 @@ def print_final_instructions(args):
         "You're almost done! Please verify that everything is in order and "
         "you are ready to make the release public, then run the following "
         "command:\n{exe} -b {user} finalize {version}".format(
-            exe=sys.argv[0], user=args.bintray_user, version=args.release
+            exe='./script/release/release.sh', user=args.bintray_user, version=args.release
         )
     )
 
 
+def distclean():
+    print('Running distclean...')
+    dirs = [
+        os.path.join(REPO_ROOT, 'build'), os.path.join(REPO_ROOT, 'dist'),
+        os.path.join(REPO_ROOT, 'docker-compose.egg-info')
+    ]
+    files = []
+    for base, dirnames, fnames in os.walk(REPO_ROOT):
+        for fname in fnames:
+            path = os.path.normpath(os.path.join(base, fname))
+            if fname.endswith('.pyc'):
+                files.append(path)
+            elif fname.startswith('.coverage.'):
+                files.append(path)
+        for dirname in dirnames:
+            path = os.path.normpath(os.path.join(base, dirname))
+            if dirname == '__pycache__':
+                dirs.append(path)
+            elif dirname == '.coverage-binfiles':
+                dirs.append(path)
+
+    for file in files:
+        os.unlink(file)
+
+    for folder in dirs:
+        shutil.rmtree(folder, ignore_errors=True)
+
+
+def pypi_upload(args):
+    print('Uploading to PyPi')
+    try:
+        twine_upload([
+            'dist/docker_compose-{}*.whl'.format(args.release),
+            'dist/docker-compose-{}*.tar.gz'.format(args.release)
+        ])
+    except HTTPError as e:
+        if e.response.status_code == 400 and 'File already exists' in e.message:
+            if not args.finalize_resume:
+                raise ScriptError(
+                    'Package already uploaded on PyPi.'
+                )
+            print('Skipping PyPi upload - package already uploaded')
+        else:
+            raise ScriptError('Unexpected HTTP error uploading package to PyPi: {}'.format(e))
+
+
 def resume(args):
     try:
+        distclean()
         repository = Repository(REPO_ROOT, args.repo)
         br_name = branch_name(args.release)
         if not repository.branch_exists(br_name):
@@ -183,6 +232,7 @@ def cancel(args):
         bintray_api = BintrayAPI(os.environ['BINTRAY_TOKEN'], args.bintray_user)
         print('Removing Bintray data repository for {}'.format(args.release))
         bintray_api.delete_repository(args.bintray_org, branch_name(args.release))
+        distclean()
     except ScriptError as e:
         print(e)
         return 1
@@ -191,6 +241,7 @@ def cancel(args):
 
 
 def start(args):
+    distclean()
     try:
         repository = Repository(REPO_ROOT, args.repo)
         create_initial_branch(repository, args)
@@ -213,6 +264,7 @@ def start(args):
 
 
 def finalize(args):
+    distclean()
     try:
         repository = Repository(REPO_ROOT, args.repo)
         img_manager = ImageManager(args.release)
@@ -238,10 +290,13 @@ def finalize(args):
         run_setup(os.path.join(REPO_ROOT, 'setup.py'), script_args=['sdist', 'bdist_wheel'])
 
         merge_status = pr_data.merge()
-        if not merge_status.merged:
-            raise ScriptError('Unable to merge PR #{}: {}'.format(pr_data.number, merge_status.message))
-        print('Uploading to PyPi')
-        twine_upload(['dist/*'])
+        if not merge_status.merged and not args.finalize_resume:
+            raise ScriptError(
+                'Unable to merge PR #{}: {}'.format(pr_data.number, merge_status.message)
+            )
+
+        pypi_upload(args)
+
         img_manager.push_images()
         repository.publish_release(gh_release)
     except ScriptError as e:
@@ -260,13 +315,13 @@ ACTIONS = [
 
 EPILOG = '''Example uses:
     * Start a new feature release (includes all changes currently in master)
-        release.py -b user start 1.23.0
+        release.sh -b user start 1.23.0
     * Start a new patch release
-        release.py -b user --patch 1.21.0 start 1.21.1
+        release.sh -b user --patch 1.21.0 start 1.21.1
     * Cancel / rollback an existing release draft
-        release.py -b user cancel 1.23.0
+        release.sh -b user cancel 1.23.0
     * Restart a previously aborted patch release
-        release.py -b user -p 1.21.0 resume 1.21.1
+        release.sh -b user -p 1.21.0 resume 1.21.1
 '''
 
 
@@ -316,6 +371,10 @@ def main():
         '--skip-ci-checks', dest='skip_ci', action='store_true',
         help='If set, the program will not wait for CI jobs to complete'
     )
+    parser.add_argument(
+        '--finalize-resume', dest='finalize_resume', action='store_true',
+        help='If set, finalize will continue through steps that have already been completed.'
+    )
     args = parser.parse_args()
 
     if args.action == 'start':
diff --git a/script/release/release/images.py b/script/release/release/images.py
index 24672f2ba..b8f7ed3d6 100644
--- a/script/release/release/images.py
+++ b/script/release/release/images.py
@@ -81,3 +81,7 @@ class ImageManager(object):
             for chunk in logstream:
                 if 'status' in chunk:
                     print(chunk['status'])
+                if 'error' in chunk:
+                    raise ScriptError(
+                        'Error pushing {name}: {err}'.format(name=name, err=chunk['error'])
+                    )
diff --git a/script/run/run.sh b/script/run/run.sh
index 7f4acb765..52ff9513f 100755
--- a/script/run/run.sh
+++ b/script/run/run.sh
@@ -15,7 +15,7 @@
 
 set -e
 
-VERSION="1.22.0-rc2"
+VERSION="1.22.0"
 IMAGE="docker/compose:$VERSION"