-
Notifications
You must be signed in to change notification settings - Fork 3
/
deploy.py
135 lines (103 loc) · 5.05 KB
/
deploy.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#!/usr/bin/env python3
"""
Deploys the package to PyPI and uploads docs. This script is called in circle.yml.
"""
import os
import shlex
import subprocess
import sys
CIRCLECI_ENV_VAR = 'CIRCLECI'
def _shell(cmd, check=True, stdin=None, stdout=None, stderr=None): # pragma: no cover
"""Runs a subprocess shell with check=True by default"""
return subprocess.run(cmd, shell=True, check=check, stdin=stdin, stdout=stdout, stderr=stderr)
def _pypi_push(dist):
"""Push created package to PyPI.
Requires the following defined environment variables:
- TWINE_USERNAME: The PyPI username to upload this package under
- TWINE_PASSWORD: The password to the user's account
Args:
dist (str):
The distribution to push. Must be a valid directory; shell globs are
NOT allowed.
"""
# Register all distributions and wheels with PyPI. We have to list the dist
# directory and register each file individually because `twine` doesn't
# handle globs.
for filename in os.listdir(dist):
full_path = os.path.join(dist, filename)
if os.path.isfile(full_path):
# This will fail if the project has never been uploaded, so use check=false
_shell('twine register ' + shlex.quote(full_path), check=False)
_shell('twine upload ' + shlex.quote(dist + '/*'))
def deploy(target):
"""Deploys the package and documentation.
Proceeds in the following steps:
1. Ensures proper environment variables are set and checks that we are on Circle CI
2. Tags the repository with the new version
3. Creates a standard distribution and a wheel
4. Updates version.py to have the proper version
5. Commits the ChangeLog, AUTHORS, and version.py file
6. Pushes to PyPI
7. Pushes the tags and newly committed files
Raises:
`EnvironmentError`:
- Not running on CircleCI
- `*_PYPI_USERNAME` and/or `*_PYPI_PASSWORD` environment variables
are missing
- Attempting to deploy to production from a branch that isn't master
"""
# Ensure proper environment
if not os.getenv(CIRCLECI_ENV_VAR): # pragma: no cover
raise EnvironmentError('Must be on CircleCI to run this script')
current_branch = os.getenv('CIRCLE_BRANCH')
if (target == 'PROD') and (current_branch != 'master'):
raise EnvironmentError((
'Refusing to deploy to production from branch {current_branch!r}. '
'Production deploys can only be made from master.'
).format(current_branch=current_branch))
if target in ('PROD', 'TEST'):
pypi_username = os.getenv('{target}_PYPI_USERNAME'.format(target=target))
pypi_password = os.getenv('{target}_PYPI_PASSWORD'.format(target=target))
else:
raise ValueError(
"Deploy target must be 'PROD' or 'TEST', got {target!r}.".format(target=target))
if not (pypi_username and pypi_password): # pragma: no cover
raise EnvironmentError((
"Missing '{target}_PYPI_USERNAME' and/or '{target}_PYPI_PASSWORD' "
"environment variables. These are required to push to PyPI."
).format(target=target))
# Twine requires these environment variables to be set. Subprocesses will
# inherit these when we invoke them, so no need to pass them on the command
# line. We want to avoid that in case something's logging each command run.
os.environ['TWINE_USERNAME'] = pypi_username
os.environ['TWINE_PASSWORD'] = pypi_password
# Set up git on circle to push to the current branch
_shell('git config --global user.email "[email protected]"')
_shell('git config --global user.name "Circle CI"')
_shell('git config push.default current')
# Obtain the version to deploy
ret = _shell('make version', stdout=subprocess.PIPE)
version = ret.stdout.decode('utf-8').strip()
print('Deploying version {version!r}...'.format(version=version))
# Tag the version
_shell('git tag -f -a {version} -m "Version {version}"'.format(version=version))
# Update the version
_shell(
'sed -i.bak "s/^__version__ = .*/__version__ = {version!r}/" */version.py'.format(
version=version))
# Create a standard distribution and a wheel
_shell('python setup.py sdist bdist_wheel')
# Add the updated ChangeLog and AUTHORS
_shell('git add ChangeLog AUTHORS */version.py')
# Start the commit message with "Merge" so that PBR will ignore it in the
# ChangeLog. Use [skip ci] to ensure CircleCI doesn't recursively deploy.
_shell('git commit --no-verify -m "Merge autogenerated files [skip ci]"')
# Push the distributions to PyPI.
_pypi_push('dist')
# Push the tag and AUTHORS / ChangeLog after successful PyPI deploy
_shell('git push --follow-tags')
print('Deployment complete. Latest version is {version}.'.format(version=version))
if __name__ == '__main__': # pragma: no cover
if len(sys.argv) != 2:
raise RuntimeError('Require one argument indicating deploy target: `prod` or `test`.')
deploy(sys.argv[1].upper())