6 from pathlib
import Path
7 import gitlab
as gitlab_api
9 from datetime
import datetime, date
10 from dateutil.relativedelta
import relativedelta, FR
11 import dateutil.parser
15 import click_config_file
18 from pprint
import pprint
21 from util
import Spinner
22 from release_notes
import (
28 version_ex = re.compile(
r"^v?(\d+)\.(\d{1,2})\.(\d{1,2})$")
32 m = version_ex.match(version)
33 assert m
is not None, f
"Version {version} is not in valid format"
34 return [
int(m.group(i))
for i
in range(1, 4)]
38 return "v{:d}.{:>2d}.{:>02d}".format(*version)
42 gl = gitlab_api.Gitlab(
"https://gitlab.cern.ch", private_token=token)
49 "--gitlab-token",
"-t",
"gitlab", required=
True, callback=gitlab_instance
55 milestones = project.milestones.list(**kwargs)
66 vstr =
"{:d}.{:>02d}.{:>02d}".format(*version)
67 ms_titles = (vstr,
"v" + vstr)
72 if ms.title
in ms_titles:
81 git(
"for-each-ref",
"refs/heads", format=
"%(refname:short)").strip().
split(
"\n")
87 branch = git.branch(show_current=
True).strip()
99 @click.option(
"--dry-run", is_flag=
True)
100 @click.argument(
"version")
101 def minor(version, dry_run, gitlab):
102 project = gitlab.projects.get(
"acts/acts-core")
104 milestone =
find_milestone(version, project.milestones.list(state=
"active"))
106 milestone
is not None
107 ), f
"Didn't find milestone for {version}. Is it closed already?"
111 release_branch =
"release/v{:d}.{:>02d}.X".format(*version)
112 source_branch =
"master"
113 version_file =
Path() /
"version_number"
117 "Will make new release with version %s from milestone %s and branch %s"
121 if click.confirm(
"Do you want to run local preparation?"):
122 if source_branch
not in branches:
123 print(
"Source branch", source_branch,
"not found.")
126 with
Spinner(text=f
"Checkout and update source branch {source_branch}"):
128 git.checkout(source_branch)
132 if release_branch
in branches
and not dry_run:
133 print(
"Release branch", release_branch,
"exists. I'm bailing")
136 with
Spinner(text=f
"Creating {release_branch} from {source_branch}"):
138 git.checkout(
"-b", release_branch)
140 with
Spinner(text=f
"Bumping version to {format_version(version)}"):
143 with version_file.open(
"w")
as fh:
144 fh.write(
".".join(map(str, version)))
147 text=f
"Committing bumped version on release branch {release_branch}"
150 git.add(str(version_file))
151 git.commit(message=
"Bump version to %s" %
".".join(map(str, version)))
153 with
Spinner(text=f
"Creating local tag {tag_name} on {release_branch}"):
156 print(f
"You might want to run 'git push REMOTE {tag_name}'")
158 if click.confirm(f
"Do you want me to try to push {release_branch}?"):
159 with
Spinner(text=f
"Pushing {release_branch}"):
163 if click.confirm(f
"Do you want me to try to push tag {tag_name}?"):
164 with
Spinner(text=f
"Pushing {tag_name}"):
166 git.push(
"REMOTE", tag_name)
168 if click.confirm(f
"Do you want me to close %{milestone.title}?"):
169 with
Spinner(text=f
"Closing milestone %{milestone.title}"):
171 milestone.state_event =
"close"
176 print(
"THIS WAS A DRY RUN!")
180 @click.option(
"--dry-run", is_flag=
True)
181 @click.argument(
"version")
182 def patch(version, dry_run, gitlab):
183 project = gitlab.projects.get(
"acts/acts-core")
186 milestone =
find_milestone(version, project.milestones.list(state=
"active"))
188 milestone
is not None
189 ), f
"Didn't find milestone for {version}. Is it closed already?"
193 release_branch =
"release/v{:d}.{:>02d}.X".format(*version)
194 version_file =
Path() /
"version_number"
197 if release_branch
not in branches:
198 print(
"Release branch", release_branch,
"does not exist. I'm bailing")
201 "Will make new patch version tag %s from milestone %s on branch %s"
205 if click.confirm(
"Do you want to run local preparation?"):
207 with
Spinner(text=f
"Checkout and update release branch {release_branch}"):
209 git.checkout(release_branch)
213 with
Spinner(text=f
"Bumping version to {format_version(version)}"):
216 with version_file.open(
"w")
as fh:
217 fh.write(
".".join(map(str, version)))
220 text=f
"Committing bumped version on release branch {release_branch}"
223 git.add(str(version_file))
224 git.commit(message=
"Bump version to %s" %
".".join(map(str, version)))
226 with
Spinner(text=f
"Creating local tag {tag_name} on {release_branch}"):
229 print(f
"You might want to run 'git push REMOTE {tag_name}'")
231 if click.confirm(f
"Do you want me to try to push {release_branch}?"):
232 with
Spinner(text=f
"Pushing {release_branch}"):
236 if click.confirm(f
"Do you want me to close %{milestone.title}?"):
237 with
Spinner(text=f
"Closing milestone %{milestone.title}"):
239 milestone.state_event =
"close"
246 @click.argument(
"version")
251 next_version = current_version[:]
254 print(current_version, next_version, file=sys.stderr)
256 project = gitlab.projects.get(
"acts/acts-core")
258 milestones = project.milestones.list()
261 assert current_milestone
is not None
265 if next_milestone
is None:
267 if click.confirm(
"Want me to create it?"):
269 next_milestone = project.milestones.create(title=title)
273 if current_milestone.due_date != date.today().strftime(dfmt):
274 if sys.stdout.isatty():
276 f
"Do you want me to set due date of %{current_milestone.title} to {date.today()}? (is {current_milestone.due_date})"
278 current_milestone.due_date = date.today().strftime(dfmt)
279 current_milestone.save()
281 if next_milestone.due_date
is None:
283 delta = relativedelta(weekday=FR(1))
284 next_due = dt + delta
286 next_due = datetime.strptime(next_milestone.due_date, dfmt)
287 if sys.stdout.isatty():
288 next_due = datetime.strptime(
290 f
"Due date for milestone %{next_milestone.title}",
291 next_due.strftime(dfmt),
296 start_date = datetime.strptime(next_milestone.start_date, dfmt)
or date.today()
297 start_date_str = start_date.strftime(dfmt)
298 next_due_str = next_due.strftime(dfmt)
301 next_milestone.start_date != start_date_str
302 or next_milestone.due_date != next_due_str
304 if click.confirm(f
"Update milestone %{next_milestone.title}?"):
305 with
Spinner(text=f
"Updating milestone %{next_milestone.title}"):
306 next_milestone.start_date = start_date_str
307 next_milestone.due_date = next_due_str
308 next_milestone.save()
310 release_branch =
"release/v{:d}.{:>02d}.X".format(*current_version)
312 tpl = jinja2.Template(
314 I've just tagged [`{{cv}}`](https://gitlab.cern.ch/acts/acts-core/-/tags/{{cv}}) from milestone [`%{{cm.title}}`](https://gitlab.cern.ch/acts/acts-core/-/milestones/{{cm.iid}}).
315 Bugfixes should be targeted at [`{{ release_branch }}`](https://gitlab.cern.ch/acts/acts-core/tree/{{ release_branch }}).
317 We will tag the next release `{{nv}}` on {{humanize.naturaldate(next_due)}} from [`%{{nm.title}}`](https://gitlab.cern.ch/acts/acts-core/-/milestones/{{nm.iid}}).
318 This release can be cancelled if a sufficient number of merges does not happen before that date.
322 tpl.globals[
"humanize"] = humanize
325 next_due=datetime.strptime(next_milestone.due_date, dfmt).date(),
326 release_branch=release_branch,
327 cm=current_milestone,
337 @click.argument(
"start", callback=
lambda c, p, s:
split_version(s))
338 @click.argument(
"end", callback=
lambda c, p, s:
split_version(s))
342 print(start, end, file=sys.stderr)
343 project = gitlab.projects.get(
"acts/acts-core")
347 for ms
in all_milestones.values():
350 milestones.append(ms)
354 sorted_milestones = list(sorted(all_milestones.keys()))
356 start_ms = all_milestones[start]
357 end_ms = all_milestones[end]
359 ms_range = (sorted_milestones.index(start), sorted_milestones.index(end))
363 for mst
in sorted_milestones[ms_range[0]+1:ms_range[1]+1]:
364 ms = all_milestones[mst]
365 print(ms.title, file=sys.stderr)
369 with
Spinner(text=
"Assembling release notes", stream=sys.stderr):
370 md += f
"## {format_version(mst)}\n\n"
377 def __init__(self, token, base_url = "https://zenodo.org/api/
"):
381 def get(self, url, params = {}, **kwargs):
382 _params = {
"access_token": self.
token}
383 _params.update(params)
384 r = requests.get(os.path.join(self.
base_url, url), params=_params, **kwargs)
387 def post(self, url, params = {}, headers = {}, **kwargs):
388 _headers = {
"Content-Type":
"application/json"}
389 _headers.update(headers)
390 _params = {
"access_token": self.
token}
391 _params.update(params)
392 r = requests.post(os.path.join(self.
base_url, url), params=_params,
395 assert r.status_code == 201, r.json()
398 def put(self, url, data, params = {}, headers = {}, **kwargs):
399 _headers = {
"Content-Type":
"application/json"}
400 _headers.update(headers)
403 _url = os.path.join(self.
base_url, url)+f
"?access_token={self.token}"
405 r = requests.put(_url,
406 data=json.dumps(data),
409 assert r.status_code == 200, f
"Status {r.status_code}, {r.json()}"
413 _params = {
"access_token": self.
token}
416 r = requests.post(os.path.join(self.
base_url, f
"deposit/depositions/{deposition}/files"),
417 params=_params, data=data, files=files)
418 assert r.status_code == 201, r.status_code
421 def delete(self, url, params = {}, **kwargs):
422 _params = {
"access_token": self.
token}
423 return requests.delete(os.path.join(self.
base_url, url), params=_params)
427 @click.argument(
"version")
428 @click.option(
"--zenodo-token",
"-z", required=
True)
429 @click.option(
"--deposition",
"-d", required=
True)
430 @click_config_file.configuration_option()
431 def zenodo(version, gitlab, zenodo_token, deposition):
434 zenodo =
Zenodo(zenodo_token)
436 with
Spinner(text=
"Creating new version of existing deposition"):
437 create_res = zenodo.post(f
"deposit/depositions/{deposition}/actions/newversion")
439 create_res = create_res.json()
441 draft_id = create_res[
"links"][
"latest_draft"].
split(
"/")[-1]
444 print(
"Created new version with id", draft_id)
446 with
Spinner(text=
"Delete all files for draft"):
447 draft = zenodo.get(f
"deposit/depositions/{draft_id}")
450 for file
in draft[
"files"]:
452 r = zenodo.delete(f
"deposit/depositions/{draft_id}/files/{file_id}")
453 assert r.status_code == 204
455 with
Spinner(text=
"Assembling authors"):
456 creator_file = os.path.join(os.path.dirname(__file__),
"../AUTHORS.md")
457 with
open(creator_file)
as fh:
458 md = fh.read().strip().
split(
"\n")
459 md = [l.strip()
for l
in md
if not l.strip().startswith(
"#")
and not l.strip() ==
""]
463 assert line.startswith(
"- ")
465 split = line.split(
",", 1)
466 creator = {
"name": split[0].strip()}
469 creator[
"affiliation"] = split[1].strip()
471 creators.append(creator)
473 with
Spinner(text=
"Collection milestones for description"):
474 project = gitlab.projects.get(
"acts/acts-core")
475 milestones = project.milestones.list()
478 assert milestone.state ==
"closed"
482 tag_date = dateutil.parser.parse(tag.commit[
"created_at"]).date().strftime(
"%Y-%m-%d")
484 description = f
'Milestone: <a href="{milestone.web_url}">%{milestone.title}</a> <br/> Merge requested accepted for this version: \n <ul>\n'
486 for mr
in sum(mrs_grouped.values(), []):
487 description += f
'<li><a href="{mr.web_url}">!{mr.iid} - {mr.title}</a></li>\n'
489 description +=
"</ul>"
491 with
Spinner(text=
"Updating deposition metadata"):
492 data = {
"metadata": {
493 "title": f
"Acts Project: {format_version(version)}",
494 "upload_type":
"software",
495 "description": description,
496 "creators": creators,
498 "publication_date": tag_date,
499 "license":
"MPL-2.0",
501 zenodo.put(f
"deposit/depositions/{draft_id}", data).
json()
504 with tempfile.TemporaryFile()
as fh:
505 with
Spinner(text=
"Downloading release archive from Gitlab"):
506 r = requests.get(f
"https://gitlab.cern.ch/acts/acts-core/-/archive/{format_version(version)}/acts-core-{format_version(version)}.zip", stream=
True)
507 r.raw.decode_content =
True
508 fh.write(r.raw.read())
510 with
Spinner(text=
"Uploading release archive to zenodo"):
511 name = f
"acts-core-{format_version(version)}.zip"
512 zenodo.upload(draft_id, name, fh)
515 print(f
"Done: https://zenodo.org/deposit/{draft_id}")
517 if "__main__" == __name__: