utils.release_helper   F
last analyzed

Complexity

Total Complexity 138

Size/Duplication

Total Lines 782
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 513
dl 0
loc 782
rs 2
c 0
b 0
f 0
wmc 138

65 Functions

Rating   Name   Duplication   Size   Complexity  
A create_ini_file_template() 0 10 3
A print_last_release_stats() 0 14 1
A describe_next_release_state() 0 5 2
A get_friday_that_follows() 0 2 1
A show_outdated_items() 0 5 2
A stats() 0 7 2
A update_contributors() 0 13 2
A print_communication_channels() 0 14 2
A get_latest_version() 0 3 1
A get_release_highlights() 0 10 5
A get_next_stabilization_date() 0 4 1
A print_specific_stat() 0 4 2
A get_repo_branches() 0 2 1
A get_repo_stable_branch() 0 3 1
A filter_outdated_items() 0 15 3
A extract_version_from_tag_name() 0 2 1
A finish() 0 8 3
A get_version_milestone() 0 6 3
A print_next_release_stats() 0 20 1
A has_milestone_version_format() 0 3 1
A get_latest_version_milestone() 0 10 3
A get_new_contributors() 0 10 4
A collect_release_info() 0 33 1
A is_next_release_in_progress() 0 7 2
A filter_version_milestones() 0 6 3
A close_milestone() 0 13 4
A get_repo_object() 0 2 1
A get_latest_release() 0 4 1
A get_parameter_from_ini() 0 7 2
B release_prep() 0 27 7
A create_repo_milestone() 0 17 4
A show_git_diff() 0 3 1
A release() 0 17 4
A get_contributors_commit_diff() 0 5 1
A get_old_stabilization_branch() 0 5 1
A get_next_release_date() 0 3 1
A get_github_token() 0 8 3
A get_items_with_milestone() 0 6 3
A get_repo_releases() 0 2 1
A get_release_end_message() 0 48 5
B parse_arguments() 0 56 1
A is_contributors_list_updated() 0 8 2
A get_repo_root_path() 0 3 1
A cleanup() 0 8 4
A update_milestone() 0 7 3
B bump_master_version() 0 21 6
A get_confirmation() 0 3 1
A get_open_prs_with_milestone() 0 3 1
A update_milestone_in_issues() 0 4 2
A main() 0 18 2
A filter_items_outdated_milestone() 0 7 4
A filter_repo_branch_by_name() 0 2 1
A get_date_for_message() 0 2 1
A bump_version() 0 13 3
A get_script_path() 0 2 1
A get_repo_milestones() 0 2 1
A get_release_start_message() 0 31 1
A get_contributors_last_update() 0 11 3
A get_next_stabilization_branch() 0 3 1
A get_monday_that_follows() 0 2 1
A get_next_release_version() 0 4 1
A get_contributors_last_commit() 0 5 1
A get_open_issues_with_milestone() 0 3 1
A create_github_session() 0 5 2
A remove_old_stabilization_branch() 0 10 3

How to fix   Complexity   

Complexity

Complex classes like utils.release_helper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#!/usr/bin/python3
2
# -*- coding: utf-8 -*-
3
4
"""
5
Script created to help maintainers during the release process by automating Github tasks.
6
"""
7
8
# References:
9
# - https://developer.github.com/v3/libraries/
10
# - https://github.com/PyGithub/PyGithub
11
# - https://pygithub.readthedocs.io/en/latest/github_objects.html
12
# - https://docs.github.com/en/rest
13
14
from datetime import datetime, timedelta
15
from github import Github
16
import argparse
17
import configparser
18
import os.path
19
import re
20
import subprocess
21
22
23
def get_parameter_from_ini(config_file, section, parameter) -> str:
24
    config = configparser.ConfigParser()
25
    try:
26
        config.read(config_file)
27
        return config[section][parameter]
28
    except Exception as e:
29
        print(f'Error: {e} entry was not found!')
30
31
32
def create_ini_file_template(creds_file):
33
    try:
34
        new_creds_file = os.path.expanduser(creds_file)
35
        with open(new_creds_file, mode='w', encoding='utf-8') as file:
36
            file.write(
37
                '[DEFAULT]\n'
38
                'github_token = <generate your token in https://github.com/settings/tokens>\n')
39
        print(f'Great! {new_creds_file} was created! Edit it to include your personal token.')
40
    except Exception as e:
41
        print(f'Error: {e}')
42
43
44
def get_github_token(creds_file) -> str:
45
    creds_file = os.path.expanduser(creds_file)
46
    if os.path.exists(creds_file):
47
        return get_parameter_from_ini(creds_file, "DEFAULT", "github_token")
48
    else:
49
        print(f'{creds_file} file was not found!')
50
        if get_confirmation(f'Would you like to create the "{creds_file}" file?'):
51
            create_ini_file_template(creds_file)
52
53
54
def create_github_session(creds_file) -> object:
55
    token = get_github_token(creds_file)
56
    if not token:
57
        print('WARNING: No token found. The API queries are very limited without a token.\n')
58
    return Github(token)
59
60
61
def get_confirmation(question) -> bool:
62
    answer = str(input(f'{question} (Y/N): ')).lower().strip()
63
    return answer[:1] == 'y'
64
65
66
def get_repo_object(session, repo_id) -> object:
67
    return session.get_repo(repo_id)
68
69
70
def get_repo_root_path() -> str:
71
    root_path = get_script_path()
72
    return os.path.dirname(root_path)
73
74
75
def get_script_path() -> str:
76
    return os.path.dirname(os.path.realpath(__file__))
77
78
79
def show_git_diff():
80
    subprocess.run(['git', 'diff'], cwd=get_repo_root_path())
81
    print('\nPlease, review the changes and propose a PR to "master" branch.')
82
83
84
# Repository Branches
85
def filter_repo_branch_by_name(branches, name) -> object:
86
    return [branch for branch in branches if branch.name == name]
87
88
89
def get_repo_branches(repo) -> list:
90
    return repo.get_branches()
91
92
93
def get_repo_stable_branch(repo) -> object:
94
    branches = get_repo_branches(repo)
95
    return filter_repo_branch_by_name(branches, "stable")
96
97
98
def get_old_stabilization_branch(repo) -> object:
99
    branches = get_repo_branches(repo)
100
    latest_version = get_latest_version(repo)
101
    branch_name = f'stabilization-v{latest_version}'
102
    return filter_repo_branch_by_name(branches, branch_name)
103
104
105
def remove_old_stabilization_branch(repo, branch) -> None:
106
    if get_confirmation(f'Are you sure about removing the "{branch.name}" branch?'):
107
        # https://github.com/PyGithub/PyGithub/issues/1570
108
        try:
109
            branch_ref = repo.get_git_ref(f'heads/{branch.name}')
110
            branch_ref.delete()
111
        except Exception as e:
112
            print(f'Error: {e}')
113
    else:
114
        print(f'Aborted! {branch.name} branch not removed.')
115
116
117
# Repository Contributors
118
def get_contributors_commit_diff(commit) -> str:
119
    commit_diff = subprocess.run(
120
        ['git', 'show', '--word-diff', commit, 'Contributors.md'],
121
        capture_output=True, text=True, cwd=get_repo_root_path())
122
    return commit_diff.stdout
123
124
125
def get_contributors_last_commit() -> str:
126
    last_commit = subprocess.run(
127
        ['git', 'log', '--pretty=format:%h', '-1', '--', 'Contributors.md'],
128
        capture_output=True, text=True, cwd=get_repo_root_path())
129
    return last_commit.stdout
130
131
132
def get_contributors_last_update() -> str:
133
    last_commit = get_contributors_last_commit()
134
    last_commit_diff = get_contributors_commit_diff(last_commit)
135
136
    for line in last_commit_diff.split('\n'):
137
        # The "generate_contributors.py" creates a commend in the beginning of the file.
138
        # This comment informs when the file was updated.
139
        if 'Last Modified:' in line:
140
            elements = line.split('{+')
141
            datetime = elements[1].replace('+}', '')
142
            return datetime
143
144
145
def get_new_contributors(commit) -> str:
146
    last_commit_diff = get_contributors_commit_diff(commit)
147
    new_contributors = []
148
149
    for line in last_commit_diff.split('\n'):
150
        if line.startswith('{+'):
151
            for chars in ['{+', '+}']:
152
                line = line.replace(chars, '')
153
            new_contributors.append(line)
154
    return '\n'.join(new_contributors)
155
156
157
def is_contributors_list_updated(date_string) -> bool:
158
    date = datetime.strptime(date_string[:16], '%Y-%m-%d %H:%M')
159
    # As a rule of thumbs, this function consider contributors list
160
    # updated if not older than 2 weeks.
161
    two_weeks_back = datetime.now() - timedelta(days=15)
162
    if date < two_weeks_back:
163
        return False
164
    return True
165
166
167
def update_contributors():
168
    last_update = get_contributors_last_update()
169
    # Currently, this script considers contributors lists is already updated if not
170
    # older than 2 weeks.
171
    if is_contributors_list_updated(last_update):
172
        print(f'It is all fine, the contributors list was updated in {last_update}')
173
    else:
174
        print(f'Contributors list last update was in {last_update}. I can update it for you.')
175
        utils_scripts_path = get_script_path()
176
        contributors_script = os.path.join(utils_scripts_path, 'generate_contributors.py')
177
        subprocess.run(
178
            [f'PYTHONPATH=. {contributors_script}'], shell=True, cwd=get_repo_root_path())
179
        show_git_diff()
180
181
182
# Repository Milestones
183
def has_milestone_version_format(milestone) -> bool:
184
    regex = re.compile(r'\d\.\d\.\d')
185
    return regex.match(milestone.title)
186
187
188
def filter_version_milestones(milestones) -> list:
189
    version_milestones = []
190
    for milestone in milestones:
191
        if has_milestone_version_format(milestone):
192
            version_milestones.append(milestone)
193
    return version_milestones
194
195
196
def get_repo_milestones(repo) -> list:
197
    return repo.get_milestones(state="all", direction="desc")
198
199
200
def get_latest_version_milestone(repo) -> object:
201
    milestones = get_repo_milestones(repo)
202
    version_milestones = filter_version_milestones(milestones)
203
    latest_version_milestone = version_milestones[0]
204
    # It is not ensured the returned milestones list is ordered as expected.
205
    # This function ensures the last milestone based on titles.
206
    for milestone in version_milestones:
207
        if milestone.title > latest_version_milestone.title:
208
            latest_version_milestone = milestone
209
    return latest_version_milestone
210
211
212
def get_version_milestone(repo, version) -> object:
213
    milestones = get_repo_milestones(repo)
214
    version_milestones = filter_version_milestones(milestones)
215
    for milestone in version_milestones:
216
        if milestone.title == version:
217
            return milestone
218
219
220
def close_milestone(milestone) -> None:
221
    if milestone.state == "closed":
222
        print(f'Nice. The "{milestone}" milestone is already closed.')
223
        return
224
225
    if get_confirmation(f'The "{milestone}" milestone should be closed. Ok?'):
226
        try:
227
            milestone.edit(milestone.title, state="closed")
228
        except Exception as e:
229
            print(f'Error: {e}')
230
            exit(1)
231
    else:
232
        print(f'Aborted! {milestone} milestone not closed.')
233
234
235
def create_repo_milestone(repo, name) -> None:
236
    if get_version_milestone(repo, name):
237
        print(f'Great! The "{name}" milestone is already created.')
238
        return
239
240
    latest_release = get_latest_release(repo)
241
    estimated_release_date = get_next_release_date(latest_release.published_at)
242
    if get_confirmation(f'Are you sure about creating the "{name}" milestone?'):
243
        try:
244
            repo.create_milestone(
245
                title=name, description=f'Milestone for the release {name}',
246
                due_on=estimated_release_date)
247
        except Exception as e:
248
            print(f'Error: {e}')
249
            exit(1)
250
    else:
251
        print(f'Aborted! {name} milestone not created.')
252
253
254
# Repository Releases
255
def get_repo_releases(repo) -> list:
256
    return repo.get_releases()
257
258
259
def get_latest_release(repo) -> object:
260
    releases = get_repo_releases(repo)
261
    # The API returns the list already ordered by published date.
262
    return releases[0]
263
264
265
# Repository Versions
266
def extract_version_from_tag_name(version) -> str:
267
    return version.split("v")[1]
268
269
270
def get_latest_version(repo) -> str:
271
    release = get_latest_release(repo)
272
    return extract_version_from_tag_name(release.tag_name)
273
274
275
# Repository Next Release
276
def bump_version(current_version, bump_position) -> str:
277
    # version format example: 0.1.64
278
    if bump_position == 'minor':
279
        position = 2
280
    elif bump_position == 'major':
281
        position = 1
282
    else:
283
        position = 0
284
285
    split_version = current_version.split('.')
286
    bumped = int(split_version[position]) + 1
287
    split_version[position] = str(bumped)
288
    return ".".join(split_version)
289
290
291
def bump_master_version(repo):
292
    content_changed = False
293
    old_version = get_next_release_version(repo)
294
    new_version = bump_version(old_version, 'minor')
295
    old_minor_version = old_version.split('.')[2]
296
    new_minor_version = new_version.split('.')[2]
297
    repo_root = get_repo_root_path()
298
    with open(os.path.join(repo_root, "CMakeLists.txt"), mode='r', encoding='utf-8') as file:
299
        content = file.readlines()
300
301
    for ln, line in enumerate(content):
302
        if f'set(SSG_PATCH_VERSION {old_minor_version}' in line:
303
            content[ln] = content[ln].replace(old_minor_version, new_minor_version)
304
            content_changed = True
305
306
    if content_changed:
307
        with open(os.path.join(repo_root, "CMakeLists.txt"), mode='w', encoding='utf-8') as file:
308
            file.writelines(content)
309
        show_git_diff()
310
    else:
311
        print('Great! The version in CMakeLists.txt file is already updated.')
312
313
314
def describe_next_release_state(repo) -> str:
315
    if is_next_release_in_progress(repo):
316
        return 'In Stabilization Phase'
317
    else:
318
        return 'Scheduled'
319
320
321
def get_friday_that_follows(date) -> datetime:
322
    return date + timedelta((4 - date.weekday()) % 7)
323
324
325
def get_monday_that_follows(date) -> datetime:
326
    return date + timedelta((0 - date.weekday()) % 7)
327
328
329
def get_next_stabilization_date(release_date) -> datetime:
330
    two_weeks_before = release_date - timedelta(weeks=2)
331
    stabilization_monday = get_monday_that_follows(two_weeks_before)
332
    return stabilization_monday.date()
333
334
335
def get_next_release_date(latest_release_date) -> datetime:
336
    two_months_ahead = latest_release_date + timedelta(days=60)
337
    return get_friday_that_follows(two_months_ahead)
338
339
340
def is_next_release_in_progress(repo) -> bool:
341
    stabilization_branch = get_next_stabilization_branch(repo)
342
    branches_names = [branch.name for branch in get_repo_branches(repo)]
343
    if stabilization_branch in branches_names:
344
        return True
345
    else:
346
        return False
347
348
349
def get_next_release_version(repo) -> str:
350
    current_version = get_latest_version(repo)
351
    next_version = bump_version(current_version, 'minor')
352
    return next_version
353
354
355
def get_next_stabilization_branch(repo) -> str:
356
    next_release = get_next_release_version(repo)
357
    return f'stabilization-v{next_release}'
358
359
360
# Communication
361
def get_date_for_message(date) -> datetime:
362
    return date.strftime("%B %d, %Y")
363
364
365
def get_release_highlights(release) -> str:
366
    highlights = []
367
    for line in release.body.split('\r\n'):
368
        if '### Important Highlights' in line:
369
            continue
370
        if '###' in line:
371
            break
372
        if line:
373
            highlights.append(line)
374
    return '\n'.join(highlights)
375
376
377
def get_release_start_message(repo) -> str:
378
    latest_release = get_latest_release(repo)
379
    next_release_version = get_next_release_version(repo)
380
    next_release_date = get_next_release_date(latest_release.published_at)
381
    date = get_date_for_message(next_release_date)
382
    branch = get_next_stabilization_branch(repo)
383
384
    future_version = bump_version(next_release_version, 'minor')
385
    future_release_date = get_next_release_date(next_release_date)
386
    future_date = get_date_for_message(future_release_date)
387
    future_stabilization_date = get_next_stabilization_date(future_release_date)
388
    future_date_stabilization = get_date_for_message(future_stabilization_date)
389
390
    template = f'''
391
        Subject: stabilization of v{next_release_version}
392
393
        Hello all,
394
395
        The release of Content version {next_release_version} is scheduled for {date}.
396
        As part of the release process, a stabilization branch was created.
397
        Issues and PRs that were not solved were moved to the {future_version} milestone.
398
399
        Any bug fixes you would like to include in release {next_release_version} should be
400
        proposed for the *{branch}* and *master* branches as a measure to avoid
401
        potential conflicts between these branches.
402
403
        The next version, {future_version}, is scheduled to be released on {future_date},
404
        with the stabilization phase starting on {future_date_stabilization}.
405
406
        Regards,'''
407
    return template
408
409
410
def get_release_end_message(repo) -> str:
411
    latest_release = get_latest_release(repo)
412
    latest_release_url = latest_release.html_url
413
    highlights = get_release_highlights(latest_release)
414
    last_commit = get_contributors_last_commit()
415
    new_contributors = get_new_contributors(last_commit)
416
    released_version = get_latest_version(repo)
417
418
    for asset in latest_release.get_assets():
419
        if asset.content_type == 'application/x-bzip2':
420
            source_tarball = asset.browser_download_url
421
        elif asset.content_type == 'application/zip':
422
            prebuild_zip = asset.browser_download_url
423
        elif '.tar.bz2.sha512' in asset.name:
424
            source_tarball_hash = asset.browser_download_url
425
        else:
426
            prebuild_zip_hash = asset.browser_download_url
427
428
    # It would be more readable if this multiline string would be indented. However, this makes
429
    # the final text weird, after the "format" substitutions, when highlights and new_contributors
430
    # have multilines too. So, the readability was compromised in favor of simplicity and user
431
    # experience. The user only needs to copy and paste, without removing leading spaces.
432
    template = f'''
433
Subject: ComplianceAsCode/content v{released_version}
434
435
Hello all,
436
437
ComplianceAsCode/Content v{released_version} is out.
438
439
Some of the highlights of this release are:
440
{highlights}
441
442
Welcome to the new contributors:
443
{new_contributors}
444
445
For full release notes, please have a look at:
446
{latest_release_url}
447
448
Pre-built content: {prebuild_zip}
449
SHA-512 hash: {prebuild_zip_hash}
450
451
Source tarball: {source_tarball}
452
SHA-512 hash: {source_tarball_hash}
453
454
Thank you to everyone who contributed!
455
456
Regards,'''
457
    return template
458
459
460
def print_communication_channels(phase='release') -> None:
461
    gitter_lnk = 'https://gitter.im/Compliance-As-Code-The/content'
462
    ghd_lnk = 'https://github.com/ComplianceAsCode/content/discussions/new?category=announcements'
463
    twitter_lnk = 'https://twitter.com/openscap'
464
    ssg_mail = '[email protected]'
465
    openscap_mail = '[email protected]'
466
467
    print('Please, share the following message in:\n'
468
          f'* Gitter ({gitter_lnk})\n'
469
          f'* Github Discussion ({ghd_lnk})\n'
470
          f'* SCAP Security Guide Mail List ({ssg_mail})')
471
472
    if phase == "finish":
473
        print(f'* OpenSCAP Mail List ({openscap_mail})\n'
474
              f'* Twitter ({twitter_lnk})')
475
476
477
# Issues and PRs
478
def filter_items_outdated_milestone(objects_list, milestone) -> list:
479
    items_old_milestone = []
480
    for item in objects_list:
481
        if has_milestone_version_format(item.milestone) and \
482
                item.milestone.title != milestone.title:
483
            items_old_milestone.append(item)
484
    return items_old_milestone
485
486
487
def show_outdated_items(items_to_update):
488
    count = len(items_to_update)
489
    print(f'{count} open issues have an outdated milestone. Here are their links:')
490
    for item in items_to_update:
491
        print(f'{item.number:5} - {item.html_url:65} - {item.milestone.title} - {item.title}')
492
493
494
def filter_outdated_items(repo, objects_list) -> list:
495
    latest_milestone = get_latest_version_milestone(repo)
496
    items_to_update = filter_items_outdated_milestone(objects_list, latest_milestone)
497
    print('INFO: Please, note that in Github API the Pull Requests are also Issues objects but '
498
          'with some few differences in properties.')
499
    if items_to_update:
500
        show_outdated_items(items_to_update)
501
        if get_confirmation(
502
                'Are you sure about updating the milestone in these open issues?'):
503
            return items_to_update
504
        else:
505
            print('Aborted! Milestones not updated in open issues.')
506
    else:
507
        print('Great! There is no open issue with an outdated milestone.')
508
        return []
509
510
511
def get_items_with_milestone(object_list) -> list:
512
    with_milestone = []
513
    for item in object_list:
514
        if item.milestone:
515
            with_milestone.append(item)
516
    return with_milestone
517
518
519
def get_open_issues_with_milestone(repo) -> list:
520
    open_issues = repo.get_issues(state='open', direction='asc')
521
    return get_items_with_milestone(open_issues)
522
523
524
def get_open_prs_with_milestone(repo) -> list:
525
    open_prs = repo.get_pulls(state='open', direction='asc')
526
    return get_items_with_milestone(open_prs)
527
528
529
def update_milestone(repo, object_list) -> None:
530
    milestone = get_latest_version_milestone(repo)
531
    for item in object_list:
532
        try:
533
            item.edit(milestone=milestone)
534
        except Exception as e:
535
            print(f'Error: {e}')
536
537
538
def update_milestone_in_issues(repo, issues_list) -> None:
539
    outdate_issues = filter_outdated_items(repo, issues_list)
540
    if outdate_issues:
541
        update_milestone(repo, outdate_issues)
542
543
544
# Repo Stats
545
def collect_release_info(repo) -> dict:
546
    data = dict()
547
    latest_release = get_latest_release(repo)
548
    data["latest_release_created"] = latest_release.created_at
549
    data["latest_release_published"] = latest_release.published_at
550
    data["latest_release_url"] = latest_release.html_url
551
    data["latest_version"] = get_latest_version(repo)
552
553
    next_release_date = get_next_release_date(latest_release.published_at)
554
    next_release_remaining_days = next_release_date - datetime.today()
555
    data["next_release_date"] = next_release_date
556
    data["next_release_remaining_days"] = next_release_remaining_days.days
557
    data["next_release_state"] = describe_next_release_state(repo)
558
    data["next_release_version"] = get_next_release_version(repo)
559
    next_stabilization_date = get_next_stabilization_date(next_release_date)
560
    next_stabilization_remaining_days = next_stabilization_date - datetime.today().date()
561
    data["next_stabilization_date"] = next_stabilization_date
562
    data["next_stabilization_remaining_days"] = next_stabilization_remaining_days.days
563
564
    previous_milestone = get_version_milestone(repo, data["latest_version"])
565
    data["previous_milestone"] = previous_milestone.title
566
    data["previous_milestone_closed_issues"] = previous_milestone.closed_issues
567
    data["previous_milestone_open_issues"] = previous_milestone.open_issues
568
    data["previous_milestone_total_issues"] = data["previous_milestone_closed_issues"]\
569
        + data["previous_milestone_open_issues"]
570
571
    active_milestone = get_latest_version_milestone(repo)
572
    data["active_milestone"] = active_milestone.title
573
    data["active_milestone_closed_issues"] = active_milestone.closed_issues
574
    data["active_milestone_open_issues"] = active_milestone.open_issues
575
    data["active_milestone_total_issues"] = data["active_milestone_closed_issues"]\
576
        + data["active_milestone_open_issues"]
577
    return data
578
579
580
def print_specific_stat(status, current, total) -> None:
581
    if current > 0:
582
        percent = round((current / total) * 100.00, 2)
583
        print(f'{status:23.23} {current} / {total} = {percent}%')
584
585
586
def print_last_release_stats(data):
587
    version = data['latest_version']
588
    created_at = data['latest_release_created']
589
    published_at = data['latest_release_published']
590
    url_download = data['latest_release_url']
591
    closed_issues = data["previous_milestone_closed_issues"]
592
    total_issues = data["previous_milestone_total_issues"]
593
594
    print('Information based on the latest published release:')
595
    print(f'Current Version:        {version}')
596
    print(f'Created at:             {created_at}')
597
    print(f'Published at:           {published_at}')
598
    print(f'Release Notes:          {url_download}')
599
    print_specific_stat("Closed Issues/PRs:", closed_issues, total_issues)
600
601
602
def print_next_release_stats(data):
603
    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
604
    milestone = data['active_milestone']
605
    next_release_version = data['next_release_version']
606
    next_release_date = data['next_release_date']
607
    next_release_days = data['next_release_remaining_days']
608
    next_release_state = data['next_release_state']
609
    next_stab_date = data['next_stabilization_date']
610
    next_stab_days = data['next_stabilization_remaining_days']
611
    closed_issues = data["active_milestone_closed_issues"]
612
    total_issues = data["active_milestone_total_issues"]
613
614
    print(f'\nNext release information in {now}:')
615
    print(f'Next Version:           {next_release_version}')
616
    print(f'Current State:          {next_release_state}')
617
    print(f'Estimated Release Date: {next_release_date} ({next_release_days} remaining days)')
618
    print(f'Stabilization Date:     {next_stab_date} ({next_stab_days} remaining days)')
619
    print(f'Working Milestone:      {milestone}')
620
621
    print_specific_stat("Closed Issues/PRs:", closed_issues, total_issues)
622
623
624
# Functions to specific phases of the process
625
def stats(repo, args):
626
    try:
627
        rel_data = collect_release_info(repo)
628
        print_last_release_stats(rel_data)
629
        print_next_release_stats(rel_data)
630
    except Exception as e:
631
        print(f'Error: {e}')
632
633
634
def cleanup(repo, args) -> None:
635
    if args.branch:
636
        branches_to_remove = get_old_stabilization_branch(repo)
637
        if branches_to_remove:
638
            for branch in branches_to_remove:
639
                remove_old_stabilization_branch(repo, branch)
640
        else:
641
            print('Great! There is no branch to be removed.')
642
643
644
def release_prep(repo, args) -> None:
645
    if args.contributors:
646
        update_contributors()
647
648
    stab_branch_name = get_next_stabilization_branch(repo)
649
    if args.branch:
650
        print('git checkout master\n'
651
              'git pull upstream master\n'
652
              f'git checkout -b {stab_branch_name}\n'
653
              f'git push -u upstream {stab_branch_name}')
654
655
    if args.milestone:
656
        if is_next_release_in_progress(repo):
657
            stab_milestone = get_latest_version_milestone(repo)
658
            next_release_version = get_next_release_version(repo)
659
            new_milestone = bump_version(next_release_version, 'minor')
660
            close_milestone(stab_milestone)
661
            create_repo_milestone(repo, new_milestone)
662
        else:
663
            print(f'Milestones can be managed after the "{stab_branch_name}" branch is created.')
664
665
    if args.issues:
666
        issues_list = get_open_issues_with_milestone(repo)
667
        update_milestone_in_issues(repo, issues_list)
668
669
    if args.bump_version:
670
        bump_master_version(repo)
671
672
673
def release(repo, args) -> None:
674
    if is_next_release_in_progress(repo):
675
        if args.tag:
676
            next_version = get_next_release_version(repo)
677
            tag_name = f'v{next_version}'
678
            stab_branch = get_next_stabilization_branch(repo)
679
            print('Release process starts when a version tag is added to the branch.\n'
680
                  'It is better to do it manually. But here are the commands you need:\n')
681
            print(f'git tag {tag_name} {stab_branch}\n'
682
                  'git push --tags')
683
684
        if args.message:
685
            message = get_release_start_message(repo)
686
            print_communication_channels('release')
687
            print(message)
688
    else:
689
        print('Please, ensure that all steps in the Release Preparation are concluded.')
690
691
692
def finish(repo, args) -> None:
693
    if not is_next_release_in_progress(repo):
694
        if args.message:
695
            message = get_release_end_message(repo)
696
            print_communication_channels('finish')
697
            print(message)
698
    else:
699
        print('Please, ensure that all steps in the Release process are concluded.')
700
701
702
def parse_arguments():
703
    '''Call argparse to process input parameters and return parsed args'''
704
    parser = argparse.ArgumentParser(
705
        description="Automate Github tasks included in the Release Process.",
706
        epilog="Example: release_helper.py -c ~/secrets.ini -r ComplianceAsCode/content stats",
707
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
708
    parser.add_argument(
709
        '-c', '--creds-file', default='~/secret.ini',
710
        help="INI file containing Github token.")
711
    parser.add_argument(
712
        '-r', '--repository', action='store', default='ComplianceAsCode/content',
713
        help="Project repository name.")
714
715
    subparsers = parser.add_subparsers(dest='subcmd', required=True)
716
    subparsers.add_parser(
717
        'stats', help="Return information about the repository current state.")
718
719
    cleanup_parser = subparsers.add_parser(
720
        'cleanup', help="Cleanup after completing a release.")
721
    cleanup_parser.add_argument(
722
        '-b', '--branch', action='store_true',
723
        help="Remove temporary branch used during stabilization phase.")
724
725
    release_prep_parser = subparsers.add_parser(
726
        'release_prep', help="Prepare the repository for the next release.")
727
    release_prep_parser.add_argument(
728
        '-c', '--contributors', action='store_true',
729
        help="Update the contributors lists. Do it before creating the stabilization branch.")
730
    release_prep_parser.add_argument(
731
        '-b', '--branch', action='store_true',
732
        help="Create the stabilization branch for the next release.")
733
    release_prep_parser.add_argument(
734
        '-m', '--milestone', action='store_true',
735
        help="Create the next milestone and close the current milestone.")
736
    release_prep_parser.add_argument(
737
        '-i', '--issues', action='store_true',
738
        help="Move Open Issues and PRs with a milestone to the next milestone.")
739
    release_prep_parser.add_argument(
740
        '-v', '--bump-version', action='store_true',
741
        help="Bump the project version in *master* branch.")
742
743
    release_parser = subparsers.add_parser(
744
        'release', help="Tasks to be done during the stabilization phase.")
745
    release_parser.add_argument(
746
        '-m', '--message', action='store_true',
747
        help="Prepare a message to communicate the stabilization process has started.")
748
    release_parser.add_argument(
749
        '-t', '--tag', action='store_true',
750
        help="Show commands to properly tag the branch and start the release.")
751
752
    finish_parser = subparsers.add_parser(
753
        'finish', help="Tasks to be done when the release is out.")
754
    finish_parser.add_argument(
755
        '-m', '--message', action='store_true',
756
        help="Prepare a message to communicate the release is out.")
757
    return parser.parse_args()
758
759
760
def main():
761
    subcmds = dict(
762
        stats=stats,
763
        cleanup=cleanup,
764
        release_prep=release_prep,
765
        release=release,
766
        finish=finish)
767
768
    args = parse_arguments()
769
770
    try:
771
        ghs = create_github_session(args.creds_file)
772
        repo = get_repo_object(ghs, args.repository)
773
    except Exception as e:
774
        print(f'Error when getting the repository {args.repository}: {e}')
775
        exit(1)
776
777
    subcmds[args.subcmd](repo, args)
0 ignored issues
show
introduced by
The variable repo does not seem to be defined for all execution paths.
Loading history...
778
779
780
if __name__ == "__main__":
781
    main()
782