|
| 1 | +#!/usr/bin/python |
| 2 | +""" |
| 3 | +Small python script that, when run, will update the CHANGELOG with information |
| 4 | +about all merged pull requests since the previous release. |
| 5 | +
|
| 6 | +This script must be run after tagging the latest version |
| 7 | +It checks the log of commits since the previous tag and parses it |
| 8 | +""" |
| 9 | +import re |
| 10 | +import subprocess |
| 11 | +import sys |
| 12 | +from datetime import datetime |
| 13 | + |
| 14 | +# Regex patterns |
| 15 | +RELEASE_MD_PATTERN = re.compile(r'## \[(\d+\.\d+\.\d+)\]') |
| 16 | +MERGED_PR_PATTERN = re.compile( |
| 17 | + r'([0-9a-f]{7}) Merge pull request #(\d+) from (.+)/.+' |
| 18 | +) |
| 19 | +TAG_PATTERN = re.compile( |
| 20 | + r'refs/tags/v(\d+\.\d+\.\d+) (\w{3} \w{3} \d{1,2} \d{2}:\d{2}:\d{2} \d{4})' |
| 21 | +) |
| 22 | + |
| 23 | +# PR Type terms |
| 24 | +FIX_TERMS = ['fix', 'change', 'update'] |
| 25 | + |
| 26 | + |
| 27 | +# Helper functions |
| 28 | +def generate_pr_link(pr_num): |
| 29 | + """ |
| 30 | + Returns a markdown link to a PR in this repo given its number |
| 31 | + """ |
| 32 | + return ( |
| 33 | + '[PR #{0}](https://github.com/sendgrid/smtpapi-python/pulls/{0})' |
| 34 | + ).format(pr_num) |
| 35 | + |
| 36 | + |
| 37 | +def generate_user_link(user): |
| 38 | + """ |
| 39 | + Returns a markdown link to a user |
| 40 | + """ |
| 41 | + return '[@{0}](https://github.com/{0})'.format(user) |
| 42 | + |
| 43 | + |
| 44 | +# Get latest tag |
| 45 | +command = ['git', 'tag', '--format=%(refname) %(creatordate)'] |
| 46 | +res = subprocess.run(command, capture_output=True, text=True) |
| 47 | +if res.returncode != 0: |
| 48 | + print('Error occurred when running git tag command:', str(res.stderr)) |
| 49 | + sys.exit(1) |
| 50 | +# Get the last line and get the tag number |
| 51 | +latest_release_match = TAG_PATTERN.match( |
| 52 | + list(filter(None, res.stdout.split('\n')))[-1], |
| 53 | +) |
| 54 | +latest_release = latest_release_match[1] |
| 55 | +latest_release_date = datetime.strptime( |
| 56 | + latest_release_match[2], '%a %b %d %H:%M:%S %Y', |
| 57 | +) |
| 58 | +print('Generating CHANGELOG for', latest_release) |
| 59 | + |
| 60 | +# Read in the CHANGELOG file first |
| 61 | +with open('CHANGELOG.md') as f: |
| 62 | + # Read the text in as a list of lines |
| 63 | + old_text = f.readlines() |
| 64 | + # Get the latest release (top of the CHANGELOG) |
| 65 | + for line in old_text: |
| 66 | + match = RELEASE_MD_PATTERN.match(line) |
| 67 | + if match: |
| 68 | + prev_release = match[1] |
| 69 | + break |
| 70 | + |
| 71 | +if latest_release == prev_release: |
| 72 | + print( |
| 73 | + 'The latest git tag matches the last release in the CHANGELOG. ' |
| 74 | + 'Please tag the repository before running this script.' |
| 75 | + ) |
| 76 | + sys.exit(1) |
| 77 | + |
| 78 | +# Use git log to list all commits between that tag and HEAD |
| 79 | +command = 'git log --oneline v{}..@'.format(prev_release).split(' ') |
| 80 | +res = subprocess.run(command, capture_output=True, text=True) |
| 81 | +if res.returncode != 0: |
| 82 | + print('Error occurred when running git log command:', str(res.stderr)) |
| 83 | + sys.exit(1) |
| 84 | + |
| 85 | +# Parse the output from the above command to find all commits for merged PRs |
| 86 | +merge_commits = [] |
| 87 | +for line in res.stdout.split('\n'): |
| 88 | + match = MERGED_PR_PATTERN.match(line) |
| 89 | + if match: |
| 90 | + merge_commits.append(match) |
| 91 | + |
| 92 | +# Determine the type of PR from the commit message |
| 93 | +added, fixes = [], [] |
| 94 | +for commit in merge_commits: |
| 95 | + # Get the hash of the commit and get the message of it |
| 96 | + commit_sha = commit[1] |
| 97 | + command = 'git show {} --format=format:%B'.format(commit_sha).split(' ') |
| 98 | + res = subprocess.run(command, capture_output=True, text=True) |
| 99 | + out = res.stdout.lower() |
| 100 | + is_added = True |
| 101 | + |
| 102 | + # When storing we need the PR title, number and user |
| 103 | + data = { |
| 104 | + # 3rd line of the commit message is the PR title |
| 105 | + 'title': out.split('\n')[2], |
| 106 | + 'number': commit[2], |
| 107 | + 'user': commit[3], |
| 108 | + } |
| 109 | + |
| 110 | + for term in FIX_TERMS: |
| 111 | + if term in out: |
| 112 | + fixes.append(data) |
| 113 | + is_added = False |
| 114 | + break |
| 115 | + if is_added: |
| 116 | + added.append(data) |
| 117 | + |
| 118 | +# Now we need to write out the CHANGELOG again |
| 119 | +with open('CHANGELOG.md', 'w') as f: |
| 120 | + # Write out the header lines first |
| 121 | + for i in range(0, 3): |
| 122 | + f.write(old_text[i]) |
| 123 | + |
| 124 | + # Create and write out the new version information |
| 125 | + latest_release_date_string = latest_release_date.strftime('%Y-%m-%d') |
| 126 | + f.write('## [{}] - {} ##\n'.format( |
| 127 | + latest_release, |
| 128 | + latest_release_date_string, |
| 129 | + )) |
| 130 | + # Add the stuff that was added |
| 131 | + f.write('### Added\n') |
| 132 | + for commit in added: |
| 133 | + f.write('- {}: {}{} (via {})\n'.format( |
| 134 | + generate_pr_link(commit['number']), |
| 135 | + commit['title'], |
| 136 | + '.' if commit['title'][-1] != '.' else '', |
| 137 | + generate_user_link(commit['user']) |
| 138 | + )) |
| 139 | + f.write('\n') |
| 140 | + # Add the fixes |
| 141 | + f.write('### Fixes\n') |
| 142 | + for commit in fixes: |
| 143 | + f.write('- {}: {}{} (via {})\n'.format( |
| 144 | + generate_pr_link(commit['number']), |
| 145 | + commit['title'], |
| 146 | + '.' if commit['title'][-1] != '.' else '', |
| 147 | + generate_user_link(commit['user']) |
| 148 | + )) |
| 149 | + f.write('\n') |
| 150 | + |
| 151 | + # Add the old stuff |
| 152 | + for i in range(3, len(old_text)): |
| 153 | + f.write(old_text[i]) |
0 commit comments