-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathrelease.py
255 lines (217 loc) · 8.12 KB
/
release.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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
#!/usr/bin/env python3
"""Release script for ODL projects"""
import re
import os
from subprocess import CalledProcessError
from pkg_resources import parse_version
from async_subprocess import (
call,
check_call,
check_output,
)
from constants import (
GIT_RELEASE_NOTES_PATH,
SCRIPT_DIR,
YARN_PATH,
)
from exception import (
DependencyException,
ReleaseException,
)
from github import create_pr
from lib import (
get_default_branch,
init_working_dir,
)
from version import update_version
async def dependency_exists(command):
"""Returns true if a command exists on the system"""
return await call(["which", command], cwd="/") == 0
async def validate_dependencies():
"""Error if a dependency is missing or invalid"""
if not await dependency_exists("git"):
raise DependencyException("Please install git https://git-scm.com/downloads")
if not await dependency_exists("node"):
raise DependencyException("Please install node.js https://nodejs.org/")
if not await dependency_exists(
GIT_RELEASE_NOTES_PATH
) or not await dependency_exists(YARN_PATH):
raise DependencyException("Please run 'npm install' first")
version_output = await check_output(["node", "--version"], cwd="/")
version = version_output.decode()
major_version = int(re.match(r"^v(\d+)\.", version).group(1))
if major_version < 6:
raise DependencyException("node.js must be version 6.x or higher")
async def any_new_commits(version, *, base_branch, root):
"""
Return true if there are any new commits since a release
Args:
version (str): A version string
base_branch (str): The branch to compare against
root (str): The project root directory
Returns:
bool: True if there are new commits
"""
return await any_commits_between_branches(
branch1=f"v{version}", branch2=base_branch, root=root
)
async def any_commits_between_branches(*, branch1, branch2, root):
"""
Return true if there are any commits between two branches
Args:
branch1 (str): The first branch to compare against
branch2 (str): The second, more recent branch to compare against
root (str): The project root directory
Returns:
bool: True if there are new commits
"""
output = await check_output(
["git", "rev-list", "--count", f"{branch1}..{branch2}", "--"], cwd=root
)
return int(output) != 0
async def create_release_notes(old_version, with_checkboxes, *, base_branch, root):
"""
Returns the release note text for the commits made for this version
Args:
old_version (str): The starting version of the range of commits
with_checkboxes (bool): If true, create the release notes with spaces for checkboxes
base_branch (str): The base branch to compare against
root (str): The project root directory
Returns:
str: The release notes
"""
if with_checkboxes:
filename = "release_notes.ejs"
else:
filename = "release_notes_rst.ejs"
if not await any_new_commits(old_version, base_branch=base_branch, root=root):
return "No new commits"
output = await check_output(
[
GIT_RELEASE_NOTES_PATH,
f"v{old_version}..{base_branch}",
os.path.join(SCRIPT_DIR, "util", filename),
],
cwd=root,
)
return f"{output.decode().strip()}\n"
async def verify_new_commits(old_version, *, base_branch, root):
"""Check if there are new commits to release"""
if not await any_new_commits(old_version, base_branch=base_branch, root=root):
raise ReleaseException("No new commits to put in release")
async def update_release_notes(old_version, new_version, *, base_branch, root):
"""Updates RELEASE.rst and commits it"""
release_notes = await create_release_notes(
old_version, with_checkboxes=False, base_branch=base_branch, root=root
)
release_filename = os.path.join(root, "RELEASE.rst")
try:
with open(release_filename, "r", encoding="utf-8") as f:
existing_note_lines = f.readlines()
except FileNotFoundError:
existing_note_lines = []
with open(release_filename, "w", encoding="utf-8") as f:
f.write("Release Notes\n")
f.write("=============\n")
f.write("\n")
version_line = f"Version {new_version}"
f.write(f"{version_line}\n")
f.write(f"{'-' * len(version_line)}\n")
f.write("\n")
f.write(release_notes)
f.write("\n")
# skip first four lines which contain the header we are replacing
for old_line in existing_note_lines[3:]:
f.write(old_line)
await check_call(["git", "add", release_filename], cwd=root)
await check_call(
["git", "commit", "-q", "--all", "--message", f"Release {new_version}"],
cwd=root,
)
async def build_release(*, root):
"""Deploy the release candidate"""
await check_call(
[
"git",
"push",
"--force",
"-q",
"origin",
"release-candidate:release-candidate",
],
cwd=root,
)
async def generate_release_pr(
*, github_access_token, repo_url, old_version, new_version, base_branch, root
): # pylint: disable=too-many-arguments
"""
Make a release pull request for the deployed release-candidate branch
Args:
github_access_token (str): The github access token
repo_url (str): URL for the repo
old_version (str): The previous release version
new_version (str): The version of the new release
base_branch (str): The base branch to compare against
root (str): The project root directory
"""
await create_pr(
github_access_token=github_access_token,
repo_url=repo_url,
title=f"Release {new_version}",
body=await create_release_notes(
old_version, with_checkboxes=True, base_branch=base_branch, root=root
),
head="release-candidate",
base="release",
)
async def release(
*, github_access_token, repo_info, new_version, branch=None, commit_hash=None
):
"""
Run a release
Args:
github_access_token (str): The github access token
repo_info (RepoInfo): RepoInfo for a repo
new_version (str): The version of the new release
branch (str): The branch to initialize the release from
commit_hash (str): Commit hash to cherry pick in case of a hot fix
"""
await validate_dependencies()
async with init_working_dir(
github_access_token, repo_info.repo_url, branch=branch
) as working_dir:
default_branch = await get_default_branch(working_dir)
await check_call(
["git", "checkout", "-qb", "release-candidate"], cwd=working_dir
)
if commit_hash:
try:
await check_call(["git", "cherry-pick", commit_hash], cwd=working_dir)
except CalledProcessError as ex:
raise ReleaseException(
f"Cherry pick failed for the given hash {commit_hash}"
) from ex
old_version = await update_version(
repo_info=repo_info,
new_version=new_version,
working_dir=working_dir,
readonly=False,
)
if parse_version(old_version) >= parse_version(new_version):
raise ReleaseException(
f"old version is {old_version} but the new version {new_version} is not newer"
)
base_branch = "release-candidate" if commit_hash else default_branch
await verify_new_commits(old_version, base_branch=base_branch, root=working_dir)
await update_release_notes(
old_version, new_version, base_branch=base_branch, root=working_dir
)
await build_release(root=working_dir)
return await generate_release_pr(
github_access_token=github_access_token,
repo_url=repo_info.repo_url,
old_version=old_version,
new_version=new_version,
base_branch=base_branch,
root=working_dir,
)