diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8f57912..b66e271 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,4 +63,4 @@ jobs: python3 -m pip install setuptools wheel python3 -m pip install -r ./requirements-dev.txt - name: "Update release notes" - run: python3 ./script/generate_releasenotes.py --token ${{ secrets.GITHUB_TOKEN }} --repo ${{ github.repository }} --update-release `git describe --abbrev=0` + run: python3 ./bin/gen_releasenotes --token ${{ secrets.GITHUB_TOKEN }} --repo ${{ github.repository }} --release `git describe --abbrev=0` diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ce3fdde..d2c7baa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: args: - --safe - --quiet - files: ^(custom_components|script)/.+\.py$ + files: ^custom_components/.+\.py$ - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.1 hooks: @@ -19,7 +19,7 @@ repos: additional_dependencies: - flake8-docstrings==1.5.0 - pydocstyle==5.0.2 - files: ^(custom_components)/.+\.py$ + files: ^custom_components/.+\.py$ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.5.0 hooks: @@ -38,7 +38,7 @@ repos: # shell. - id: mypy name: mypy - entry: script/run-in-env.sh mypy + entry: bin/run-in-env mypy language: script types: [python] require_serial: true diff --git a/script/bootstrap b/bin/bootstrap similarity index 100% rename from script/bootstrap rename to bin/bootstrap diff --git a/script/dev-deploy b/bin/dev-deploy similarity index 100% rename from script/dev-deploy rename to bin/dev-deploy diff --git a/bin/gen_releasenotes b/bin/gen_releasenotes new file mode 100755 index 0000000..006ed50 --- /dev/null +++ b/bin/gen_releasenotes @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +"""Helper script to generate release notes.""" +import argparse +import logging +import os +import re +import subprocess +from datetime import datetime +from typing import List + +from github import Github, Repository, Tag +from packaging.version import Version + +# http://docs.python.org/2/howto/logging.html#library-config +# Avoids spurious error messages if no logger is configured by the user +logging.getLogger(__name__).addHandler(logging.NullHandler()) + +logging.basicConfig(level=logging.CRITICAL) + +_LOGGER = logging.getLogger(__name__) + +VERSION = "1.1.1" + +ROOT = os.path.dirname(os.path.abspath(f"{__file__}/..")) + +BODY = """ +[![Downloads for this release](https://img.shields.io/github/downloads/{repo}/{version}/total.svg)](https://github.com/{repo}/releases/{version}) + +{changes} + +## Links + +- [If you like what I (@limych) do please consider sponsoring me on Patreon](https://www.patreon.com/join/limych?) +""" + +CHANGE = "- [{line}]({link}) @{author}\n" +NOCHANGE = "_No changes in this release._" + + +def get_commits(repo: Repository, since: datetime, until: datetime): + """Get commits in repo.""" + develop_sha = repo.get_branch("develop").commit.sha + commits = repo.get_commits(sha=develop_sha, since=since, until=until) + if len(list(commits)) == 1: + return [] + return reversed(list(commits)[:-1]) + + +def get_release_tags(repo: Repository) -> List[Tag.Tag]: + """Get list of all release tags from repository.""" + reg = re.compile( + r"^v?[0-9]+\.[0-9]+\.[0-9]+" + r"(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+)?$" + ) + tags = list(filter(lambda tag: re.match(reg, tag.name), list(repo.get_tags()))) + tags.sort(key=lambda x: x.name.lstrip("v"), reverse=True) + _LOGGER.debug("Found tags: %s", tags) + return tags + + +def get_period(repo: Repository, release: str = None) -> List[datetime]: + """Return time period for release notes.""" + data = [datetime.now()] + dateformat = "%a, %d %b %Y %H:%M:%S GMT" + found = release is None + for tag in get_release_tags(repo): + commit = repo.get_commit(tag.commit.sha) + timestamp = datetime.strptime(commit.last_modified, dateformat) + _LOGGER.debug("Process tag %s => timestamp %s", tag.name, timestamp) + data.append(timestamp) + if found: + break + if release is not None and release == tag.name: + found = True + return list(reversed(data[-2:])) + + +def gen_changes(repo: Repository, tag: str = None) -> str: + """Generate list of commits.""" + changes = "" + period = get_period(repo, tag) + _LOGGER.debug("Period: %s", period) + + commits = get_commits(repo, period[0], period[1]) + for commit in commits: + msg = repo.get_git_commit(commit.sha).message + if "\n" in msg: + msg = msg.split("\n")[0] + if ( + "Bump version " in msg + or "Merge branch " in msg + or "Merge tag " in msg + or "Merge pull request " in msg + ): + continue + changes += CHANGE.format( + line=msg, link=commit.html_url, author=commit.author.login + ) + + return changes if changes != "" else NOCHANGE + + +def _bump_release(release, bump_type): + """Bump a release tuple consisting of 3 numbers.""" + major, minor, patch = release + + if bump_type == "patch": + patch += 1 + elif bump_type == "minor": + minor += 1 + patch = 0 + + return major, minor, patch + + +def bump_version(version: Version) -> Version: + """Return a new version given a current version and action.""" + to_change = {} + + # Convert 0.67.3 to 0.67.4 + # Convert 0.67.3.b5 to 0.67.3 + # Convert 0.67.3.dev0 to 0.67.3 + to_change["dev"] = None + to_change["pre"] = None + + if not version.is_prerelease: + to_change["release"] = _bump_release(version.release, "patch") + + temp = Version("0") + temp._version = version._version._replace( # pylint: disable=protected-access + **to_change + ) + return Version(str(temp)) + + +def main(): + """Execute script.""" + parser = argparse.ArgumentParser( + description=f"Release notes generator. Version {VERSION}" + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Enable debugging output.", + ) + parser.add_argument( + "-n", + "--dry-run", + "--dryrun", + action="store_true", + help="Preview release notes generation without running it.", + ) + parser.add_argument( + "--token", + help="Github token to access to repository.", + # required=True, + ) + parser.add_argument( + "--repo", + help="Github repository (default: %(default)s).", + default=subprocess.run( + ["git", "config", "--get", "remote.origin.url"], + stdout=subprocess.PIPE, + check=True, + ) + .stdout.decode("UTF-8") + .replace("https://github.com/", "") + .replace(".git", "") + .strip(), + ) + parser.add_argument( + "--release", + help="Github release tag to update release notes.", + ) + arguments = parser.parse_args() + + if arguments.verbose: + _LOGGER.setLevel(logging.DEBUG) + + if arguments.dry_run: + _LOGGER.debug("Dry run mode ENABLED") + print("!!! Dry Run !!!") + + github = Github(arguments.token) + _LOGGER.debug("Repo: %s", arguments.repo) + repo = github.get_repo(arguments.repo) + if arguments.release is None: + changes = gen_changes(repo) + _LOGGER.debug(changes) + if changes != NOCHANGE: + version = Version(get_release_tags(repo)[0].name.lstrip("v")) + _LOGGER.debug(version) + new_version = bump_version(version) + _LOGGER.debug(new_version) + print( + "Generated release notes for v{version}:\n{message}".format( + version=new_version, + message=changes, + ) + ) + else: + print("Not enough changes for a release.") + else: + version = arguments.release.replace("refs/tags/", "") + _LOGGER.debug("Release tag: %s", version) + msg = BODY.format( + repo=arguments.repo, + version=version, + changes=gen_changes(repo, version), + ) + if arguments.dry_run: + print("Generated release notes:\n" + msg) + else: + release = repo.get_release(version) + release.update_release( + name=version, + prerelease=release.prerelease, + draft=release.draft, + message=msg, + ) + + +if __name__ == "__main__": + main() diff --git a/script/run-in-env.sh b/bin/run-in-env similarity index 100% rename from script/run-in-env.sh rename to bin/run-in-env diff --git a/script/setup b/bin/setup similarity index 100% rename from script/setup rename to bin/setup diff --git a/script/update b/bin/update similarity index 100% rename from script/update rename to bin/update diff --git a/script/update_requirements.py b/bin/update_requirements similarity index 100% rename from script/update_requirements.py rename to bin/update_requirements diff --git a/custom_components/average/manifest.json b/custom_components/average/manifest.json index f19342e..99d50c1 100644 --- a/custom_components/average/manifest.json +++ b/custom_components/average/manifest.json @@ -1,8 +1,10 @@ { - "domain": "average", - "name": "Average", - "documentation": "https://github.com/Limych/ha-average", - "requirements": [], - "dependencies": [], - "codeowners": ["@Limych"] -} + "codeowners": [ + "@Limych" + ], + "dependencies": [], + "documentation": "https://github.com/Limych/ha-average", + "domain": "average", + "name": "Average", + "requirements": [] +} \ No newline at end of file diff --git a/hass-integration-manifest.schema.json b/hass-integration-manifest.schema.json deleted file mode 100644 index 784a914..0000000 --- a/hass-integration-manifest.schema.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://github.com/Limych/ha-average/blob/master/hass-integration-manifest.schema.json", - "title": "Home Assistant Integration Manifest", - "description": "Manifest of integration for Home Assistant smart home system", - "type": "object", - "properties": { - "domain": { - "type": "string", - "description": "The unique domain name of this integration. Cannot be changed" - }, - "name": { - "type": "string", - "description": "The name of the integration" - }, - "documentation": { - "type": "string", - "description": "URL of the website containing documentation on how to use that integration" - }, - "requirements": { - "type": "array", - "description": "List of required Python libraries or modules for this integration", - "items": { - "type": "string" - }, - "minItems": 0, - "uniqueItems": true - }, - "dependencies": { - "type": "array", - "description": "List of the other Home Assistant integrations that need to Home Assistant to set up successfully prior to the integration being loaded", - "items": { - "type": "string" - }, - "minItems": 0, - "uniqueItems": true - }, - "codeowners": { - "type": "array", - "description": "List of GitHub usernames or team names of people that are responsible for this integration", - "items": { - "type": "string" - }, - "minItems": 0, - "uniqueItems": true - } - }, - "required": [ - "domain", - "name", - "documentation", - "requirements", - "dependencies", - "codeowners" - ] -} diff --git a/requirements-dev.txt b/requirements-dev.txt index 58008ff..c3becef 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,10 +1,11 @@ -r requirements.txt -black -flake8 -mypy -pre-commit +black==20.8b1 +flake8~=3.8 +mypy==0.782 +packaging~=20.4 +pre-commit~=2.7 PyGithub==1.53 -pylint -pylint-strict-informational -pyupgrade -yamllint +pylint~=2.6 +pylint-strict-informational==0.1 +pyupgrade~=2.7 +yamllint~=1.24 diff --git a/script/__init__.py b/script/__init__.py deleted file mode 100755 index da52cab..0000000 --- a/script/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Development support scripts.""" diff --git a/script/generate_releasenotes.py b/script/generate_releasenotes.py deleted file mode 100755 index fc9d353..0000000 --- a/script/generate_releasenotes.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env python3 -"""Helper script to generate release notes.""" -import argparse -import logging -import os -import re -import subprocess -from datetime import datetime -from typing import Dict - -from github import Github, Repository - -# http://docs.python.org/2/howto/logging.html#library-config -# Avoids spurious error messages if no logger is configured by the user -logging.getLogger(__name__).addHandler(logging.NullHandler()) - -logging.basicConfig(level=logging.CRITICAL) - -_LOGGER = logging.getLogger(__name__) - -VERSION = "0.1.0" - -ROOT = os.path.dirname(os.path.abspath(f"{__file__}/..")) - -BODY = """ -[![Downloads for this release](https://img.shields.io/github/downloads/{repo}/{version}/total.svg)](https://github.com/{repo}/releases/{version}) - -{changes} - -## Links - -- [If you like what I (@limych) do please consider sponsoring me on Patreon](https://www.patreon.com/join/limych?) -""" - -CHANGE = "- [{line}]({link}) @{author}\n" -NOCHANGE = "_No changes in this release._" - - -def new_commits(repo: Repository, sha: str): - """Get new commits in repo.""" - dateformat = "%a, %d %b %Y %H:%M:%S GMT" - release_commit = repo.get_commit(sha) - since = datetime.strptime(release_commit.last_modified, dateformat) - commits = repo.get_commits(since=since) - if len(list(commits)) == 1: - return [] - return reversed(list(commits)[:-1]) - - -def last_release(github: Github, repo: Repository, skip=True) -> Dict[str, str]: - """Return last release.""" - tag_sha = None - data = {} - tags = list(repo.get_tags()) - reg = "(v|^)?(\\d+\\.)?(\\d+\\.)?(\\*|\\d+)$" - _LOGGER.debug("Found tags: %s", tags) - if tags: - for tag in tags: - tag_name = tag.name - if re.match(reg, tag_name): - tag_sha = tag.commit.sha - if skip: - skip = False - continue - break - data["tag_name"] = tag_name - data["tag_sha"] = tag_sha - return data - - -def get_commits(github: Github, repo: Repository) -> str: - """Generate list of commits.""" - changes = "" - commits = new_commits(repo, last_release(github, repo)["tag_sha"]) - - for commit in commits: - msg = repo.get_git_commit(commit.sha).message - if "Bump version " in msg: - continue - if "Merge branch " in msg: - continue - if "Merge tag " in msg: - continue - if "Merge pull request " in msg: - continue - if "\n" in msg: - msg = msg.split("\n")[0] - changes += CHANGE.format( - line=msg, link=commit.html_url, author=commit.author.login - ) - - if changes == "": - changes = NOCHANGE - - return changes - - -def main(): - """Execute script.""" - parser = argparse.ArgumentParser( - description=f"Release notes generator. Version {VERSION}" - ) - parser.add_argument( - "-v", "--verbose", action="store_true", help="Enable debugging output.", - ) - parser.add_argument( - "--dry-run", - action="store_true", - help="Preview version bumping without running it.", - ) - parser.add_argument( - "--token", - help="Github token to access to repository.", - # required=True, - ) - parser.add_argument( - "--repo", - help="Github repository (default: %(default)s).", - default=subprocess.run( - ["git", "config", "--get", "remote.origin.url"], - stdout=subprocess.PIPE, - check=True, - ) - .stdout.decode("UTF-8") - .replace("https://github.com/", "") - .replace(".git", "") - .strip(), - ) - parser.add_argument( - "--update-release", help="Github release tag to update.", required=True, - ) - arguments = parser.parse_args() - - if arguments.verbose: - _LOGGER.setLevel(logging.DEBUG) - - if arguments.dry_run: - print("!!! Dry Run !!!") - - github = Github(arguments.token) - _LOGGER.debug("Repo: %s", arguments.repo) - repo = github.get_repo(arguments.repo) - version = arguments.update_release.replace("refs/tags/", "") - _LOGGER.debug("Tag: %s", version) - release = repo.get_release(version) - release.update_release( - name=version, - prerelease=release.prerelease, - draft=release.draft, - message=BODY.format( - repo=arguments.repo, version=version, changes=get_commits(github, repo), - ), - ) - - -if __name__ == "__main__": - main()