Skip to content

Commit 2bd99af

Browse files
authored
chore: deduplicate and abstract some bits of the release script (#7592)
This pull request deduplicates and abstracts some repetitive pieces of the release script in the interest of readability and maintainability. I generated the 2.2.0 draft release using this branched version of `release.py`. ## Checklist - [x] Change(s) are motivated and described in the PR description. - [x] Testing strategy is described if automated tests are not included in the PR. - [x] Risk is outlined (performance impact, potential for breakage, maintainability, etc). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] [Library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) are followed. If no release note is required, add label `changelog/no-changelog`. - [x] Documentation is included (in-code, generated user docs, [public corp docs](https://github.com/DataDog/documentation/)). - [x] Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [ ] Title is accurate. - [ ] No unnecessary changes are introduced. - [ ] Description motivates each change. - [ ] Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [ ] Testing strategy adequately addresses listed risk(s). - [ ] Change is maintainable (easy to change, telemetry, documentation). - [ ] Release note makes sense to a user of the library. - [ ] Reviewer has explicitly acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment. - [ ] Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) - [ ] If this PR touches code that signs or publishes builds or packages, or handles credentials of any kind, I've requested a review from `@DataDog/security-design-and-guidance`. - [ ] This PR doesn't touch any of that.
1 parent 80bf6a9 commit 2bd99af

File tree

1 file changed

+93
-104
lines changed

1 file changed

+93
-104
lines changed

scripts/release.py

Lines changed: 93 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import namedtuple
12
import json
23
import os
34
import re
@@ -50,99 +51,88 @@
5051
""" # noqa
5152

5253
MAX_GH_RELEASE_NOTES_LENGTH = 125000
54+
ReleaseParameters = namedtuple("ReleaseParameters", ["branch", "name", "tag", "dd_repo", "rn", "prerelease"])
5355

5456

55-
def create_release_draft(dd_repo, base, rc, patch, latest_branch):
56-
# make sure we're up to date
57+
def _ensure_current_checkout():
5758
subprocess.run("git fetch", shell=True, cwd=os.pardir)
5859

60+
61+
def _decide_next_release_number(base: str, candidate: bool = False) -> int:
62+
"""Return the next number to use as a patch or release candidate version, based on existing tags on the remote"""
63+
search = r"v%s.0\.?rc((\d+$))" % base if candidate else r"v%s.((\d+))" % base
64+
tags = dd_repo.get_tags()
65+
latest_version = 0
66+
for tag in tags:
67+
try:
68+
other_num = int(re.findall(search, tag.name)[0][0])
69+
except (IndexError, ValueError, TypeError):
70+
continue
71+
if other_num > latest_version:
72+
latest_version = other_num
73+
return latest_version + 1
74+
75+
76+
def _get_rc_parameters(dd_repo, base: str, rc, patch, latest_branch) -> ReleaseParameters:
77+
"""Build a ReleaseParameters object representing the in-progress release candidate"""
78+
new_rc_version = _decide_next_release_number(base, candidate=True)
79+
release_branch = latest_branch if new_rc_version == 1 else base
80+
rn = clean_release_notes(generate_release_notes(release_branch))
81+
return ReleaseParameters(release_branch, "%s.0rc%s" % (base, str(new_rc_version)), "v%s" % name, dd_repo, rn, True)
82+
83+
84+
def _get_patch_parameters(dd_repo, base: str, rc, patch, latest_branch) -> ReleaseParameters:
85+
"""Build a ReleaseParameters object representing the in-progress patch release"""
86+
name = "%s.%s" % (base, str(_decide_next_release_number(base)))
87+
release_notes = clean_release_notes(generate_release_notes(base))
88+
return ReleaseParameters(base, name, "v%s" % name, dd_repo, release_notes, False)
89+
90+
91+
def _get_minor_parameters(dd_repo, base: str, rc, patch, latest_branch) -> ReleaseParameters:
92+
"""Build a ReleaseParameters object representing the in-progress minor release"""
93+
name = "%s.0" % base
94+
95+
rn_raw = generate_release_notes(base)
96+
rn_sections_clean = create_release_notes_sections(rn_raw, base)
97+
release_notes = ""
98+
rn_key_order = [
99+
"Prelude",
100+
"New Features",
101+
"Known Issues",
102+
"Upgrade Notes",
103+
"Deprecation Notes",
104+
"Bug Fixes",
105+
"Other Changes",
106+
]
107+
for key in rn_key_order:
108+
try:
109+
release_notes += "### %s\n\n%s" % (key, rn_sections_clean[key])
110+
except KeyError:
111+
continue
112+
return ReleaseParameters(base, name, "v%s" % name, dd_repo, release_notes, False)
113+
114+
115+
def create_release_draft(dd_repo, base, rc, patch, latest_branch):
116+
_ensure_current_checkout()
117+
args = (dd_repo, base, rc, patch, latest_branch)
59118
if rc:
60-
# figure out the rc version we want
61-
search = r"v%s.0\.?rc((\d+$))" % base
62-
tags = dd_repo.get_tags()
63-
latest_rc_version = 0
64-
for tag in tags:
65-
try:
66-
other_rc_num = re.findall(search, tag.name)[0][0]
67-
other_rc_num = int(other_rc_num)
68-
except (IndexError, ValueError, TypeError):
69-
continue
70-
if other_rc_num > latest_rc_version:
71-
latest_rc_version = other_rc_num
72-
new_rc_version = latest_rc_version + 1
73-
# if this is the first rc for this base, we want to target the latest branch
74-
if new_rc_version == 1:
75-
name = "%s.0rc1" % base
76-
tag = "v%s" % name
77-
branch = latest_branch
78-
# if this is the rc+1 for this base
79-
else:
80-
name = "%s.0rc%s" % (base, str(new_rc_version))
81-
tag = "v%s" % name
82-
branch = base
83-
rn_raw = generate_rn(branch)
84-
rn = clean_rn(rn_raw)
85-
create_draft_release(branch=branch, name=name, tag=tag, dd_repo=dd_repo, rn=rn, prerelease=True)
86-
87-
# patch release
119+
parameters = _get_rc_parameters(*args)
88120
elif patch:
89-
# figure out the patch version we want
90-
search = r"v%s.((\d+))" % base
91-
tags = dd_repo.get_tags()
92-
latest_patch_version = 0
93-
for tag in tags:
94-
try:
95-
other_patch_num = re.findall(search, tag.name)[0][0]
96-
other_patch_num = int(other_patch_num)
97-
except (IndexError, ValueError, TypeError):
98-
continue
99-
if other_patch_num > latest_patch_version:
100-
latest_patch_version = other_patch_num
101-
new_patch_version = latest_patch_version + 1
102-
103-
name = "%s.%s" % (base, str(new_patch_version))
104-
tag = "v%s" % name
105-
rn_raw = generate_rn(base)
106-
rn = clean_rn(rn_raw)
107-
create_draft_release(branch=base, name=name, tag=tag, dd_repo=dd_repo, rn=rn, prerelease=False)
108-
109-
# official minor release
121+
parameters = _get_patch_parameters(*args)
110122
else:
111-
name = "%s.0" % base
112-
tag = "v%s" % name
113-
branch = base
114-
115-
rn_raw = generate_rn(branch)
116-
rn_sections_clean = create_release_notes_sections(rn_raw, branch)
117-
# combine the release note sections into a string in the correct order
118-
rn = ""
119-
rn_key_order = [
120-
"Prelude",
121-
"New Features",
122-
"Known Issues",
123-
"Upgrade Notes",
124-
"Deprecation Notes",
125-
"Bug Fixes",
126-
"Other Changes",
127-
]
128-
for key in rn_key_order:
129-
try:
130-
rn += "### %s\n\n%s" % (key, rn_sections_clean[key])
131-
except KeyError:
132-
continue
133-
134-
create_draft_release(branch=branch, name=name, tag=tag, dd_repo=dd_repo, rn=rn, prerelease=False)
135-
136-
return name, rn
137-
138-
139-
def clean_rn(rn_raw):
140-
# remove all release notes generated,
141-
# except for those that haven't been released yet, which are the ones we care about
123+
parameters = _get_minor_parameters(*args)
124+
125+
create_draft_release_github(parameters)
126+
127+
return parameters.name, parameters.rn
128+
129+
130+
def clean_release_notes(rn_raw: str) -> str:
131+
"""removes everything from the given string except for release notes that haven't been released yet"""
142132
return rn_raw.decode().split("## v")[0].replace("\n## Unreleased\n", "", 1).replace("# Release Notes\n", "", 1)
143133

144134

145-
def generate_rn(branch):
135+
def generate_release_notes(branch: str) -> str:
146136
subprocess.check_output(
147137
"git checkout {branch} && \
148138
git pull origin {branch}".format(
@@ -163,7 +153,7 @@ def generate_rn(branch):
163153

164154
def create_release_notes_sections(rn_raw, branch):
165155
# get anything in unreleased section in case there were updates since the last RC
166-
unreleased = clean_rn(rn_raw)
156+
unreleased = clean_release_notes(rn_raw)
167157
unreleased = break_into_release_sections(unreleased)
168158
try:
169159
unreleased_sections = dict(section.split("\n\n-") for section in unreleased)
@@ -202,45 +192,42 @@ def break_into_release_sections(rn):
202192
return [ele for ele in re.split(r"[^#](#)\1{2}[^#]", rn)[1:] if ele != "#"]
203193

204194

205-
def create_draft_release(
206-
branch,
207-
name,
208-
tag,
209-
rn,
210-
prerelease,
211-
dd_repo,
212-
):
213-
base_branch = dd_repo.get_branch(branch=branch)
195+
def create_draft_release_github(release_parameters: ReleaseParameters):
196+
base_branch = dd_repo.get_branch(branch=release_parameters.branch)
214197
print_release_notes = bool(os.getenv("PRINT"))
215198
if print_release_notes:
216199
print(
217200
"""RELEASE NOTES INFO:\nName:%s\nTag:%s\nprerelease:%s\ntarget_commitish:%s\nmessage:%s
218201
"""
219-
% (name, tag, prerelease, base_branch, rn)
202+
% (
203+
release_parameters.name,
204+
release_parameters.tag,
205+
release_parameters.prerelease,
206+
base_branch,
207+
release_parameters.rn,
208+
)
220209
)
221210
else:
222211
dd_repo.create_git_release(
223-
name=name,
224-
tag=tag,
225-
prerelease=prerelease,
212+
name=release_parameters.name,
213+
tag=release_parameters.tag,
214+
prerelease=release_parameters.prerelease,
226215
draft=True,
227216
target_commitish=base_branch,
228-
message=rn[:MAX_GH_RELEASE_NOTES_LENGTH],
217+
message=release_parameters.rn[:MAX_GH_RELEASE_NOTES_LENGTH],
229218
)
230219
print("\nPlease review your release notes draft here: https://github.com/DataDog/dd-trace-py/releases")
231220

232-
return name, rn
221+
return release_parameters.name, release_parameters.rn
233222

234223

235-
def setup_gh():
236-
# get dd-trace-py repo
224+
def get_ddtrace_repo():
237225
gh_token = os.getenv("GH_TOKEN")
238226
if not gh_token:
239227
raise ValueError(
240228
"We need a Github token to generate the release notes. Please follow the instructions in the script."
241229
)
242-
g = Github(gh_token)
243-
return g.get_repo(full_name_or_id="DataDog/dd-trace-py")
230+
return Github(gh_token).get_repo(full_name_or_id="DataDog/dd-trace-py")
244231

245232

246233
def create_notebook(dd_repo, name, rn, base, latest_branch):
@@ -439,8 +426,10 @@ def create_notebook(dd_repo, name, rn, base, latest_branch):
439426

440427
if base is None:
441428
raise ValueError("Need to specify the base version with envar e.g. BASE=2.10")
429+
if ".x" in base:
430+
raise ValueError("Base branch must be a fully qualified semantic version.")
442431

443-
dd_repo = setup_gh()
432+
dd_repo = get_ddtrace_repo()
444433
name, rn = create_release_draft(dd_repo, base, rc, patch, latest_branch)
445434

446435
if rc:

0 commit comments

Comments
 (0)