Skip to content

Commit a0bfd79

Browse files
authored
Merge pull request #356 from FoamyGuy/automated_releaser
adding automated releaser
2 parents b08b400 + 2502c31 commit a0bfd79

File tree

6 files changed

+325
-1
lines changed

6 files changed

+325
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ env.sh
1515
.cp_org/*
1616
.blinka/*
1717
.vscode
18+
.idea/*

README.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,45 @@ run the following command:
160160
# the help argument to display usage.
161161
python3 -m adabot.circuitpython_library_patches -h
162162
163+
164+
Making Releases For CircuitPython Libraries
165+
===========================================
166+
Adabot includes a utility to check if a library needs a new release
167+
and to help a human create the release with a CLI instead of the
168+
web interface.
169+
170+
To use it:
171+
172+
1. Clone the adabot repo locally and open a terminal inside of it
173+
2. Run ``pip install .`` in the root of Adabot repo to install it via pip
174+
3. Clone the library repo locally
175+
4. ``cd`` into the library repo
176+
5. run ``python -m adabot.circuitpython_library_release``
177+
6. Answer the prompts for new tag name and title.
178+
179+
This utility can be used in conjunction with ``git submodule foreach`` inside of the
180+
CircuitPython Library Bundle.
181+
182+
These are the steps for that process:
183+
184+
1. Clone the adabot repo locally and open a terminal inside of it
185+
2. If you want to use the same title for all libraries (i.e. due to a patch rollout)
186+
then modify the ``RELEASE_TITLE`` dictionary value at the top
187+
of ``adabot/circuitpython_library_release.py``
188+
3. Run ``pip install .`` in the root of Adabot repo to install it via pip
189+
4. Clone the Library Bundle repo and open a terminal inside of it
190+
5. Run these commands to update all submodules
191+
192+
.. code-block:: shell
193+
194+
git submodule sync --quiet --recursive
195+
git submodule update --init
196+
197+
198+
6. Run ``git submodule foreach 'python -m adabot.circuitpython_library_release'``
199+
200+
201+
163202
Contributing
164203
============
165204

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
# SPDX-FileCopyrightText: 2023 Tim Cocks for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
"""
6+
Check if a new release needs to be made, and if so, make it.
7+
"""
8+
import subprocess
9+
import logging
10+
from datetime import datetime
11+
import toml
12+
from jinja2 import Template
13+
14+
# Empty RELEASE_TITLE will prompt to ask for a title for each release.
15+
# Set a value here if you want to use the same string for the title of all releases
16+
config = {"RELEASE_TITLE": ""}
17+
18+
release_date_format = "%Y-%m-%dT%H:%M:%SZ"
19+
commit_date_format = "%a %b %d %H:%M:%S %Y"
20+
21+
VALID_MENU_CHOICES = ("1", "2", "3", "4", "")
22+
23+
24+
def make_release(new_tag, logger, test_run=False):
25+
"""
26+
Make the release
27+
"""
28+
# pylint: disable=line-too-long
29+
30+
while config["RELEASE_TITLE"] == "":
31+
config["RELEASE_TITLE"] = input("Enter a Release Title: ")
32+
33+
if not test_run:
34+
make_release_result = subprocess.getoutput(
35+
f"gh release create {new_tag} -F release_notes.md -t '{new_tag} - {config['RELEASE_TITLE']}'"
36+
)
37+
38+
if logger is not None:
39+
logger.info(make_release_result)
40+
else:
41+
print(make_release_result)
42+
else:
43+
print("would run: ")
44+
print(
45+
"gh release create {new_tag} -F release_notes.md -t '{new_tag} - {config['RELEASE_TITLE']}'"
46+
)
47+
48+
49+
def create_release_notes(pypi_name):
50+
"""
51+
render the release notes into a md file.
52+
"""
53+
# pylint: disable=line-too-long
54+
RELEASE_NOTES_TEMPLATE = """To use in CircuitPython, simply install the [Adafruit CircuitPython Bundle](https://circuitpython.org/libraries).
55+
56+
To use in CPython, `pip3 install {{ pypi_name }}`.
57+
58+
Read the [docs](https://circuitpython.readthedocs.io/projects/{{ pypi_name }}/en/latest/) for info on how to use it."""
59+
60+
release_notes_template = Template(RELEASE_NOTES_TEMPLATE)
61+
62+
_rendered_template_text = release_notes_template.render(pypi_name=pypi_name)
63+
64+
with open("release_notes.md", "w") as f:
65+
f.write(_rendered_template_text)
66+
67+
68+
if __name__ == "__main__":
69+
create_release_notes("testrepo")
70+
71+
72+
def get_pypi_name():
73+
"""
74+
return the shorthand pypi project name
75+
"""
76+
data = toml.load("pyproject.toml")
77+
78+
return data["project"]["name"].replace("adafruit-circuitpython-", "")
79+
80+
81+
def needs_new_release(logger):
82+
"""
83+
return true if there are commits newer than the latest release
84+
"""
85+
last_commit_time = subprocess.getoutput(
86+
" TZ=UTC0 git log -1 --date=local --format='%cd'"
87+
)
88+
logger.info(f"last commit: {last_commit_time}")
89+
90+
last_commit_date_obj = datetime.strptime(last_commit_time, commit_date_format)
91+
92+
release_info = get_release_info()
93+
94+
logger.info(f"Latest release is: {release_info['current_tag']}")
95+
logger.info(f"createdAt: {release_info['created_at']}")
96+
97+
release_date_obj = datetime.strptime(
98+
release_info["created_at"], release_date_format
99+
)
100+
return release_date_obj < last_commit_date_obj
101+
102+
103+
def bump_major(tag_symver):
104+
"""
105+
Returns a string with a new tag created by incrementing
106+
the major version of the given semantic version tag.
107+
"""
108+
tag_parts = tag_symver.split(".")
109+
tag_parts[0] = str(int(tag_parts[0]) + 1)
110+
tag_parts[1] = "0"
111+
tag_parts[2] = "0"
112+
return ".".join(tag_parts)
113+
114+
115+
def bump_minor(tag_symver):
116+
"""
117+
Returns a string with a new tag created by incrementing
118+
the minor version of the given semantic version tag.
119+
"""
120+
tag_parts = tag_symver.split(".")
121+
tag_parts[1] = str(int(tag_parts[1]) + 1)
122+
tag_parts[2] = "0"
123+
return ".".join(tag_parts)
124+
125+
126+
def bump_patch(tag_symver):
127+
"""
128+
Returns a string with a new tag created by incrementing
129+
the patch version of the given semantic version tag.
130+
"""
131+
tag_parts = tag_symver.split(".")
132+
tag_parts[-1] = str(int(tag_parts[-1]) + 1)
133+
return ".".join(tag_parts)
134+
135+
136+
def get_release_info():
137+
"""
138+
return a dictionary of info about the latest release
139+
"""
140+
result = subprocess.getoutput("gh release list -L 1 | awk 2")
141+
createdAt = result.split("\t")[-1]
142+
tag = result.split("\t")[-2]
143+
return {
144+
"current_tag": tag,
145+
"new_tag_patch": bump_patch(tag),
146+
"new_tag_minor": bump_minor(tag),
147+
"new_tag_major": bump_major(tag),
148+
"created_at": createdAt,
149+
}
150+
151+
152+
def get_compare_url(tag_name):
153+
"""
154+
Get the URL to the GitHub compare page for the latest release compared
155+
to current main.
156+
"""
157+
remote_url = subprocess.getoutput("git ls-remote --get-url origin")
158+
if not remote_url.startswith("https"):
159+
remote_url = subprocess.getoutput("git ls-remote --get-url adafruit")
160+
161+
if not remote_url.startswith("https"):
162+
return "Sorry, Unknown Remotes"
163+
164+
compare_url = remote_url.replace(".git", f"/compare/{tag_name}...main")
165+
return compare_url
166+
167+
168+
def main_cli():
169+
"""
170+
Main CLI entry point
171+
"""
172+
logging.basicConfig(
173+
level=logging.INFO,
174+
format="%(asctime)s [%(levelname)s] %(message)s",
175+
handlers=[
176+
logging.FileHandler("../../../automated_releaser.log"),
177+
logging.StreamHandler(),
178+
],
179+
)
180+
181+
def menu_prompt(release_info):
182+
"""
183+
Prompt the user to ask which part of the symantic version should be
184+
incremented, or if the library release should be skipped.
185+
Returns the choice inputted by the user.
186+
"""
187+
print("This library needs a new release. Please select a choice:")
188+
print(f"Changes: {get_compare_url(release_info['current_tag'])}")
189+
print(
190+
f"1. *default* Bump Patch, new tag would be: {release_info['new_tag_patch']}"
191+
)
192+
print(f"2. Bump Minor, new tag would be: {release_info['new_tag_minor']}")
193+
print(f"3. Bump Major, new tag would be: {release_info['new_tag_major']}")
194+
print("4. Skip releasing this library and go to next in the list")
195+
return input("Choice, enter blank for default: ")
196+
197+
result = subprocess.getoutput("git checkout main")
198+
199+
result = subprocess.getoutput("pwd")
200+
logging.info("Checking: %s", "/".join(result.split("/")[-3:]))
201+
202+
if needs_new_release(logging):
203+
release_info = get_release_info()
204+
choice = menu_prompt(release_info)
205+
while choice not in VALID_MENU_CHOICES:
206+
logging.info("Error: Invalid Selection '%s'", choice)
207+
choice = menu_prompt(release_info)
208+
209+
if choice in ("1", ""):
210+
logging.info(
211+
"Making a new release with tag: %s", release_info["new_tag_patch"]
212+
)
213+
create_release_notes(get_pypi_name())
214+
make_release(release_info["new_tag_patch"], logging)
215+
elif choice == "2":
216+
logging.info(
217+
"Making a new release with tag: %s", release_info["new_tag_minor"]
218+
)
219+
create_release_notes(get_pypi_name())
220+
make_release(release_info["new_tag_minor"], logging)
221+
elif choice == "3":
222+
logging.info(
223+
"Making a new release with tag: %s", release_info["new_tag_major"]
224+
)
225+
create_release_notes(get_pypi_name())
226+
make_release(release_info["new_tag_major"], logging)
227+
elif choice == "4":
228+
logging.info("Skipping release.")
229+
230+
else:
231+
logging.info("No new commits since last release, skipping")
232+
233+
234+
if __name__ == "__main__":
235+
main_cli()

pyproject.toml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# SPDX-FileCopyrightText: 2022 Alec Delaney for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
[build-system]
6+
requires = [
7+
"setuptools",
8+
"wheel",
9+
"setuptools-scm",
10+
]
11+
12+
[project]
13+
name = "adafruit-adabot"
14+
description = "Adabot is our robot friend who helps Adafruit online "
15+
version = "0.0.0+auto.0"
16+
readme = "README.rst"
17+
authors = [
18+
{name = "Adafruit Industries", email = "[email protected]"}
19+
]
20+
urls = {Homepage = "https://github.com/adafruit/adabot"}
21+
keywords = [
22+
"adafruit",
23+
"micropython",
24+
"circuitpython",
25+
"automation",
26+
]
27+
license = {text = "MIT"}
28+
classifiers = [
29+
"Intended Audience :: Developers",
30+
"Topic :: Software Development :: Libraries",
31+
"Topic :: Software Development :: Embedded Systems",
32+
"Topic :: System :: Hardware",
33+
"License :: OSI Approved :: MIT License",
34+
"Programming Language :: Python :: 3",
35+
]
36+
dynamic = ["dependencies", "optional-dependencies"]
37+
38+
[project.scripts]
39+
adabot-release = "adabot.circuitpython_library_release:main_cli"
40+
41+
[tool.setuptools]
42+
packages = ["adabot"]
43+
44+
[tool.setuptools.dynamic]
45+
dependencies = {file = ["requirements.txt"]}
46+
optional-dependencies = {optional = {file = ["optional_requirements.txt"]}}

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ black==22.3.0
66
packaging==20.3
77
pylint==2.11.1
88
pytest
9-
pyyaml==5.4.1
9+
pyyaml>=5.4.1
1010
redis==4.5.4
1111
requests==2.31.0
1212
sh==1.12.14
@@ -17,3 +17,5 @@ PyGithub==1.57
1717
typing-extensions~=4.0
1818
google-auth~=2.13
1919
google-cloud-bigquery~=3.3
20+
toml
21+
jinja2

tools/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
# Adabot Tools and Scripts
23

34

0 commit comments

Comments
 (0)