Написание документации для кода (будь то небольшая библиотека или полный набор микросервисов, взаимодействие с которыми необходимо другим разработчикам) — это занятие кропотливое и утомительное.
Основная проблема документирования системы — она очень легко устаревает из-за изменений в коде. И хотя разработчики все чаще отдают предпочтение автоматизации в своих задачах, обновление этих изменений обычно нужно делать вручную. Приходится проходить код фрагмент за фрагментом и следить за тем, чтобы каждый пример в документации был актуален. Но теперь в этом нет необходимости, ведь дальше в статье будет показан рабочий поток, помогающий определить момент, когда фрагмент кода устаревает и нуждается в проверке.
Перейдем к его рассмотрению.
Рабочий поток
Главный в этом рабочем потоке — бот GitHub на Bit. Он занимается созданием Pull Request
(запроса на включение изменений) для каждого репозитория, который в данный момент использует обновляемый компонент.
Задействование автоматизации в задачах позволяет тем, кто работает с компонентами, получать уведомления в тот момент, когда что-то меняется. Но будем откровенны: одна из главных проблем современной экосистемы JavaScript в ее невероятной быстроте. Она настолько быстра, что библиотеки и фреймворки обновляются каждый день. И эти обновления иногда разбивают часть кода (или фактически часть документации).
Помочь избежать этого должен рабочий поток, состоящий из бота Bit, Github Action и инструмента Gist-It. Он позволяет автоматически импортировать из Github фрагменты кода.
Чтобы лучше представлять себе, о чем дальше пойдет речь, взгляните на то, как должен выглядеть финальный рабочий поток:
Выглядит сложновато, но стоит рассмотреть получше, и станет понятно, что ничего сложного здесь нет:
- Начнем с начала. Триггером для всего рабочего потока будет обновление, сделанное в компоненте. Безопасным это обновление делает то, что Bit известно о каждом репозитории, использующем компонент. Это, в свою очередь, позволяет реагировать на обновление компонента вместо того, чтобы постоянно проверять, не изменилось ли что-то.
- Благодаря этой интеграции Bit затем создает запрос на включение изменений в репозиторий для каждого проекта, использующего обновленный компонент. В случае проектов, код которых уже интегрирован в кодовую базу, запрос на включение изменений в репозиторий проекта обновит этот код. Если же проекты просто импортируют компонент в качестве зависимости, запрос на включение изменений в репозиторий проекта обновит версию зависимости. В любом случае будет создано изменение.
- И на этом пока что все. Здесь рабочий поток останавливается, пока ответственный за сопровождение проекта не решит выполнить слияние запроса на включение изменений в репозиторий проекта (пункт 3). Конечно, Github укажет на любые конфликты слияния (если таковые имеются), и вы дальше сами решите, как с ними быть. После же слияния произойдет следующее:
- Будет активирован GitHub action. Этот экшен запустит набор тестов, которые будут созданы для примеров кода (перейдем к этому через минуту). И если какой-то из тестов завершится неудачей, выполнится пункт 5.
- В верхней части соответствующего фрагмента кода будет добавлено предупреждение об устаревании. Конечно же, здесь у нас приводится только пример экшена. Если вы знакомы с Github Action, то наверняка в курсе дополнительных его возможностей, например добавления уведомления о том, что что-то не так, через Slack или электронную почту. Это позволяет узнавать, на какие файлы требуется срочно обратить внимание для поддержания документации в актуальном состоянии.
- Наконец, фрагменты кода из GitHub будут автоматически включаться в документацию проекта. Так при добавлении этого предупреждения об устаревании оно в ту же секунду будет отображено в документации. И любой, кто пожелает использовать ваши фрагменты кода в своем коде, получит об этом уведомление.
А теперь рассмотрим каждый из пунктов, необходимых для запуска всего этого дела.
Если вы автор компонента
Как автору используемого вами компонента вам придется создать организацию на Bit.dev. Это бесплатно. Внутри организации у вас будет возможность добавлять коллекции и компоненты.
Выберите компонент для рабочего потока и нажмите на кнопку Integrations («Интеграции»), расположенную в правом верхнем углу страницы организации.
После этого будут показаны все потенциальные интеграции. Для нашего примера выберем Github.
Наконец, нужно будет выбрать компонент, для которого все это будет работать, а также необходимые вам репозитории (вместе с ветками). Выберите All («Все»), чтобы все, кто использует компонент, хотя бы получали уведомление при его обновлении.
Нажмите на кнопку Add («Добавить»), и новая интеграция появится точно так же, как на этом изображении:
Вот все, о чем вам надо позаботиться как автору.
Задание правильной структуры проекта
Одно из преимуществ автоматизации в том, что при ее задействовании отпадает необходимость в выполнении той или иной задачи вручную. Но для этого нужно выбрать определенные конфигурации или структуры папок. В нашем случае происходит то же самое и для выполнения этой автоматизации нужно будет следовать определенным правилам.
Структура папок
Адаптируйте следующую структуру под свои нужды, но только затем обновите остальные примеры в соответствии со сделанными в ней изменениями.
Вот структура папок, которую я выбрал для своего примерного проекта:
- Папку
.github
создаст для нас Github. Если хотите, добавьте ее сами вручную, это не проблема. В ней будет файл рабочего потока для Github action. - В папке
components
у нас будут компоненты, «импортированные» из Bit. На самом деле вы можете поместить их куда угодно. - В папке
examples
будем сохранять фрагменты кода и соответствующие тестовые файлы (внутри папкиexamples/tests
). - И, наконец, последняя интересная часть — это файл
test-output-parser.js
, который будет брать то, что выводится из тестов, парсить это и исходя из результата определять, какие файлы нужно обновлять.
Настройка тестов для фрагментов кода
Вы, должно быть, думаете: «Кто пишет тесты для фрагментов кода?». И действительно, если у вас всего два или три фрагмента кода, в этом нет никакого смысла. Если у вас только три фрагмента кода, вам вообще нет смысла делать что-либо из того, что я здесь показываю.
А вот если вы создаете что-то большое и с обширной документацией, стоит просмотреть десятки или даже сотни фрагментов кода, показывающих все возможные способы взаимодействия с проектом. И если это ваш случай, поверьте мне, вам захочется провести автоматическую проверку. И выяснить, остаются ли они валидными или нет после того, как обновится компонент, от которого они зависят.
Поэтому для каждого фрагмента кода напишите хотя бы один тест вот такой:
const {stringToCamelCase} = require("../sample1")
const assert = require("assert")
describe("Sample files: ", _ => {
describe("sample1.js: stringToCamelCase ", _ => {
it("turn any number of whitespace characters into the '_' character", done => {
let input = "hello world, this is double"
let output = stringToCamelCase(input)
assert.equal(output, "HelloWorldThisIsDouble")
done()
})
})
})
Пара замечаний:
- Для тестов я предпочитаю тестовую среду Mocha. Если вы используете что-то другое, вам снова надо будет адаптировать остальные примеры.
- Обратите внимание на то, как вложенное
describe
ссылается на имя файла соответствующего фрагмента кода. Это необходимо, чтобы понять, какой именно файл тестируется в этом тесте (другого способа сделать это на самом деле нет).
Этого должно быть достаточно, чтобы удостовериться в валидности фрагментов кода.
Создание Github Action
Итак, структура папок определена и тесты написаны. Теперь нужно позаботиться о том, чтобы после слияния запроса на включение изменений в репозиторий проекта тесты выполнились и был произведен парсинг того, что из них выводится.
С этим нам поможет GitHub. Переходим в репозиторий и нажимаем на вкладку Actions:
Нажмите на кнопку Create new workflow («Создать новый рабочий поток») и выберите любой из доступных шаблонов (мы все равно заменим их содержимое за секунду).
По сути, нам нужен рабочий поток, который запускается на экшене Push к ветке main
. Почему? Вы ведь не позволяете разработчикам напрямую выполнять push в main
. И только pull requests
(запросы на включение изменений) будут создаваться напротив него, а единственный фактический Push будет сделан Github при слиянии в репозиторий проекта. К сожалению, сейчас нет триггеров merge
, так что это событие здесь использовать не получится.
Содержимое файла рабочего потока выглядит следующим образом:
# Этот рабочий поток выполнит чистую установку зависимостей node, создаст исходный код и запустит тесты на разных версиях node
# Подробная информация здесь: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
id: example_test_runner
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: node test-output-parser.js $(./node_modules/mocha/bin/mocha examples/tests -R json)
- name: Commit files
run: |
echo ${{ github.ref }}
git add .
git restore --staged node_modules/
git config --local user.email "[email protected]"
git config --local user.name "GitHub Action"
git commit -m "ci: Automatic Deprecation Warning added" -a | exit 0
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
Немного шаблонного кода, но внимание стоит обратить вот на что:
- В строке 16 мы устанавливаем версию Node: 14. Добавьте список версий, если хотите выполнять тесты с разными версиями (а что, а вдруг :)).
- Строка 26 запускает
npm install
, так что у нас будут все зависимости, необходимые для запуска тестов в проекте. Это тоже важно, ведь если запрос на включение изменений в репозиторий проекта обновил только версию пакета внутриpackage.json
, то эта версия будет установлена. - Строка 27 запускает тесты, выполняет парсинг того, что из них выводится, и обновляет все необходимые файлы. Да, одна эта строка все это делает (через минуту мы взглянем на соответствующий файл JS).
- Строки 28–40 занимаются обновлением ветки. Так как мы, возможно, внесли изменения в некоторые файлы (фрагменты кода, которые устарели), нужно будет создать новый коммит (вот почему там появились описание коммита и учетные данные).
Этот рабочий поток будет выполняться каждый раз при слиянии запроса на включение изменений в репозиторий проекта. Это могут быть запросы, созданные на Bit, или запросы, созданными другими пользователями. В любом случае при каждом запуске потока вы будете видеть в окне вывода логов примерно следующее:
Обратите внимание, как в этом конкретном сценарии файл examples/sample1.js
обновился предупреждением об устаревании.
Заметили команду, которая используется для запуска тестов? Вот она: ./node_modules/mocha/bin/mocha examples/tests -R json
.
Приходится здесь задействовать локальную версию Mocha, которая устанавливается как зависимость, и использовать генератор отчетов JSON
, упрощающий выполнение парсинга того, что выводится. Как раз об этом и пойдет речь дальше.
Парсинг того, что выводится из тестов
Генератор отчетов JSON
в Mocha используем ради простоты: он дает всю необходимую информацию в формате, который Node.js легко интерпретирует.
Скрипт должен будет:
- Искать сообщения об ошибках.
- Для каждой ошибки будем парсить сообщение, извлекая имя файла.
- Открывать файл и добавлять в начало предупреждение об устаревании (только если оно отсутствует).
А вот код, который все это выполняет:
const { fail } = require("assert")
const fs = require("fs")
const promisify = require("util").promisify
const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile)
let input = process.argv.slice(2)
const jsonInput = JSON.parse(input.join(""))
const EXAMPLE_FOLDER = 'examples/'
const WARNING_MSG = "/** ::DeprecationWarning:: The tests for this sample are now failing **/\n"
if(jsonInput.failures.length > 0) {
console.log("There were errors")
jsonInput.failures.forEach( async f => { //перебираем сообщения с ошибкой
let failingSampleFN = f.fullTitle.split(":")[1]
let failingFilePath = EXAMPLE_FOLDER + failingSampleFN
console.log("Looking into: " + failingFilePath)
try {
let fileContent = await readFile(failingFilePath)
fileContent = fileContent.toString()
if(fileContent.indexOf(WARNING_MSG) == -1) { //предупреждение еще не добавлено
fileContent = WARNING_MSG + fileContent
await writeFile(failingFilePath, fileContent)
console.log("The file was updated with the proper warning")
} else {
console.log("The file already has the warning message")
}
} catch (e) {
throw new Error(e)
}
})
} else {
console.log("No errors found!")
}
Скрипт предельно прост: он захватывает JSON, полученный как часть массива argv
, и парсит его. Если внутри элемента errors
что-то есть, то будем перебирать эти элементы и выполнять указанные выше задачи.
Содержимое файлов будет обновляться только в том случае, если соответствующие тесты завершатся неудачей и если внутри них нет предупреждения. Это для того, чтобы не добавлялось предупреждение за предупреждением вместе с последующими запросами на включение изменений в репозиторий проекта.
А как узнать, изменил ли что-нибудь рабочий поток?
Самый простой способ проверить это — заглянуть в список файлов репозитория. Если рабочий поток был выполнен и какие-то тесты завершились неудачей, то он должен был внести изменения и сохранить их со следующим сообщением: “ci: Automatic Deprecation Warning added” («Добавлено автоматическое предупреждение об устаревании»).
Оно легко находится в списке файлов:
В этот рабочий поток при желании добавляется возможность уведомлять пользователей по предпочитаемым ими каналам (например, через Slack, по электронной почте и т. д.).
Использование фрагментов кода прямо из Github
И последнее, о чем мы еще не говорили, — включение фрагментов кода непосредственно из GitHub. Это включение приведет к тому, что документация будет автоматически обновлена со внесением в нее предупреждения об устаревании. Причем обновление это произойдет в тот самый момент, когда предупреждение будет добавлено во фрагмент кода.
Предпочитаете хранить документацию и реализацию как два отдельных проекта? Вот инструмент, который лучше всего подходит для этого: Gist-it.
Достаточно вставить тег script
там, где должен появиться ваш фрагмент кода:
<script src="http://gist-it.appspot.com/URL-TO-SNIPPET-FILE"></script>
И у нас получилась следующая страница:
<html>
<head>
</head>
<body>
<h1>
Documentation for Github actions project
</h1>
<p>
The following snippets gets automatically updated everytime something happens in Github, so we don't have to worry about manually
keeping track of it.
</p>
<p>
<script src="http://gist-it.appspot.com/https://github.com/deleteman/actions-tests-repo/blob/main/examples/sample1.js"></script>
</p>
</body>
</html>
которая отображается вот так:
Теперь все переживания останутся в прошлом: фрагмент кода будет автоматически обновляться с каждым новым изменением, опубликованным на Github. К тому же в нем реализована подсветка синтаксиса. И обратите внимание на первую строку кода: предупреждение об устаревании уже на месте.
Заключение
В ходе этой статьи был создан рабочий поток, который позволяет нам оставаться в курсе любых изменений, внесенных в зависимости. А кроме того, избавляет от лишних переживаний: теперь вы знаете, что используемые в документации фрагменты кода либо должным образом обновляются, либо хотя бы помечаются как устаревшие, так что их легко обнаружить.
Это была нелегкая задача, которая решилась с помощью нескольких простых конфигурационных файлов и небольшого скрипта.
Читайте также:
- Работа с GitHub Actions на маркетплейсе
- Реализация GitHub Action в контейнере Docker
- В гостях у GitHub Package Registry
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Fernando Doglio: Keeping Your Documentation Up-to-Date with Bit and GitHub