diff --git a/.gitignore b/.gitignore index a1aed8a..1d3bda6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ env.sh .cp_org/* .blinka/* .vscode +.idea/* diff --git a/README.rst b/README.rst index 6f03814..5e4278f 100644 --- a/README.rst +++ b/README.rst @@ -160,6 +160,45 @@ run the following command: # the help argument to display usage. python3 -m adabot.circuitpython_library_patches -h + +Making Releases For CircuitPython Libraries +=========================================== +Adabot includes a utility to check if a library needs a new release +and to help a human create the release with a CLI instead of the +web interface. + +To use it: + +1. Clone the adabot repo locally and open a terminal inside of it +2. Run ``pip install .`` in the root of Adabot repo to install it via pip +3. Clone the library repo locally +4. ``cd`` into the library repo +5. run ``python -m adabot.circuitpython_library_release`` +6. Answer the prompts for new tag name and title. + +This utility can be used in conjunction with ``git submodule foreach`` inside of the +CircuitPython Library Bundle. + +These are the steps for that process: + +1. Clone the adabot repo locally and open a terminal inside of it +2. If you want to use the same title for all libraries (i.e. due to a patch rollout) + then modify the ``RELEASE_TITLE`` dictionary value at the top + of ``adabot/circuitpython_library_release.py`` +3. Run ``pip install .`` in the root of Adabot repo to install it via pip +4. Clone the Library Bundle repo and open a terminal inside of it +5. Run these commands to update all submodules + +.. code-block:: shell + + git submodule sync --quiet --recursive + git submodule update --init + + +6. Run ``git submodule foreach 'python -m adabot.circuitpython_library_release'`` + + + Contributing ============ diff --git a/adabot/circuitpython_library_release.py b/adabot/circuitpython_library_release.py new file mode 100644 index 0000000..a242d04 --- /dev/null +++ b/adabot/circuitpython_library_release.py @@ -0,0 +1,235 @@ +# SPDX-FileCopyrightText: 2023 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +Check if a new release needs to be made, and if so, make it. +""" +import subprocess +import logging +from datetime import datetime +import toml +from jinja2 import Template + +# Empty RELEASE_TITLE will prompt to ask for a title for each release. +# Set a value here if you want to use the same string for the title of all releases +config = {"RELEASE_TITLE": ""} + +release_date_format = "%Y-%m-%dT%H:%M:%SZ" +commit_date_format = "%a %b %d %H:%M:%S %Y" + +VALID_MENU_CHOICES = ("1", "2", "3", "4", "") + + +def make_release(new_tag, logger, test_run=False): + """ + Make the release + """ + # pylint: disable=line-too-long + + while config["RELEASE_TITLE"] == "": + config["RELEASE_TITLE"] = input("Enter a Release Title: ") + + if not test_run: + make_release_result = subprocess.getoutput( + f"gh release create {new_tag} -F release_notes.md -t '{new_tag} - {config['RELEASE_TITLE']}'" + ) + + if logger is not None: + logger.info(make_release_result) + else: + print(make_release_result) + else: + print("would run: ") + print( + "gh release create {new_tag} -F release_notes.md -t '{new_tag} - {config['RELEASE_TITLE']}'" + ) + + +def create_release_notes(pypi_name): + """ + render the release notes into a md file. + """ + # pylint: disable=line-too-long + RELEASE_NOTES_TEMPLATE = """To use in CircuitPython, simply install the [Adafruit CircuitPython Bundle](https://circuitpython.org/libraries). + +To use in CPython, `pip3 install {{ pypi_name }}`. + +Read the [docs](https://circuitpython.readthedocs.io/projects/{{ pypi_name }}/en/latest/) for info on how to use it.""" + + release_notes_template = Template(RELEASE_NOTES_TEMPLATE) + + _rendered_template_text = release_notes_template.render(pypi_name=pypi_name) + + with open("release_notes.md", "w") as f: + f.write(_rendered_template_text) + + +if __name__ == "__main__": + create_release_notes("testrepo") + + +def get_pypi_name(): + """ + return the shorthand pypi project name + """ + data = toml.load("pyproject.toml") + + return data["project"]["name"].replace("adafruit-circuitpython-", "") + + +def needs_new_release(logger): + """ + return true if there are commits newer than the latest release + """ + last_commit_time = subprocess.getoutput( + " TZ=UTC0 git log -1 --date=local --format='%cd'" + ) + logger.info(f"last commit: {last_commit_time}") + + last_commit_date_obj = datetime.strptime(last_commit_time, commit_date_format) + + release_info = get_release_info() + + logger.info(f"Latest release is: {release_info['current_tag']}") + logger.info(f"createdAt: {release_info['created_at']}") + + release_date_obj = datetime.strptime( + release_info["created_at"], release_date_format + ) + return release_date_obj < last_commit_date_obj + + +def bump_major(tag_symver): + """ + Returns a string with a new tag created by incrementing + the major version of the given semantic version tag. + """ + tag_parts = tag_symver.split(".") + tag_parts[0] = str(int(tag_parts[0]) + 1) + tag_parts[1] = "0" + tag_parts[2] = "0" + return ".".join(tag_parts) + + +def bump_minor(tag_symver): + """ + Returns a string with a new tag created by incrementing + the minor version of the given semantic version tag. + """ + tag_parts = tag_symver.split(".") + tag_parts[1] = str(int(tag_parts[1]) + 1) + tag_parts[2] = "0" + return ".".join(tag_parts) + + +def bump_patch(tag_symver): + """ + Returns a string with a new tag created by incrementing + the patch version of the given semantic version tag. + """ + tag_parts = tag_symver.split(".") + tag_parts[-1] = str(int(tag_parts[-1]) + 1) + return ".".join(tag_parts) + + +def get_release_info(): + """ + return a dictionary of info about the latest release + """ + result = subprocess.getoutput("gh release list -L 1 | awk 2") + createdAt = result.split("\t")[-1] + tag = result.split("\t")[-2] + return { + "current_tag": tag, + "new_tag_patch": bump_patch(tag), + "new_tag_minor": bump_minor(tag), + "new_tag_major": bump_major(tag), + "created_at": createdAt, + } + + +def get_compare_url(tag_name): + """ + Get the URL to the GitHub compare page for the latest release compared + to current main. + """ + remote_url = subprocess.getoutput("git ls-remote --get-url origin") + if not remote_url.startswith("https"): + remote_url = subprocess.getoutput("git ls-remote --get-url adafruit") + + if not remote_url.startswith("https"): + return "Sorry, Unknown Remotes" + + compare_url = remote_url.replace(".git", f"/compare/{tag_name}...main") + return compare_url + + +def main_cli(): + """ + Main CLI entry point + """ + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + handlers=[ + logging.FileHandler("../../../automated_releaser.log"), + logging.StreamHandler(), + ], + ) + + def menu_prompt(release_info): + """ + Prompt the user to ask which part of the symantic version should be + incremented, or if the library release should be skipped. + Returns the choice inputted by the user. + """ + print("This library needs a new release. Please select a choice:") + print(f"Changes: {get_compare_url(release_info['current_tag'])}") + print( + f"1. *default* Bump Patch, new tag would be: {release_info['new_tag_patch']}" + ) + print(f"2. Bump Minor, new tag would be: {release_info['new_tag_minor']}") + print(f"3. Bump Major, new tag would be: {release_info['new_tag_major']}") + print("4. Skip releasing this library and go to next in the list") + return input("Choice, enter blank for default: ") + + result = subprocess.getoutput("git checkout main") + + result = subprocess.getoutput("pwd") + logging.info("Checking: %s", "/".join(result.split("/")[-3:])) + + if needs_new_release(logging): + release_info = get_release_info() + choice = menu_prompt(release_info) + while choice not in VALID_MENU_CHOICES: + logging.info("Error: Invalid Selection '%s'", choice) + choice = menu_prompt(release_info) + + if choice in ("1", ""): + logging.info( + "Making a new release with tag: %s", release_info["new_tag_patch"] + ) + create_release_notes(get_pypi_name()) + make_release(release_info["new_tag_patch"], logging) + elif choice == "2": + logging.info( + "Making a new release with tag: %s", release_info["new_tag_minor"] + ) + create_release_notes(get_pypi_name()) + make_release(release_info["new_tag_minor"], logging) + elif choice == "3": + logging.info( + "Making a new release with tag: %s", release_info["new_tag_major"] + ) + create_release_notes(get_pypi_name()) + make_release(release_info["new_tag_major"], logging) + elif choice == "4": + logging.info("Skipping release.") + + else: + logging.info("No new commits since last release, skipping") + + +if __name__ == "__main__": + main_cli() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..66f522b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2022 Alec Delaney for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +[build-system] +requires = [ + "setuptools", + "wheel", + "setuptools-scm", +] + +[project] +name = "adafruit-adabot" +description = "Adabot is our robot friend who helps Adafruit online " +version = "0.0.0+auto.0" +readme = "README.rst" +authors = [ + {name = "Adafruit Industries", email = "circuitpython@adafruit.com"} +] +urls = {Homepage = "https://github.com/adafruit/adabot"} +keywords = [ + "adafruit", + "micropython", + "circuitpython", + "automation", +] +license = {text = "MIT"} +classifiers = [ + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Embedded Systems", + "Topic :: System :: Hardware", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", +] +dynamic = ["dependencies", "optional-dependencies"] + +[project.scripts] +adabot-release = "adabot.circuitpython_library_release:main_cli" + +[tool.setuptools] +packages = ["adabot"] + +[tool.setuptools.dynamic] +dependencies = {file = ["requirements.txt"]} +optional-dependencies = {optional = {file = ["optional_requirements.txt"]}} diff --git a/requirements.txt b/requirements.txt index fd50094..504b731 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ black==22.3.0 packaging==20.3 pylint==2.11.1 pytest -pyyaml==5.4.1 +pyyaml>=5.4.1 redis==4.5.4 requests==2.31.0 sh==1.12.14 @@ -17,3 +17,5 @@ PyGithub==1.57 typing-extensions~=4.0 google-auth~=2.13 google-cloud-bigquery~=3.3 +toml +jinja2 diff --git a/tools/README.md b/tools/README.md index 179abc3..f3addc0 100644 --- a/tools/README.md +++ b/tools/README.md @@ -1,3 +1,4 @@ + # Adabot Tools and Scripts