Skip to content

Commit 9f695a2

Browse files
committed
add release script and workflow
1 parent ff4fedf commit 9f695a2

File tree

9 files changed

+275
-2
lines changed

9 files changed

+275
-2
lines changed

.github/workflows/build_release.yml renamed to .github/workflows/build_publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Build and Release
1+
name: Build and Publish
22

33
on:
44
pull_request:

.github/workflows/release.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Release
2+
3+
on:
4+
schedule:
5+
# every weekday at 9:00 AM
6+
- cron: '0 9 * * 1-5'
7+
8+
jobs:
9+
release:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v1
13+
- name: Setup Python 3.7
14+
uses: actions/setup-python@v1
15+
with:
16+
python-version: 3.7
17+
- name: Install Dependencies
18+
run: pip install tox
19+
- name: Unit Tests
20+
run: tox -e py37 -- tests/unit
21+
env:
22+
AWS_DEFAULT_REGION: us-west-2
23+
- name: Create Release
24+
run: tox -e release
25+
env:
26+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

CONTRIBUTING.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,17 @@ Please remember to:
112112

113113
### Committing Your Change
114114

115+
Prefix your commit message with one of the following to indicate the version part incremented in the next release:
116+
117+
| Commit Message Prefix | Version Part Incremented
118+
| --- | ---
119+
| break, breaking | major
120+
| feat, feature | minor
121+
| depr, deprecation | minor
122+
| change, fix | patch
123+
| doc, documentation | patch
124+
| default | patch
125+
115126
For the message use imperative style and keep things concise but informative. See [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/) for guidance.
116127

117128

scripts/release.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""
2+
Invoke development tasks.
3+
"""
4+
from subprocess import check_output
5+
from version import next_version_from_current_version
6+
from subject_parser import SubjectParser
7+
from release_manager import ReleaseManager
8+
9+
10+
def recent_changes_to_src(last_version):
11+
stdout = check_output(["git", "log", "{}..HEAD".format(last_version), "--name-only", "--pretty=format: master"])
12+
stdout = stdout.decode("utf-8")
13+
lines = stdout.splitlines()
14+
src_lines = filter(lambda l: l.startswith("src"), lines)
15+
return src_lines
16+
17+
18+
def get_changes(last_version):
19+
stdout = check_output(["git", "log", "{}..HEAD".format(last_version), "--pretty=format:%s"])
20+
stdout = stdout.decode("utf-8")
21+
changes = list(map(lambda line: line.strip(), stdout.splitlines()))
22+
print(f"{len(changes)} changes since last release {last_version}")
23+
return changes
24+
25+
26+
def get_next_version(last_version, increment_type):
27+
# remove the 'v' prefix
28+
last_version = last_version[1:]
29+
return next_version_from_current_version(last_version, increment_type)
30+
31+
32+
def get_version_increment_type(last_version):
33+
stdout = check_output(["git", "log", "{}..HEAD".format(last_version), "--pretty=format:%s"])
34+
stdout = stdout.decode("utf-8")
35+
subjects = stdout.splitlines()
36+
parsed_subjects = SubjectParser(subjects)
37+
return parsed_subjects.increment_type()
38+
39+
40+
def release():
41+
"""Creates a github release."""
42+
43+
# get the last release tag
44+
stdout = check_output(["git", "describe", "--abbrev=0", "--tags"])
45+
stdout = stdout.decode("utf-8")
46+
last_version = stdout.strip()
47+
48+
if not recent_changes_to_src(last_version):
49+
print("Nothing to release.")
50+
return
51+
52+
changes = get_changes(last_version)
53+
54+
increment_type = get_version_increment_type(last_version)
55+
56+
next_version = get_next_version(last_version, increment_type)
57+
58+
manager = ReleaseManager(str(next_version), changes)
59+
manager.create_release()
60+
61+
62+
def main():
63+
release()
64+
65+
66+
if __name__ == "__main__":
67+
main()

scripts/release_manager.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from github import Github
2+
import os
3+
from pathlib import Path
4+
5+
6+
class ReleaseManager:
7+
def __init__(self, version, changes):
8+
self._version = version
9+
self._changes = changes
10+
self._token = os.getenv("GITHUB_TOKEN")
11+
self._repo = os.getenv("GITHUB_REPOSITORY")
12+
if not self._token or not self._repo:
13+
raise ValueError("Missing required environment variables.")
14+
15+
def create_release(self):
16+
tag = "v" + self._version
17+
name = f"Sagemaker Experiment SDK {tag}"
18+
template_text = Path(__file__).parent.joinpath("release_template.rst").read_text(encoding="UTF-8")
19+
change_list_content = "\n".join(list(map(lambda c: f"- {c}", self._changes)))
20+
message = template_text.format(version=tag, changes=change_list_content)
21+
g = Github(self._token)
22+
repo = g.get_repo(self._repo)
23+
# keep draft=True release script manually verified working
24+
repo.create_git_release(tag=tag, name=name, message=message, draft=True, prerelease=False)
25+
print(f"Created release {name}")
26+
print(f"See it at https://github.com/{self._repo}/releases")

scripts/release_template.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
SageMaker Experiments {version} release!
2+
3+
Changes:
4+
{changes}
5+
6+
You can upgrade from PyPI via:
7+
8+
pip install -U sagemaker-experiments

scripts/subject_parser.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import logging
2+
import re
3+
4+
5+
logger = logging.getLogger(__name__)
6+
7+
8+
class SubjectParser:
9+
"""
10+
Parses git commit subject lines to determine the type of change (breaking, feature, fix, etc...)
11+
"""
12+
13+
# order must be aligned with associated increment_type (major...post)
14+
_CHANGE_TYPES = ["breaking", "deprecation", "feature", "fix", "documentation", "infrastructure"]
15+
16+
_PARSE_SUBJECT_REGEX = re.compile(
17+
r"""
18+
(?:(?P<label>break(?:ing)?|feat(?:ure)?|depr(?:ecation)?|change|fix|doc(?:umentation)?)\s*:)?
19+
""",
20+
re.VERBOSE | re.IGNORECASE,
21+
)
22+
23+
_CANONICAL_LABELS = {
24+
"break": "breaking",
25+
"feat": "feature",
26+
"depr": "deprecation",
27+
"change": "fix",
28+
"doc": "documentation",
29+
}
30+
31+
_CHANGE_TO_INCREMENT_TYPE_MAP = {
32+
"breaking": "major",
33+
"feature": "minor",
34+
"deprecation": "minor",
35+
"fix": "patch",
36+
"change": "patch",
37+
"documentation": "patch",
38+
}
39+
40+
_DEFAULT_LABEL = "fix"
41+
42+
def __init__(self, subjects):
43+
self._groups = {}
44+
self._add_subjects(subjects)
45+
46+
def _add_subjects(self, subjects):
47+
for subject in subjects:
48+
self._parse_subject(subject)
49+
50+
def _parse_subject(self, subject):
51+
label = None
52+
match = SubjectParser._PARSE_SUBJECT_REGEX.search(subject)
53+
54+
if match:
55+
label = match.group("label") or SubjectParser._DEFAULT_LABEL
56+
label = SubjectParser._CANONICAL_LABELS.get(label, label)
57+
else:
58+
print(f"no match {subject}")
59+
label = SubjectParser._DEFAULT_LABEL
60+
61+
if label in self._groups:
62+
self._groups[label].append(subject)
63+
64+
def increment_type(self):
65+
for change_type in SubjectParser._CHANGE_TYPES:
66+
if change_type in self._groups:
67+
return SubjectParser._CHANGE_TO_INCREMENT_TYPE_MAP[change_type]
68+
69+
return "patch"

scripts/version.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import re
2+
3+
4+
# a subset of PEP 440
5+
_VERSION_REGEX = re.compile(
6+
r"""
7+
^\s*
8+
v?
9+
(?P<major>\d+)
10+
(?:\.(?P<minor>\d+))?
11+
(?:\.(?P<patch>\d+))?
12+
\s*$
13+
""",
14+
re.VERBOSE | re.IGNORECASE,
15+
)
16+
17+
18+
class Version:
19+
"""
20+
Represents a major.minor.patch version string
21+
"""
22+
23+
def __init__(self, major, minor=0, patch=0):
24+
self.major = major
25+
self.minor = minor
26+
self.patch = patch
27+
28+
self.tag = f"v{str(self)}"
29+
30+
def __str__(self):
31+
parts = [str(x) for x in [self.major, self.minor, self.patch]]
32+
33+
return ".".join(parts).lower()
34+
35+
def increment(self, increment_type):
36+
incr = None
37+
if increment_type == "major":
38+
incr = Version(self.major + 1)
39+
elif increment_type == "minor":
40+
incr = Version(self.major, self.minor + 1)
41+
elif increment_type == "patch":
42+
incr = Version(self.major, self.minor, self.patch + 1)
43+
44+
return incr
45+
46+
47+
def parse(version):
48+
match = _VERSION_REGEX.search(version)
49+
if not match:
50+
raise ValueError(f"invalid version: {version}")
51+
52+
return Version(int(match.group("major") or 0), int(match.group("minor") or 0), int(match.group("patch") or 0),)
53+
54+
55+
def next_version_from_current_version(current_version, increment_type):
56+
return parse(current_version).increment(increment_type)

tox.ini

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,14 @@ deps =
117117
sphinx-rtd-theme
118118
readthedocs-sphinx-ext
119119
commands =
120-
sphinx-build -T -W -b html -d _build/doctrees-readthedocs -D language=en . _build/html
120+
sphinx-build -T -W -b html -d _build/doctrees-readthedocs -D language=en . _build/html
121+
122+
[testenv:release]
123+
description = create a GitHub release, version number is derived from commit messages
124+
basepython = python3
125+
passenv =
126+
GITHUB_*
127+
deps =
128+
PyGithub
129+
pathlib
130+
commands = python scripts/release.py {posargs}

0 commit comments

Comments
 (0)