From db58db4ff56e6881154fcf24ae78b443078a275f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Beamonte?= Date: Tue, 3 Jan 2023 21:17:43 -0500 Subject: [PATCH] [ci] add release process and info.md file for HACS (#58) * Add info.md file for a better experience with HACS * Add release process and auto-updates of info.md --- .github/workflows/build.yaml | 73 +++++- .github/workflows/scripts/release.py | 328 +++++++++++++++++++++++++++ README.md | 4 +- hacs.json | 3 +- info.md | 50 ++++ 5 files changed, 454 insertions(+), 4 deletions(-) create mode 100755 .github/workflows/scripts/release.py create mode 100644 info.md diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index eaf2347..d3405de 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -17,7 +17,10 @@ jobs: name: Prepare environment runs-on: "ubuntu-latest" outputs: - requires_tests: ${{ github.event_name == 'schedule' || steps.changed-files.outputs.any_changed == 'true' || steps.changed-files.outputs.any_deleted == 'true' || steps.changed-files.outputs.any_modified == 'true' }} + requires_tests: ${{ github.event_name == 'schedule' || steps.changed-files.outputs.any_changed == 'true' || steps.changed-files.outputs.any_deleted == 'true' || steps.changed-files.outputs.any_modified == 'true' || env.PUBLISH_RELEASE == 'true' }} + publish_release: ${{ env.PUBLISH_RELEASE }} + release_version: ${{ env.RELEASE_VERSION }} + release_desc: ${{ env.RELEASE_DESC }} steps: - name: Checkout current commit uses: "actions/checkout@v3" @@ -33,6 +36,14 @@ jobs: tests/** .github/workflows/build.yaml + - name: Check if a new release should be published + if: github.event_name == 'push' + env: + GITHUB_EVENT: ${{ toJSON(github.event) }} + run: | + .github/workflows/scripts/release.py --printenv | tee -a "$GITHUB_ENV" + + unit-integration-tests: name: Unit & integration tests runs-on: "ubuntu-latest" @@ -202,13 +213,73 @@ jobs: pytest --cache-clear tests/end-to-end/ + update_changelog: + name: Update changelog + runs-on: "ubuntu-latest" + + needs: + - prepare-env + - unit-integration-tests + - end-to-end-tests + + if: github.event_name == 'push' && needs.prepare-env.outputs.publish_release != 'true' && needs.unit-integration-tests.result != 'failure' && needs.end-to-end-tests.result != 'failure' + + steps: + - name: Checkout current commit + uses: "actions/checkout@v3" + + - name: Update changelog for future release + env: + GITHUB_EVENT: ${{ toJSON(github.event) }} + run: | + .github/workflows/scripts/release.py --update-changelog + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v4 + with: + file_pattern: "info.md" + commit_message: 📒 Update changelog for future release + + + push_release: + name: Release version + runs-on: ubuntu-latest + + needs: + - prepare-env + - unit-integration-tests + - end-to-end-tests + + if: github.event_name == 'push' && needs.prepare-env.outputs.publish_release == 'true' && needs.unit-integration-tests.result != 'failure' && needs.end-to-end-tests.result != 'failure' + + permissions: + contents: write + + steps: + - name: Checkout current commit + uses: "actions/checkout@v3" + + - name: Release version + uses: ncipollo/release-action@v1 + with: + tag: v${{ needs.prepare-env.outputs.release_version }} + name: v${{ needs.prepare-env.outputs.release_version }} + body: ${{ needs.prepare-env.outputs.release_desc }} + commit: main + makeLatest: true + skipIfReleaseExists: true + + auto-merge: name: "Auto-merge Dependabot pull requests" runs-on: ubuntu-latest + needs: - unit-integration-tests - end-to-end-tests + if: github.event_name == 'pull_request' && github.actor == 'dependabot[bot]' && needs.unit-integration-tests.result != 'failure' && needs.end-to-end-tests.result != 'failure' + steps: - name: Checkout current commit uses: "actions/checkout@v3" diff --git a/.github/workflows/scripts/release.py b/.github/workflows/scripts/release.py new file mode 100755 index 0000000..15f0a6e --- /dev/null +++ b/.github/workflows/scripts/release.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python3 + +import argparse +import json +import os +import re +import requests +import shlex + + +class ChangeLogRegex(object): + NEXT = re.compile( + r'(?P(?P
{%\s*if true\s*-?%}\s*' + r'### _Next \(dev\)_\n*)' + r'(?P[^\n].*?[^\n]))' + r'(?P\n*{%\s*endif\s*-?%})', + re.DOTALL, + ) + NEXT_SUB = re.compile( + r'(?P{%\s*if )true(?P\s*-?%}\s*### )' + r'_Next \(dev\)_(?P\n)', + re.IGNORECASE, + ) + VERSION = re.compile( + r'{%\s*if parsed_version < \[' + r'(?P[0-9]*), (?P[0-9]*), (?P[0-9]*)\]' + r'\s*-%}\s*### (?P[^\n]*)\n*' + r'(?P[^\n].*?[^\n])' + r'\n*{%\s*endif\s*-?%}', + re.DOTALL, + ) + CHANGE = re.compile( + r'^\s*\*\s*(?P.*)$', + re.MULTILINE, + ) + + COMMIT_CHANGE = re.compile( + r'^\s*(\*\*)?\[(?P[^\]]*)\](\*\*)?\s*' + r'(?P.*)$', + ) + COMMIT_CHANGE_BUG = re.compile(r'\b(bug|fix)\b', re.IGNORECASE) + COMMIT_CHANGE_FEATURE = re.compile(r'\b(feature|add)\b', re.IGNORECASE) + + +class ChangeLogHandler(object): + + CHANGELOG_FILE = 'info.md' + RELEASE_PATHS = [ + re.compile(r'^apps/'), + ] + + def __init__(self, github_event, github_token=None): + self._changelog = None + self._gh_event = github_event + + if github_token: + self._headers = {'Authorization': f'Bearer {github_token}'} + else: + self._headers = {} + + @property + def repo(self): + return self._gh_event['repository']['full_name'] + + @property + def commits(self): + return self._gh_event['commits'] + + @property + def before_sha(self): + return self._gh_event['before'] + + def printenv(self): + new_release = self.new_release() + + publish_release = str(bool(new_release)).lower() + print(f'PUBLISH_RELEASE={publish_release}') + + if new_release: + print(f'RELEASE_VERSION={new_release["version"]}') + print(f'RELEASE_MAJOR={new_release["major"]}') + print(f'RELEASE_MINOR={new_release["minor"]}') + print(f'RELEASE_PATCH={new_release["patch"]}') + print(f'RELEASE_VERSION_MAJOR={new_release["major"]}') + print(f'RELEASE_VERSION_MINOR={new_release["major"]}.' + f'{new_release["minor"]}') + print(f'RELEASE_VERSION_PATCH={new_release["major"]}.' + f'{new_release["minor"]}.{new_release["patch"]}') + print(f'RELEASE_NAME={shlex.quote(new_release["release_name"])}') + + print('RELEASE_DESC<\n{new_changes}\g', + self.read_file(), + ) + else: + new_content = re.sub( + '(## ChangeLog)', + '\g<1>\n{% if true -%}\n### _Next (dev)_\n\n' + f'{new_changes}\n' + '{% endif %}', + self.read_file(), + re.IGNORECASE, + ) + + with open(self.CHANGELOG_FILE, 'w') as f: + f.write(new_content) + + def release_next(self, version): + if not ChangeLogRegex.NEXT_SUB.search(self.read_file()): + raise RuntimeError('No release waiting to be released') + + version_parts = ['major', 'minor', 'patch'] + if version in version_parts: + releases = list(self.read_file_releases()) + last_release = releases[0] + + idx = version_parts.index(version) + new_version = { + k: last_release[k] if i <= idx else 0 + for i, k in enumerate(version_parts) + } + new_version[version] += 1 + + version = new_version + else: + version = version.replace('v', '').split('.') + version = { + 'major': int(version[0]), + 'minor': int(version[1]), + 'patch': int(version[2]), + } + + major = version['major'] + minor = version['minor'] + patch = version['patch'] + + new_content = ChangeLogRegex.NEXT_SUB.sub( + f'\gparsed_version < [{major}, {minor}, {patch}]' + f'\gVersion {major}.{minor}.{patch}\g', + self.read_file(), + re.IGNORECASE, + ) + + with open(self.CHANGELOG_FILE, 'w') as f: + f.write(new_content) + + print(f'Replaced NEXT by release {major}.{minor}.{patch}') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + + parser.add_argument( + '-e', '--github-event', type=json.loads, + default=json.loads(os.getenv('GITHUB_EVENT')) if os.getenv('GITHUB_EVENT') else None, + help='The JSON github event, returned by ${{ toJSON(github.event) }}', + ) + + parser.add_argument( + '-t', '--github-token', type=str, + default=os.getenv('GITHUB_TOKEN'), + help='If the repository is private, the github token to access it', + ) + + command = parser.add_mutually_exclusive_group(required=True) + command.add_argument( + '-p', '--printenv', '--print-env', + action='store_true', + help='Print the environment variables useful for the process and exit', + ) + command.add_argument( + '-u', '--update-changelog', + action='store_true', + help='Update the changelog file', + ) + command.add_argument( + '-r', '--release-next', + nargs='?', const='patch', + help='Release the next version', + ) + + args = parser.parse_args() + + handler = ChangeLogHandler( + github_event=args.github_event, + github_token=args.github_token, + ) + + if args.printenv: + handler.printenv() + elif args.update_changelog: + handler.update_changelog() + elif args.release_next: + handler.release_next(args.release_next) + diff --git a/README.md b/README.md index a7c5725..237575f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Qolsys Gateway - `qolsysgw` -![build](https://github.com/XaF/qolsysgw/actions/workflows/build.yaml/badge.svg) ![hacs validation](https://github.com/XaF/qolsysgw/actions/workflows/hacs-validation.yaml/badge.svg) +![build](https://github.com/XaF/qolsysgw/actions/workflows/build.yaml/badge.svg) +![hacs validation](https://github.com/XaF/qolsysgw/actions/workflows/hacs-validation.yaml/badge.svg) +[![latest release](https://img.shields.io/github/v/release/XaF/qolsysgw?logo=github&sort=semver)](https://github.com/XaF/qolsysgw/releases) Qolsys Gateway (`qolsysgw`) is an [AppDaemon][appdaemon] automation that serves as a gateway between a Qolsys IQ Panel diff --git a/hacs.json b/hacs.json index bf07c8a..d40ecdc 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,4 @@ { "name": "Qolsys Gateway", - "content_in_root": false, - "render_readme": true + "content_in_root": false } diff --git a/info.md b/info.md new file mode 100644 index 0000000..bfa3f34 --- /dev/null +++ b/info.md @@ -0,0 +1,50 @@ +# Qolsys Gateway - `qolsysgw` + +![build](https://github.com/XaF/qolsysgw/actions/workflows/build.yaml/badge.svg) +![hacs validation](https://github.com/XaF/qolsysgw/actions/workflows/hacs-validation.yaml/badge.svg) +[![latest release](https://img.shields.io/github/v/release/XaF/qolsysgw?logo=github&sort=semver)](https://github.com/XaF/qolsysgw/releases) + +Qolsys Gateway (`qolsysgw`) is an [AppDaemon][appdaemon] +automation that serves as a gateway between a Qolsys IQ Panel +([2][qolsys-panel-2], [2+][qolsys-panel-2-plus] or [4](qolsys-panel-4)) +and [Home Assistant][hass]. Qolsys Gateway works by establishing a connection +to your Qolsys Panel and uses the [MQTT integration of Home Assistant][hass-mqtt]. +It takes advantages of the [MQTT discovery][hass-mqtt-discovery] +feature (automatically enabled when you setup the integration) to declare the +device, alarm control panels (for each partition) and different sensors, and +keep them up to date with the information coming from the panel, while +providing you with the means to arm, disarm or trigger your alarm directly +from Home Assistant, manually or through automations. + +{% if not installed -%} +## Requirements + +- A Qolsys IQ Panel 2 or 2+ (software version 2.5.3 or greater), or 4 + (software version 4.1 or greater), + for which you have the **dealer code** (defaults to `2222`). In some cases, + the _installer code_ (defaults to `1111`) might be sufficient, but in my + experience, it was not, as the required menus were not visible. + +- Understanding that this automation is not part of the core of Home Assistant + and is thus not officially supported by Home Assistant. By using it, you + agree that neither Home Assistant nor myself are responsible for any issues + with your Home Assistant configuration, loss of data, or whatever could be + caused by using Qolsys Gateway. Setting up Qolsys Gateway requires enabling + the Control4 protocol on your Qolsys Panel, which may open to security issues + and someone taking over control of your alarm system, so please be aware of + what you are doing, and only do it if you are ready to take those risks. + +## Installation + +You can refer to the [README](https://github.com/XaF/qolsysgw#readme) for the details of [how to install](https://github.com/XaF/qolsysgw#installation) and [how to configure](https://github.com/XaF/qolsysgw#configuration) `qolsysgw` and the different components of the process (Home Assistant, AppDaemon, MQTT and the Qolsys Panel). +{% else -%} +{% set parsed_version = version_installed.split('-')[0].replace('v', '').split('.') | map('int') | list -%} +## ChangeLog +{% if parsed_version < [1, 0, 0] -%} +### Version 1.0.0 + +This is the first official version of `qolsysgw`, which will now have release numbers. +This release includes all commits up to this point, and will allow to provide an easy +and simple changelog when making new releases. +{% endif %} +{% endif -%}