Поддержание документации в актуальном состоянии с помощью Bit и GitHub

Написание документации для кода (будь то небольшая библиотека или полный набор микросервисов, взаимодействие с которыми необходимо другим разработчикам)  —  это занятие кропотливое и утомительное.

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

Перейдем к его рассмотрению.

Рабочий поток

Главный в этом рабочем потоке  —  бот GitHub на Bit. Он занимается созданием Pull Request (запроса на включение изменений) для каждого репозитория, который в данный момент использует обновляемый компонент.

Задействование автоматизации в задачах позволяет тем, кто работает с компонентами, получать уведомления в тот момент, когда что-то меняется. Но будем откровенны: одна из главных проблем современной экосистемы JavaScript в ее невероятной быстроте. Она настолько быстра, что библиотеки и фреймворки обновляются каждый день. И эти обновления иногда разбивают часть кода (или фактически часть документации).

Помочь избежать этого должен рабочий поток, состоящий из бота Bit, Github Action и инструмента Gist-It. Он позволяет автоматически импортировать из Github фрагменты кода.

Чтобы лучше представлять себе, о чем дальше пойдет речь, взгляните на то, как должен выглядеть финальный рабочий поток:

Выглядит сложновато, но стоит рассмотреть получше, и станет понятно, что ничего сложного здесь нет:

  1. Начнем с начала. Триггером для всего рабочего потока будет обновление, сделанное в компоненте. Безопасным это обновление делает то, что Bit известно о каждом репозитории, использующем компонент. Это, в свою очередь, позволяет реагировать на обновление компонента вместо того, чтобы постоянно проверять, не изменилось ли что-то.
  2. Благодаря этой интеграции Bit затем создает запрос на включение изменений в репозиторий для каждого проекта, использующего обновленный компонент. В случае проектов, код которых уже интегрирован в кодовую базу, запрос на включение изменений в репозиторий проекта обновит этот код. Если же проекты просто импортируют компонент в качестве зависимости, запрос на включение изменений в репозиторий проекта обновит версию зависимости. В любом случае будет создано изменение.
  3. И на этом пока что все. Здесь рабочий поток останавливается, пока ответственный за сопровождение проекта не решит выполнить слияние запроса на включение изменений в репозиторий проекта (пункт 3). Конечно, Github укажет на любые конфликты слияния (если таковые имеются), и вы дальше сами решите, как с ними быть. После же слияния произойдет следующее:
  4. Будет активирован GitHub action. Этот экшен запустит набор тестов, которые будут созданы для примеров кода (перейдем к этому через минуту). И если какой-то из тестов завершится неудачей, выполнится пункт 5.
  5. В верхней части соответствующего фрагмента кода будет добавлено предупреждение об устаревании. Конечно же, здесь у нас приводится только пример экшена. Если вы знакомы с Github Action, то наверняка в курсе дополнительных его возможностей, например добавления уведомления о том, что что-то не так, через Slack или электронную почту. Это позволяет узнавать, на какие файлы требуется срочно обратить внимание для поддержания документации в актуальном состоянии.
  6. Наконец, фрагменты кода из 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()
        })
    })
})

Пара замечаний:

  1. Для тестов я предпочитаю тестовую среду Mocha. Если вы используете что-то другое, вам снова надо будет адаптировать остальные примеры.
  2. Обратите внимание на то, как вложенное 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 легко интерпретирует.

Скрипт должен будет:

  1. Искать сообщения об ошибках.
  2. Для каждой ошибки будем парсить сообщение, извлекая имя файла.
  3. Открывать файл и добавлять в начало предупреждение об устаревании (только если оно отсутствует).

А вот код, который все это выполняет:

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. К тому же в нем реализована подсветка синтаксиса. И обратите внимание на первую строку кода: предупреждение об устаревании уже на месте.

Заключение

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

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

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Fernando Doglio: Keeping Your Documentation Up-to-Date with Bit and GitHub

Предыдущая статьяЗа гранью HCD: нужен ли новый подход в дизайне для ИИ?
Следующая статьяСоздание простого веб-сервера с помощью Node.js и Express