QA Documentation. Как я автоматизировал самую нелюбимую часть работы — написание ReleaseNote
Уже два года я работаю специалистом по тестированию, и многие коллеги меня поймут — одна из самых ненавистных и рутинных задач — это написание тестовой документации. И конечно я цепляюсь за каждую, даже самую маленькую возможность автоматизировать этот процесс. И в этой статье я хотел бы рассказать вам о том, как я автоматизировал написание отчета по релизу используя версионность гита и интеграцию с 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.0.2.04 v.0.0.3.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
Исполнитель задачи: Кириллов Денис Владимирович:
Номер и автор коммита берется из гита
Описание, приоритет, тип и исполнитель задачи из джиры.