EIC Software
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Groups Pages
release.py
Go to the documentation of this file. Or view the newest version in sPHENIX GitHub for file release.py
1 #!/usr/bin/env python3
2 
3 from pathlib import Path
4 import io
5 
6 import click
7 import github
8 from github import Github
9 import yaml
10 import re
11 from sh import git
12 from rich import print
13 
14 from util import Spinner
15 
16 default_branch_name = "master"
17 
19  return git("rev-parse", "--abbrev-ref", "HEAD").strip()
20 
21 def split_version(version):
22  version_ex = re.compile(r"^v?(\d+)\.(\d{1,2})\.(\d{1,2})$")
23  m = version_ex.match(version)
24  assert m is not None, f"Version {version} is not in valid format"
25  return tuple((int(m.group(i)) for i in range(1, 4)))
26 
27 def format_version(version):
28  return "v{:d}.{:>2d}.{:>02d}".format(*version)
29 
30 def check_branch_exists(branch):
31  with Spinner(f"Checking for {branch} branch"):
32  all_branches = [l.strip() for l in git.branch(all=True, _tty_out=False).strip().split("\n")]
33  for b in all_branches:
34  if b.endswith(branch):
35  return True
36  return False
37 
38 @click.group()
39 @click.option("--token", "-T", required=True, envvar="GITHUB_TOKEN")
40 @click.option("--repository", "-R", required=True, envvar="GITHUB_REPOSITORY")
41 @click.option("--retry", default=3)
42 @click.pass_context
43 def main(ctx, token, repository, retry):
44  gh = Github(token, retry=retry)
45  repo = gh.get_repo(repository)
46 
47  ctx.obj = gh, repo
48 
49 
50 def confirm(*args, yes=False, **kwargs):
51  if yes == True:
52  return True
53  return click.confirm(*args, **kwargs)
54 
55 @main.command()
56 @click.argument("tag_name")
57 @click.option("--remote", default="origin")
58 @click.option("--yes", "-y", is_flag=True, default=False)
59 @click.pass_obj
60 def tag(obj, tag_name, remote, yes):
61  current_branch = get_current_branch()
62  remote_url = git.remote("get-url", remote).strip()
63 
64  gh, repo = obj
65 
66 
67  tag = split_version(tag_name)
68  tag_name = format_version(tag)
69  major, minor, fix = tag
70 
71  with Spinner(f"Checking for milestone for tag {tag_name}"):
72  tag_milestone = None
73  for ms in repo.get_milestones(state="all"):
74  if ms.title == tag_name:
75  tag_milestone = ms
76  break
77  assert tag_milestone is not None, "Did not find milestone for tag"
78 
79  release_branch_name = f"release/v{major}.{minor:>02}.X"
80 
81  with Spinner("Refreshing branches"):
82  git.fetch(all=True, prune=True)
83 
84  if fix == 0:
85  # new minor release
86  with Spinner(f"Checking out and updating {default_branch_name}"):
87  git.checkout(default_branch_name)
88  git.pull()
89 
90 
91  assert not check_branch_exists(release_branch_name), "For new minor: release branch CANNOT exist yet"
92 
93  with Spinner(f"Creating {release_branch_name}"):
94  git.checkout("-b", release_branch_name)
95  else:
96  assert check_branch_exists(release_branch_name), "For new fix: release brunch MUST exist"
97 
98  with Spinner(f"Checking out {release_branch_name}"):
99  git.checkout(release_branch_name)
100 
101  # we are not on release branch
102 
103  version_file = Path("version_number")
104  assert version_file.exists(), "Version number file not found"
105 
106  current_version_string = version_file.read_text()
107  print(f"Current version: [bold]{current_version_string}[/bold]")
108 
109  if fix == 0:
110  assert current_version_string == "9.9.9", "Unexpected current version string found"
111  else:
112  assert current_version_string != f"{major}.{minor}.{fix-1}", "Unexpected current version string found"
113 
114  version_string = f"{major}.{minor}.{fix}"
115  with Spinner(f"Bumping version number in '{version_file}' to '{version_string}'"):
116  with version_file.open("w") as fh:
117  fh.write(version_string)
118 
119  with Spinner("Comitting"):
120  git.add(version_file)
121  git.commit(m=f"Bump version number to {version_string}")
122 
123  with Spinner(f"Creating tag {tag_name}"):
124  git.tag(tag_name)
125 
126  print(f"I will now: push tag [bold green]{tag_name}[/bold green] and branch [bold green]{release_branch_name}[/bold green] to [bold]{remote_url}[/bold]")
127  if not confirm("Continue?", yes=yes):
128  raise SystemExit("Aborting")
129 
130  with Spinner(f"Pushing branch {release_branch_name}"):
131  git.push("-u", remote, release_branch_name)
132 
133  with Spinner(f"Pushing tag {tag_name}"):
134  git.push(remote, tag_name)
135 
136 
137 @main.command()
138 @click.argument("tag_name")
139 @click.option("--draft/--publish", default=True)
140 @click.option("--yes", "-y", is_flag=True, default=False)
141 @click.pass_obj
142 def notes(obj, tag_name, draft, yes):
143  gh, repo = obj
144 
145  label_file = repo.get_contents(".labels.yml", ref=default_branch_name).decoded_content
146  labels = yaml.safe_load(io.BytesIO(label_file))["labels"]
147 
148  with Spinner(f"Finding tag {tag_name}"):
149  tag = None
150  for t in repo.get_tags():
151  if t.name == tag_name:
152  tag = t
153  break
154  assert tag is not None, "Did not find tag"
155 
156  with Spinner(f"Loading milestone for tag {tag_name}"):
157  tag_milestone = None
158  for ms in repo.get_milestones(state="all"):
159  if ms.title == tag_name:
160  tag_milestone = ms
161  break
162  assert tag_milestone is not None, "Did not find milestone for tag"
163 
164  with Spinner(f"Getting PRs for milestone {tag_milestone.title}"):
165 
166  prs = list(
167  gh.search_issues(
168  "", milestone=tag_milestone.title, repo=repo.full_name, type="pr", **{"is": "merged"}
169  )
170  )
171 
172  assert not any(
173  [pr.state == "open" for pr in prs]
174  ), "PRs assigned to milestone that are still open!"
175 
176  click.echo("Have " + click.style(str(len(prs)), bold=True) + " PRs, all closed.")
177 
178  body = ""
179 
180  groups = {l: [] for l in sorted(labels)}
181  groups["Uncategorized"] = []
182 
183  for pr in prs:
184  pr_labels = [l.name for l in pr.labels]
185 
186  assigned = False
187  for label in labels:
188  if label in pr_labels:
189  groups[label].append(pr)
190  assigned = True
191  break
192  if not assigned:
193  groups["Uncategorized"].append(pr)
194 
195  for group, prs in groups.items():
196  if len(prs) == 0:
197  continue
198  name = group
199  if name.lower() == "bug":
200  name = "Bug Fixes"
201  body += f"#### {name}:\n\n"
202  for pr in prs:
203  body += f"- {pr.title} [#{pr.number}]({pr.html_url})\n"
204  body += "\n"
205 
206  body = body.strip()
207 
208  width, _ = click.get_terminal_size()
209 
210  print()
211  click.secho(
212  "\n".join([l.ljust(width) for l in [""] + body.split("\n") + [""]]),
213  fg="black",
214  bg="white",
215  )
216  print()
217 
218  release = None
219  with Spinner("Getting release"):
220  try:
221  release = repo.get_release(tag.name)
222 
223  except github.UnknownObjectException:
224  pass
225 
226  if release is not None:
227  # existing release, update
228 
229  click.echo(
230  "Existing release {} is at {}".format(
231  click.style(release.title, bold=True),
232  click.style(release.html_url, bold=True),
233  )
234  )
235  if confirm(f"Update release {release.title}?", yes=yes):
236  with Spinner(f"Updating release {release.title}"):
237  release.update_release(name=release.title, message=body)
238  click.echo(
239  "Updated release is at {}".format(
240  click.style(release.html_url, bold=True)
241  )
242  )
243 
244  else:
245  # new release
246  if confirm(f"Create release for tag {tag.name} (draft: {draft})?", yes=yes):
247  with Spinner(f"Creating release {tag.name}"):
248  release = repo.create_git_release(
249  tag=tag.name, name=tag.name, message=body, draft=draft
250  )
251  click.echo(
252  "Created release is at {}".format(
253  click.style(release.html_url, bold=True)
254  )
255  )
256  else:
257  print("Not creating a release")
258 
259  if tag_milestone.state == "open":
260  if confirm(f"Do you want me to close milestone {tag_milestone.title}?", yes=yes):
261  with Spinner(f"Closing milestone {tag_milestone.title}"):
262  tag_milestone.edit(title=tag_milestone.title, state="closed")
263  else:
264  print("Not closing milestone")
265 
266 
267 main()