diff --git a/.gitignore b/.gitignore
index 67956823a7615f6f9a6493ee8911a2769b9ab309..f95eabb39795d2dfd0a85885d3baf2f59f410b23 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,4 +49,5 @@ tags
 # Python
 build/
 dist/
+venv/
 *.egg-info/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 152dd89eb3d087f998acee5b147f60bf09a04c1e..5e7fda9eb6bbfffb6063a2564a9eafa408a5373d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -3,6 +3,7 @@
 image: python:3.7-slim-stretch
 
 stages:
+  - test
   - build
   - deploy
 
@@ -10,17 +11,106 @@ variables:
   package: gitlab-job-guard
 
 before_script:
-  - env | sort | egrep -i 'CI|GIT'
+  - env | sort |
+    perl -pe 's/((?:PASSWORD|TOKEN)=)(.*)/$1 . q[X] x length($2)/e'
   - pwd; pwd -P
-  - python -m pip install --upgrade setuptools wheel twine
+  - python -m pip install --upgrade pip virtualenv
+  - virtualenv -p python venv
+  - . venv/bin/activate
+  - python -m pip install --upgrade pip setuptools virtualenv wheel
+  - echo '.' > requirements.txt
   - pip install -e ./
-  - pip freeze > requirements.txt
+  - pip freeze -l --exclude-editable > requirements.txt
   - awk -F"'" '
       /version/ { $0=$1"'"'"$CI_COMMIT_REF_NAME"'"'"$3 }{ print $0 }
     ' setup.py > setup.py.tmp; mv -v setup.py.tmp setup.py
   - chmod +x ./setup.py
-  - ./setup.py sdist bdist_wheel
-  - find */ -type f -ls -a -exec sha256sum {} +
+  - python ./setup.py sdist bdist_wheel
+  - pip install dist/gitlab-job-guard*.tar.gz
+
+'test-py2':
+  stage: test
+  image: python:2.7-slim-stretch
+  script:
+    - python ./setup.py sdist bdist_wheel
+    - pip install dist/gitlab-job-guard*.tar.gz
+    - python2 $(which gitlab-job-guard) -c='feature/' -s 'running' -w 60;
+
+'test-py3':
+  stage: test
+  image: python:3.7-slim-stretch
+  script:
+    - python3 $(which gitlab-job-guard) -c='feature/' -s 'running' -w 60;
+
+'test-py3-exit-immediately':
+  stage: test
+  image: python:3.7-slim-stretch
+  script:
+    - ( set +e; gitlab-job-guard -c='.+' -s='.+' -x; [ $? = 7 ] )
+
+'test-py3-retry-until-timeout':
+  stage: test
+  image: python:3.7-slim-stretch
+  script:
+    - ( set +e; gitlab-job-guard -c='.+' -s='.+' -w 60; [ $? = 11 ] )
+
+'test-py3-retry-failed-until-timeout':
+  stage: test
+  image: python:3.7-slim-stretch
+  script:
+    - ( set +e;
+        gitlab-job-guard -c='.+' -s='failed|skipped' -w 60;
+        [ $? = 11 ]
+      )
+
+'test-py3-retry-feature-failed-until-timeout':
+  stage: test
+  image: python:3.7-slim-stretch
+  script:
+    - ( set +e;
+        gitlab-job-guard -c='feature/' -s='failed|skipped' -w 60;
+        [ $? = 11 ]
+      )
+
+'test-py3-unreachable-api-endpoint':
+  stage: test
+  image: python:3.7-slim-stretch
+  script:
+    - ( set +e;
+        CI_API_V4_URL='https://gitlab.local/api/v4/'
+        gitlab-job-guard -c='.+' -s='.+' -w 60;
+        [ $? = 11 ]
+      )
+
+'test-py3-invalid-api-url':
+  stage: test
+  image: python:3.7-slim-stretch
+  script:
+    - ( set +e;
+        CI_API_V4_URL="$CI_API_V4_URL/blah-blah-blah"
+        gitlab-job-guard -c='.+' -s='.+' -w 60;
+        [ $? = 11 ]
+      )
+
+'test-py3-invalid-project-id':
+  stage: test
+  image: python:3.7-slim-stretch
+  script:
+    - ( set +e;
+        CI_PROJECT_ID='123123123'
+        gitlab-job-guard -c='.+' -s='.+' -w 60;
+        [ $? = 11 ]
+      )
+
+'test-py3-invalid-token':
+  stage: test
+  image: python:3.7-slim-stretch
+  script:
+    - ( set +e;
+        PRIVATE_TOKEN='abracadabra'
+        gitlab-job-guard -c='.+' -s='.+' -w 60;
+        [ $? = 11 ]
+      )
 
 'build-package':
   stage: build
@@ -37,7 +127,9 @@ before_script:
   script:
     - echo "Deploying package to artifactory"
     - echo "Deploying $package@$CI_COMMIT_REF_NAME to $af_repo_url as $af_username ..."
+    - python -m pip install --upgrade twine
     - cat setup.py
+    - twine check dist/*
     - twine upload
         --repository-url "$af_url"
         --username       "$af_user"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb7f6b1af6868dc9e5da009d626e24107dbeff51..8eb515dd3eb44815bca33d5f1fff026b6dc4e9fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+v0.0.2 - 2019-04-16T18:29:44
+----------------------------
+
+* Add README.md
+* Add e2e test suite
+* Fix up CI build issues
+
 v0.0.1 - 2019-04-15T20:17:13
 ----------------------------
 
diff --git a/README.md b/README.md
index 232be412a7de068982e64dbee6a9ae0e0299901b..5b02dc19f79b41c8af2422324e3905526c02b0f2 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,118 @@
+[![pipeline status](https://gitlab.com/s.bhooshi/gitlab-job-guard/badges/master/pipeline.svg)](https://gitlab.com/s.bhooshi/gitlab-job-guard/commits/master)
+
 # gitlab-job-guard
 
-Guard pipeline jobs from multiple simultaneous executions
\ No newline at end of file
+Guard pipeline jobs from multiple simultaneous executions
+
+```bash
+$ PRIVATE_TOKEN="$GITLAB_API_TOKEN" gitlab-job-guard -w 3600
+$ my-unguarded-deployment-task --to=production
+```
+
+`gitlab-job-guard`  will block  if  it detects  other  pipelines running  for
+the  current  project   to  avoid  multiple  pipelines  from   clobbering  up  a
+deployment/environment.
+
+While gitlab will _auto-cancel redundant, pending pipelines_ for the same branch
+by  default -  this  is not  the  case for  multiple  pipelines from  _different
+branches_ targeting  a particular deployment/environment.  Gitlab has no  way to
+detect  or control  these user-defined  branch-to-environment mappings  and this
+means  environments  can  easily  be  left  in  an  unsafe/broken  state.  (e.g.
+`terraform apply` or `ansible`, etc from different pipelines running at the same
+time).
+
+`gitlab-job-guard` uses the Gitlab API to determine if existing pipelines are
+scheduled and to backoff-and-retry  until it is safe to proceed. Conflicts
+are detected  by user-defined matches on  pipeline ref names (branch,  tag, etc)
+and/or pipeline status.
+
+## Usage
+
+The  simplest  usage   would  likely  be  placing   `gitlab-job-guard`  in  a
+`before_script` section in your `gitlab-ci.yml` to protect all jobs (though this
+can slow things down).
+
+```yaml
+before_script:
+  - PRIVATE_TOKEN="$GITLAB_API_TOKEN" gitlab-job-guard
+```
+
+Though often,  this is only  needed to guard  jobs that share  common state/data
+(i.e. a deployment environment, an artifact build/release, etc).
+
+```yaml
+deploy-production:
+  stage: deploy
+  script:
+    - PRIVATE_TOKEN="$GITLAB_API_TOKEN" gitlab-job-guard
+    - my-unguarded-deployment-task --to=production
+```
+
+or to guard something like a `terraform` job running for tags.
+
+```yaml
+provision-infrastructure:
+  stage: provision
+  script:
+    - export PRIVATE_TOKEN="$GITLAB_API_TOKEN"
+    - gitlab-job-guard --guard-ref-regex='^v[0-9\.]+'  # Regex matches tags
+    - terraform plan  ...
+    - terraform apply ...
+  only:
+    - tags
+```
+
+### Other usages
+
+To hold jobs for a collisions on pattern matches on the ref/branch name.
+
+```bash
+gitlab-job-guard -c=^master$                # Match branch names matching 'master' exactly
+
+gitlab-job-guard -c=^(master|dev(elop)?)$   # Match any of the mainline branches
+
+gitlab-job-guard -c=^(feature|release|hotfix)/  # Match any gitflow transient branch prefixes
+
+gitlab-job-guard -c=^[0-9]\-                # Match branch names beginning with a number
+                                            # and dash ignoring all other text.
+                                            # e.g. a gitlab branch made from an issue
+
+gitlab-job-guard -c=^v?[\d.]+$              # Match (semver) tags like v1.0.9, 2.0
+
+gitlab-job-guard -c=^environment/           # Match any environment deployments?
+
+gitlab-job-guard -c=^environment/dc1.+      # Match environment deployments to DC1?
+
+gitlab-job-guard -c="$CI_BUILD_REF_NAME"    # Match current branch name (partially).
+                                            # i.e. 'master' matches 'feature/master-document'
+
+gitlab-job-guard -c="^$CI_BUILD_REF_NAME$"  # Match current branch name (exactly).
+                                            # i.e. 'master' does not match 'master-deployment'
+
+gitlab-job-guard -c='.+' -s='running|pending'  # Match any pipeline in running or pending state
+```
+
+To hold a job for a collision on part of the ref name (e.g. on branch prefix
+such as `feature/` or `hotfix/` or `release/`, etc _a la_ `gitflow`).
+
+```bash
+# Assuming CI_BUILD_REF_NAME=feature/foo
+
+CI_BUILD_REF_PREFIX=$(echo "$CI_BUILD_REF_NAME" | sed -r 's@(.+/)(.+)@\1@')
+# CI_BUILD_REF_PREFIX now contains 'feature/'
+
+gitlab-job-guard -c="^$CI_BUILD_REF_PREFIX" -s='running|pending'
+```
+
+# TODO
+
+For long pipelines, this solution can have subtle consequences with growing
+queues and increased contention and unpredictability as to which pipeline is
+the first-past-the-post. An older pipeline taking precedence over newer commits
+if often not desired and newer pipelines always winning is probably desired.
+
+* Handle existing conflicting pipelines - cancel them or give-way.
+* Narrow down conflicts to jobs (`CI_JOB_NAME`) or stages (`CI_JOB_STAGE`)
+  so that other parts of the pipelines that do not share state are allowed to
+  run freely.
+
diff --git a/gitlab-job-guard/gitlab-job-guard b/gitlab-job-guard/gitlab-job-guard
new file mode 120000
index 0000000000000000000000000000000000000000..27579279f4481b29ce4001b8372d609e8df81ca5
--- /dev/null
+++ b/gitlab-job-guard/gitlab-job-guard
@@ -0,0 +1 @@
+gitlab-job-guard.py
\ No newline at end of file
diff --git a/gitlab-job-guard/guard.py b/gitlab-job-guard/gitlab-job-guard.py
similarity index 93%
rename from gitlab-job-guard/guard.py
rename to gitlab-job-guard/gitlab-job-guard.py
index c96d96178f229f07c86c6277ea71de0b359a14a0..b5e4f497d5bd5f8680b8e81643762eb36605f477 100755
--- a/gitlab-job-guard/guard.py
+++ b/gitlab-job-guard/gitlab-job-guard.py
@@ -2,7 +2,8 @@
 
 # -*- coding: utf-8 -*-
 
-# guard.py - guard pipeline jobs from multiple simultaneous executions
+# gitlab-job-guard
+# guard pipeline jobs from multiple simultaneous executions
 
 from __future__ import absolute_import, division, print_function
 
@@ -15,6 +16,7 @@ except ImportError:
   import simplejson as json
 import logging
 from os        import environ, path
+from os.path   import basename
 from posixpath import join as urljoin
 from random    import randint, random
 import re
@@ -87,7 +89,7 @@ def setup_logger(*args, **kwargs):
     '''
     Setup and return the root logger ojbect for the application
     '''
-    root = logging.getLogger(__file__)
+    root = logging.getLogger(basename(__file__))
     root.setLevel(logging.DEBUG)
 
     handler = logging.StreamHandler(sys.stdout)
@@ -103,6 +105,13 @@ def setup_logger(*args, **kwargs):
 def print_unbuffered(*args, **kwargs):
     '''
     Workaround for python3 where stderr is buffered. WTH python?
+    TODO: We could use python -u or set PYTHONUNBUFFERED in the environment but
+          those are both settings made outside of this script and also requires
+          users to be explicit in how they call it. We need an elegant way that
+          does not impose on the user and always does the right thing.
+          Fix this once python2 is fully deprecated and we only have to support
+          python3 - we may be able to set the shebang to something like
+          #!/usr/local/bin/python3 -u
     '''
     if PY3:
         kwargs['flush'] = True
@@ -261,7 +270,7 @@ def main():
             conflicts = [ p for p in runs if
                             re.search( args.guard_ref_regex,    p.ref    ) and
                             re.match(  args.guard_status_regex, p.status ) and
-                            p.id != args.ci_pipeline_id ]
+                            int(p.id) != int(args.ci_pipeline_id) ]
 
         except Exception as e:
             log.error('{}("{}")'.format(e.__class__.__name__, str(e)))
diff --git a/setup.py b/setup.py
index 12710500342e057dc70962e851eafff333633ece..3c5be66173fc562e0aee984df7a7d8ce9bec3124 100755
--- a/setup.py
+++ b/setup.py
@@ -14,18 +14,30 @@ requests
 six
 '''.split('\n')
 
+
+# read the contents of your README file
+from os import path
+this_directory = path.abspath(path.dirname(__file__))
+with open(path.join(this_directory, 'README.md')) as f:
+    long_description = f.read()
+
 setup(  name='gitlab-job-guard',
         version='v0.0.1',
         description="Guard gitlab jobs from multiple simultaneous executions",
+        long_description=long_description,
+        long_description_content_type="text/markdown",
         url='https://gitlab.com/s.bhooshi/gitlab-job-guard',
         author='Shalom Bhooshi',
         author_email='s.bhooshi@gmail.com',
         license='Apache License 2.0',
         packages=['gitlab-job-guard'],
         zip_safe=False,
-        scripts=['gitlab-job-guard/guard.py'],
+        scripts=[
+                'gitlab-job-guard/gitlab-job-guard.py',
+                'gitlab-job-guard/gitlab-job-guard'
+            ],
         install_requires=requirements,
         keywords='gitlab-ci pipeline job guard',
-        python_requires='>=2.7, >=3.5, !=3.0, !=3.0.*, !=3.1, !=3.1.*, !=3.2, !=3.2.*, !=3.3, !=3.3.*, !=3.4, !=3.4.*',
+        python_requires='>=2.7, >=2.7.1, !=3.0, !=3.0.*, !=3.1, !=3.1.*, !=3.2, !=3.2.*, !=3.3, !=3.3.*, !=3.4, !=3.4.*',
     )