-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
MRG: Merge pull request #86 from octue/release/0.0.21
Enable continuous deployment with semantic versions
- Loading branch information
Showing
8 changed files
with
277 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import os | ||
import subprocess | ||
import sys | ||
|
||
|
||
PACKAGE_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) | ||
|
||
|
||
RED = '\033[0;31m' | ||
GREEN = "\033[0;32m" | ||
NO_COLOUR = '\033[0m' | ||
|
||
|
||
def get_setup_version(): | ||
process = subprocess.run(["python", "setup.py", "--version"], capture_output=True) | ||
return process.stdout.strip().decode("utf8") | ||
|
||
|
||
def get_expected_semantic_version(): | ||
process = subprocess.run(["git-mkver", "next"], capture_output=True) | ||
return process.stdout.strip().decode("utf8") | ||
|
||
|
||
if __name__ == "__main__": | ||
os.chdir(PACKAGE_ROOT) | ||
setup_version = get_setup_version() | ||
expected_semantic_version = get_expected_semantic_version() | ||
|
||
if setup_version != expected_semantic_version: | ||
print( | ||
f"{RED}VERSION FAILED CHECKS:{NO_COLOUR} The version stated in 'setup.py' ({setup_version}) is different " | ||
f"from the expected semantic version ({expected_semantic_version})." | ||
) | ||
sys.exit(1) | ||
|
||
print( | ||
f"{GREEN}VERSION PASSED CHECKS:{NO_COLOUR} The version stated in 'setup.py' is the same as the expected " | ||
f"semantic version: {expected_semantic_version}." | ||
) | ||
sys.exit(0) |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
import re | ||
import subprocess | ||
import sys | ||
|
||
|
||
LAST_RELEASE = "LAST_RELEASE" | ||
LAST_PULL_REQUEST = "LAST_PULL_REQUEST" | ||
|
||
SEMANTIC_VERSION_PATTERN = r"tag: (\d+\.\d+\.\d+)" | ||
PULL_REQUEST_INDICATOR = "Merge pull request #" | ||
|
||
COMMIT_CODES_TO_HEADINGS_MAPPING = { | ||
"FEA": "### New features", | ||
"ENH": "### Enhancements", | ||
"FIX": "### Fixes", | ||
"OPS": "### Operations", | ||
"DEP": "### Dependencies", | ||
"REF": "### Refactoring", | ||
"TST": "### Testing", | ||
"MRG": "### Other", | ||
"REV": "### Reversions", | ||
"CHO": "### Chores", | ||
"WIP": "### Other", | ||
"DOC": "### Other", | ||
"STY": "### Other", | ||
} | ||
|
||
AUTO_GENERATION_START_INDICATOR = "<!--- START AUTOGENERATED NOTES --->" | ||
AUTO_GENERATION_END_INDICATOR = "<!--- END AUTOGENERATED NOTES --->" | ||
|
||
|
||
class ReleaseNoteCompiler: | ||
"""A release/pull request notes compiler. The notes are pulled together from Conventional Commit messages, stopping | ||
at the specified stop point. The stop point can either be the last merged pull request in the branch or the last | ||
semantically-versioned release tagged in the branch. If previous notes are provided, only the text between the | ||
comment lines `<!--- START AUTOGENERATED NOTES --->` and `<!--- END AUTOGENERATED NOTES --->` will be replaced - | ||
anything outside of this will appear in the new release notes. | ||
:param str header: | ||
:param str|None previous_notes: | ||
:param str list_item_symbol: | ||
:param dict|None commit_codes_to_headings_mapping: | ||
:param str stop_point: | ||
:return None: | ||
""" | ||
|
||
def __init__( | ||
self, | ||
stop_point, | ||
previous_notes=None, | ||
header="## Contents", | ||
list_item_symbol="- [x] ", | ||
commit_codes_to_headings_mapping=None, | ||
): | ||
if stop_point.upper() not in {LAST_RELEASE, LAST_PULL_REQUEST}: | ||
raise ValueError(f"`stop_point` must be one of {LAST_RELEASE, LAST_PULL_REQUEST!r}; received {stop_point!r}.") | ||
|
||
self.stop_point = stop_point.upper() | ||
self.previous_notes = previous_notes | ||
self.header = header | ||
self.list_item_symbol = list_item_symbol | ||
self.commit_codes_to_headings_mapping = commit_codes_to_headings_mapping or COMMIT_CODES_TO_HEADINGS_MAPPING | ||
|
||
def compile_release_notes(self): | ||
"""Compile the release or pull request notes into a multiline string, sorting the commit messages into headed | ||
sections according to their commit codes and the commit-codes-to-headings mapping. | ||
:return str: | ||
""" | ||
git_log = subprocess.run(["git", "log", "--pretty=format:%s|%d"], capture_output=True).stdout.strip().decode() | ||
parsed_commits, unparsed_commits = self._parse_commit_messages(git_log) | ||
categorised_commit_messages = self._categorise_commit_messages(parsed_commits, unparsed_commits) | ||
autogenerated_release_notes = self._build_release_notes(categorised_commit_messages) | ||
|
||
if not self.previous_notes: | ||
return autogenerated_release_notes | ||
|
||
previous_notes_lines = self.previous_notes.splitlines() | ||
|
||
for i, line in enumerate(previous_notes_lines): | ||
if AUTO_GENERATION_START_INDICATOR in line: | ||
autogeneration_start_index = i | ||
|
||
elif AUTO_GENERATION_END_INDICATOR in line: | ||
autogeneration_end_index = i + 1 | ||
|
||
try: | ||
return "\n".join( | ||
( | ||
*previous_notes_lines[:autogeneration_start_index], | ||
autogenerated_release_notes, | ||
*previous_notes_lines[autogeneration_end_index:] | ||
) | ||
) | ||
|
||
except NameError: | ||
return autogenerated_release_notes | ||
|
||
def _parse_commit_messages(self, formatted_oneline_git_log): | ||
"""Parse commit messages from the git log (formatted using `--pretty=format:%s|%d`) until the stop point is | ||
reached. The parsed commit messages are returned separately to any that fail to parse. | ||
:param str formatted_oneline_git_log: | ||
:return list(tuple), list(str): | ||
""" | ||
parsed_commits = [] | ||
unparsed_commits = [] | ||
|
||
for commit in formatted_oneline_git_log.splitlines(): | ||
split_commit = commit.split("|") | ||
|
||
if len(split_commit) == 2: | ||
message, decoration = split_commit | ||
|
||
try: | ||
code, message = message.split(":") | ||
except ValueError: | ||
unparsed_commits.append(message.strip()) | ||
continue | ||
|
||
if self.stop_point == LAST_RELEASE: | ||
if "tag" in decoration: | ||
if re.compile(SEMANTIC_VERSION_PATTERN).search(decoration): | ||
break | ||
|
||
if self.stop_point == LAST_PULL_REQUEST: | ||
if PULL_REQUEST_INDICATOR in message: | ||
break | ||
|
||
parsed_commits.append((code.strip(), message.strip(), decoration.strip())) | ||
|
||
return parsed_commits, unparsed_commits | ||
|
||
def _categorise_commit_messages(self, parsed_commits, unparsed_commits): | ||
"""Categorise the commit messages into headed sections. Unparsed commits are put under an "uncategorised" | ||
header. | ||
:param iter(tuple)) parsed_commits: | ||
:param iter(str) unparsed_commits: | ||
:return dict: | ||
""" | ||
release_notes = {heading: [] for heading in self.commit_codes_to_headings_mapping.values()} | ||
|
||
for code, message, _ in parsed_commits: | ||
try: | ||
release_notes[self.commit_codes_to_headings_mapping[code]].append(message) | ||
except KeyError: | ||
release_notes["### Other"].append(message) | ||
|
||
release_notes["### Uncategorised!"] = unparsed_commits | ||
return release_notes | ||
|
||
def _build_release_notes(self, categorised_commit_messages): | ||
"""Build the the categorised commit messages into a single multi-line string ready to be used as formatted | ||
release notes. | ||
:param dict categorised_commit_messages: | ||
:return str: | ||
""" | ||
release_notes_for_printing = f"{AUTO_GENERATION_START_INDICATOR}\n{self.header}\n\n" | ||
|
||
for heading, notes in categorised_commit_messages.items(): | ||
|
||
if len(notes) == 0: | ||
continue | ||
|
||
note_lines = "\n".join(self.list_item_symbol + note for note in notes) | ||
release_notes_for_printing += f"{heading}\n{note_lines}\n\n" | ||
|
||
release_notes_for_printing += AUTO_GENERATION_END_INDICATOR | ||
return release_notes_for_printing | ||
|
||
|
||
if __name__ == "__main__": | ||
release_notes = ReleaseNoteCompiler(*sys.argv[1:]).compile_release_notes() | ||
print(release_notes) |
Oops, something went wrong.