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