QA Documentation. Как я автоматизировал самую нелюбимую часть работы — написание ReleaseNotes

от автора

Уже два года я работаю специалистом по тестированию, и многие коллеги меня поймут — одна из самых ненавистных и рутинных задач — это написание тестовой документации. И конечно я цепляюсь за каждую, даже самую маленькую возможность автоматизировать этот процесс. И в этой статье я хотел бы рассказать вам о том, как я автоматизировал написание отчета по релизу используя версионность гита и интеграцию с Jira. Очень надеюсь что моя задумка сможет помочь вам в работе, а более опытные коллеги смогу предложить дополнительные доработки и оптимизации моего решения.

И так. Началось все с достаточно невинной просьбы архитектора моего проекта — «Денчик, смотри, нужно взять все задачи, сделанные в этом спринте — то есть с версии 0.0.1.01 до версии 0.0.2.01 и выписать в статью».

Ну… подумал я…полез в жиру и стал думать — как же это сделать? И начал я просто копировать номер задачи и ее заголовок в текстовик. Очень неэффективно, и с огромной погрешностью. Что же делать?

Меня в этом плане очень выручил регламент оформления коммитов в нашей команде. Каждый коммит должен называться номером задачи в рамках которой он должен быть сделан. Моя коллега предложила такой баш скрипт:

#!/usr/bin/env bash  # Переменные, хранящие начальную и конечную версии, а также название продукта RELEASE_VERSION_FROM="$1" RELEASE_VERSION_TO="$2" RELEASE_PRODUCT="$3"  # Проверка наличия аргументов if [[ -z "$1" && -z "$2" && -z "$3" ]] then     echo 'No version or product name provided' # Вывод сообщения об отсутствии версии или названия продукта     exit # Завершение работы скрипта fi  # Цикл для обработки каждого подкаталога в текущем рабочем каталоге for entry in $(ls -d */) do   echo "$entry" # Вывод имени текущего обрабатываемого подкаталога   cd "$entry" # Переход в текущий подкаталог   git pull # Извлечение изменений из удаленного репозитория в текущую ветку    # Вывод списка коммитов между заданными версиями, отфильтрованных по названию продукта   # Результат отформатирован так, чтобы выводить только идентификаторы коммитов, соответствующие шаблону RELEASE_PRODUCT-число   git log "$1".."$2" --format=%s | grep -i '^'$RELEASE_PRODUCT | sed -r 's/('$RELEASE_PRODUCT'-[0-9]+).*/\1/' | sort | uniq    cd .. # Возврат на уровень выше done 

Работает этот скрипт безумно просто — он находится на одном уровне с папками-репозиторями. До его исполнения я вывожу git tag, получаю список версий и запускаю скрипт командой:

sh test.sh v.0.01.01 v.0.0.1.01 projectname

Где:

v.0.0.1.01 — Номер первой версии

v.0.0.2.01 — Номер конечной версии

projectname — имя проекта (тег в Jirа которым же помечается и кормит)

Этот скрипт, как я писал выше, должен находится на одном уровне с папками-репозиторями проекта. В качестве примера привожу проект с тремя приложениями — app, printer, ui:

├── app ├── printer ├── ui └── main.sh

После исполнения этого скрипта я получаю примерно следующий вывод:

app/ projectname-426 projectname-432 projectname-453 projectname-471 printer/ projectname-352 projectname-369 ui/ projectname-321 projectname-420 projectname-422 projectname-425 projectname-431

Вывод достаточно простой и понятный — мы видим папку и вывод команды git log — все сделанные коммиты в рамках разработки данной (конечной) версии. Дальше с этими данными идем в жиру, ищем по тегу задачу и оформляем в текстовый документ.

Вот так вот ручками я нахожу задачку в джире

Вот так вот ручками я нахожу задачку в джире

В целом схема рабочая, но первый вопрос, которым я задался — «А зачем мне вручную вводить git tag, если это может делать скрипт!»
Тут появилась идея разработки более интерактивно реализации скрипта, за что в последствии я получил оплеух от архитектора, но мне все равно нравится подобное исполнение.

И так. Я отказался от bash в пользу Python и тут началась разработка полноценного и функционального скрипта. Рассмотрим уже написанный скрипт более подробно.

Я поставил задачу запихнуть весь функционал в один файл в угоду удобства запуска — удобно закинуть один скриптик в папку с репозиториями, запустить и получить вывод. Очень не хотелось плодить модули, хотя такой вариант рассматривался.

Первая функция — инициализация работы с апи Jira. Это нужно для того, что бы скрипт, получив номер коммита сам запросил у джиры заголовок задачи (ну и любые другие данные)

class JiraAPI:     def __init__(self, jira_url, username, password):         # Инициализация JiraAPI с URL Jira, логином и паролем         self.jira_url = jira_url         self.username = username         self.password = password         self.auth = (self.username, self.password)  # Создание кортежа для аутентификации         self.headers = {             "Content-Type": "application/json"  # Установка типа содержимого         }      def get_issue_summary(self, issue_key):         # Получение краткого описания задачи по ключу задачи         try:             issue_url = f"{self.jira_url}/rest/api/2/issue/{issue_key}"  # Сборка URL для запроса задачи             response = requests.get(issue_url, headers=self.headers,                                     auth=self.auth)  # Выполнение GET-запроса с аутентификацией              if response.status_code == 200:  # Проверка успешного ответа                 issue_data = response.json()  # Получение данных задачи                 return issue_data['fields']['summary']  # Возврат краткого описания задачи             else:                 return None         except Exception as e:  # Обработка исключений             return None   jira_url = "https://jira.com"  # Захардкодим URL Jira  # Получение логина и пароля от пользователя username = input("Введите ваш логин JIRA: ") password = input("Введите ваш пароль Jira: ")   def retrieve_issue_summary(issue_key):     jira = JiraAPI(jira_url, username, password)     issue_summary = jira.get_issue_summary(issue_key)     return issue_summary

Здесь вы должны заменить jira_url на ссылку на свою джиру.

Так же я реализовал пользовательский ввод авторизационных данных. API джиры позволяет логинится не только по «логопасу» но и по токены, но так как моим скриптом пользуются разные коллеги с разных проектов, гораздо проще для использования именно такая реализация. Всю полезную и нужную документацию по работе с API джиры вы можете найти на официальном сайте.

Далее идет основной код программы:

def list_repository_tags(repo_directory):     # Получение списка тегов в репозитории     os.chdir(repo_directory)  # Изменение директории на переданный репозиторий     tag_output = subprocess.check_output(["git", "tag"], text=True)  # Получение списка тегов     os.chdir(os.path.pardir)  # Возврат на уровень выше     return tag_output  # Возвращение списка тегов   class GitCommitExtractor:     def __init__(self):         self.RELEASE_VERSION_TO = None         self.RELEASE_VERSION_FROM = None         self.RELEASE_PRODUCT = input("Введите код продукта: ")  # Продукт (префикс) для поиска в коммитах         self.unique_commits = {}  # Словарь для хранения уникальных коммитов в каждом репозитории      def get_versions_from_user(self):         # Получение версий от пользователя         self.RELEASE_VERSION_FROM = input("Введите изначальную версию (из списка выше): ")         self.RELEASE_VERSION_TO = input("Введите конечную версию (из списка выше): ")          if not self.RELEASE_VERSION_FROM or not self.RELEASE_VERSION_TO:             print('Введенная версия отсутствует. Выход...')             exit()      def process_repositories(self):  # Функция для обработки репозиториев         current_directory = os.getcwd()  # Получение текущего рабочего каталога          # Перебор всех элементов в текущем каталоге         for entry in os.listdir(current_directory):             if os.path.isdir(entry):  # Проверка, является ли элемент директорией                 repo_directory = os.path.join(current_directory, entry)                  if os.path.exists(os.path.join(repo_directory, '.git')):                     # Получение списка тегов и вывод названия репозитория                     tags = list_repository_tags(repo_directory)                     print(f'Репозиторий: {entry}')                     print(f'Версии:\n{tags}')                      # Добавление информации о репозитории и его тегах в словарь                     if entry not in self.unique_commits:                         self.unique_commits[entry] = {"tags": tags, "commits": set()}          # Получение версий от пользователя         self.get_versions_from_user()          # Обработка коммитов в каждом репозитории         for repo, info in self.unique_commits.items():             self.process_repository(repo, info["tags"])          # Вывод уникальных коммитов для каждого репозитория         self.print_unique_commits()      def process_repository(self, repo_directory, tags):         os.chdir(repo_directory)         # Обновление репозитория с помощью git pull & git fetch         subprocess.run(["git", "fetch"])         subprocess.run(["git", "pull"])          # Проверка существования указанных версий в репозитории         if not self.version_exists(self.RELEASE_VERSION_FROM, tags):             print(f"Error: Version {self.RELEASE_VERSION_FROM} not found in {repo_directory}.")         elif not self.version_exists(self.RELEASE_VERSION_TO, tags):             print(f"Error: Version {self.RELEASE_VERSION_TO} not found in {repo_directory}.")         else:             # Извлечение и сохранение коммитов в указанном диапазоне версий             self.extract_commits(repo_directory)          os.chdir(os.path.pardir)      def version_exists(self, version, tags):         # Проверка существования версии в списке тегов         return version in tags      def extract_commits(self, repo_directory):         # Функция для извлечения и сохранения коммитов с номерами задач          # Получение вывода команды git log для указанного диапазона версий         log_output = subprocess.check_output(             ["git", "log", f"{self.RELEASE_VERSION_FROM}..{self.RELEASE_VERSION_TO}", "--format=%s"], text=True)          # Разделение вывода на отдельные сообщения коммитов         commit_messages = log_output.split('\n')          # Перебор каждого сообщения коммита         for message in commit_messages:             if message.lower().startswith(self.RELEASE_PRODUCT.lower()):  # Проверка на соответствие продукту                 task_number = message.split('-')[1].split()[0]  # Извлечение номера задачи                 task_prefix = self.RELEASE_PRODUCT + '-' + task_number  # Сборка префикса задачи                 # Добавление коммита в множество коммитов репозитория                 self.unique_commits[repo_directory]["commits"].add(task_prefix)      def print_unique_commits(self):         # Вывод уникальных коммитов для каждого репозитория         for repo, info in self.unique_commits.items():             print(f'\n\nРепозиторий: {repo}')             print("Коммиты:")             for commit in info["commits"]:                 commit = commit[:-1]                 issue_summary = retrieve_issue_summary(commit)                 print(commit, issue_summary)   if __name__ == "__main__":     git_commit_extractor = GitCommitExtractor()     # Запуск программы и обработка репозиториев     git_commit_extractor.process_repositories()

Я постараться все максимально закомментировать на русском языке, так что думаю тут вопросов возникнуть не должно.

Попробуем запустить скрипт теперь. Так же как и прошлый он должен находится на одном уровне с папками-репозиториями проекта.

├── app ├── printer ├── ui └── main.py

Запускаем его без каких либо флагов — python3 main.py и видим поля для пользовательского ввода

Введите ваш логин JIRA: my_jira_login Введите ваш пароль Jira: my_jira_password Введите код продукта: projectname

После заполнения полей сразу же происходит вывод git tag для каждого репозитория и снова предложение пользовательского ввода

Репозиторий: app Версии: v.0.0.2.01 v.0.0.2.02 v.0.0.2.04 v.0.0.3.01 v.0.0.4.01 v.0.1.0.01 v.0.1.0.04 v.0.1.0.05 v.0.1.0.07 v.0.2.0.01   Репозиторий: printer Версии: v.0.0.2.01 v.0.0.2.02 v.0.0.2.04 v.0.0.4.01 v.0.1.0.01 v.0.1.0.04 v.0.2.0.01  Репозиторий: ui Версии: v.0.0.2.01 v.0.0.2.02 v.0.0.2.04 v.0.0.4.01 v.0.1.0.01 v.0.1.0.04 v.0.2.0.01  Введите изначальную версию (из списка выше): v.0.1.0.01 Введите конечную версию (из списка выше): v.0.2.0.01

В конечном итоге видим вывод всех коммитов сделанных в рамках данного релиза, подписанных заголовком задачи, взятом из Jira:

Репозиторий: app Коммиты: projectname-426 Добавить поля input_date в таблицу document projectname-432 Доработка внешнего API projectname-453 Доработка раздела "Редактирование дела" projectname-471 Интеграция с принтером (projectname-352)  Репозиторий: printer Коммиты: projectname-352 Доработка приложения Printer  projectname-369 Доработка Печатной формы  Репозиторий: ui Коммиты: projectname-321 Перенос кнопки "Изменить" projectname-420 Валидация полей на главной странице projectname-422 Реализация предзаполненности полей на главной странице projectname-425 Доработка экранной формы главной страницы projectname-431 Создание страницы для принтера (projectname-352)

И примерно в таком виде это и идет в отчет по релизу. Иногда требует минимальных правок, но при ответственном и грамотном оформлении коммитов и задачек — все работает отлично. АПИ жиры позволяет получить все данные по задаче, по этому по просьбе коллег, я немного доработал вывод программы, что бы информация о выполненной задаче была немного подробнее:

Коммит номер: projectname-420   Описание задачи: Валидация полей на главной странице   Приоритет задачи: 1   Тип задачи: Баг   Автор коммита: Denis Kirillov   Исполнитель задачи: Кириллов Денис Владимирович:

Номер и автор коммита берется из гита.
Описание, приоритет, тип и исполнитель задачи из джиры.


ссылка на оригинал статьи https://habr.com/ru/articles/770432/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *