[Перевод] Как сделать CI на Github для современного фронтенда
Совсем скоро, 6 и 18 ноября, у нас стартуют новые потоки курса по JavaScript и
курса «Профессия Веб-разработчик», специально к их старту делимся с вами полезным туториалом, как настроить Github Actions для реальных проектов в области фронтенда со множеством линтеров и тестированием UI, а также уведомлениями о рабочем процессе в Slack. Подробности и репозиторий под катом.
GitHub Actions упрощает автоматизацию всех ваших рабочих процессов, теперь с помощью CI/CD мирового класса. Создавайте, тестируйте и развертывайте свой код прямо из GitHub. Сделайте так, чтобы проверки, управление ветками и организация проблем работали, как вы этого хотите.
Но это звучит как вступление со страницы приветствия GitHub, правда? Итак, что нам здесь действительно нужно, так это показать процесс конфигурации конвейера CI для современного веба. Этим мы и займемся. Здесь мы будем полагаться на репозиторий с предварительно настроенными GitHub Actions.
Вступление
Не секрет, что современный интерфейс основан не только на комбинации HTML + CSS + JS, которая хранится и отдается браузеру серверной частью. Создано много сложной архитектуры, чтобы доставлять высококачественный кода конечному пользователю. Мы должны заниматься этим изо дня в день.
Сейчас, как фронтенд инженеры, мы несем ответственность за хранение множества скриптов препроцессоров, конвейеров вместе с кодовой базой бизнеса, соглашениями по линтингу, тестированию, безопасности. Это позволяет отвлечься от контроля качества и сосредоточить все силы на написании кода.
Примечание. Здесь не обсуждаются процессы непрерывного развертывания (CD), потому что это другая часть экосистемы CI/CD. Наша цель — разобраться, как автоматизировать надоедливые рутинные задачи при написании кода или подготовке пул-реквестов.
Представим, что в проекте нет никого с определенной ролью DevOps. Это печально. Иногда это нормально для бизнеса, потому что даже в этом случае вы все еще можете создавать какие-то конвейеры с помощью ESLint, TSLint, Prettier, Jest и т.д.
Хотя это довольно просто сделать локально на основе документации и JavaScript, все же довольно сложно понять, как работать с внешними рабочими процессами CI и YML.
Вот, куда мы будем копать дальше.
Базовая конфигурация GitHub CI
Благослови бог GitHub и их страсть создания шаблонов для всего: пул-реквесты, проблемы и вопросы, финансовая поддержка и т.д. Так что в конечном итоге настройка CI/CD выглядит не намного сложнее. Вот, что мы имеем после активации базового рабочего процесса NodeJS на GitHub:
# This workflow will do a clean install of node dependencies, build the source code, and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actionsname: Node.js CIon:
push:
branches: [ master ]
pull_request:
branches: [ master ]jobs:
build:
runs-on: ubuntu-latest strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }} - run: npm ci
- run: npm run build — if-present
- run: npm test
Видно, что здесь не так много логики. Уже можно отметить знакомые npm, ci, build, test
.
Между npm
и yarn
здесь нет разницы. Я предпочитаю yarn
, так что в примерах фигурирует именно этот менеджер.
Здесь мы должны понимать, что среди всех полей: name
, on
, jobs
, run-on
, strategy
, steps
, build
только одно ценно для нас в смысле обновления базового шаблона. И это steps
. Они предоставляют нам возможность добавлять, удалять, изменять сценарии (действия) в течение всего рабочего процесса. Кроме того, насколько вы видите, нет ни слова о том, как автоматизировать линтинг, тестирование или другие промежуточные процессы.
Пользовательская конфигурация CI
Как только мы выяснили, как установить GitHub Actions, двинемся вперед и создадим нашу собственную конфигурацию CI.
Самая первая проблема понимания того, как работает CI — нельзя поверить, что возможно запускать те же скрипты, что и на наших локальных машинах, например yarn eslint ./someAwesomeFile.ts
или npm run jest ./someAwesomeFile.test.js
— и так далее.
Единственное основное различие: нужно выяснить, как запускать их для ожидающих коммита файлов. В конце концов, это работает аналогично, как срабатывающий перед коммитом хук lint-staged
и husky
. Но мы вернемся к этому в конце.
Создание рабочего процесса тестирования в CI
Итак, давайте углубимся в CI! Вот весь рабочий процесс тестирования:
name: Unit + UI Testing
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
unit-ui-testing:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
steps:
- uses: actions/checkout@v2
- name: Staring Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Bootstraping packages
run: yarn install
- name: Testing Shared Utils
if: always()
run: yarn jest ./shared
- name: Testing Storybook UI
if: always()
run: yarn storybook:build
Принимая во внимание, что все, что находится выше запуска сервера «Starting Node.js» (включая сам запуск), кажется нам знакомым по шаблону GitHub, мы могли бы сосредоточиться только на основных и описанных ниже сценариях тестирования.
Для работы GitHub Action CI требуется та же предварительная конфигурация, что и перед локальным запуском репозитория. Например, вот загрузка необходимых пакетов:
- name: Bootstraping packages
run: yarn install
Установка Jest + Enzyme
Хорошо, теперь мы находимся в одном шаге от создания собственной конфигурации тестирования:
- name: Testing Shared Utils
if: always()
run: yarn jest **/*.test.*
Вам интересно, что делает флаг if: always()
? Ответ прост. Если у вас есть несколько независимых шагов, но в то же время связанных общей абстракцией, они все равно смогут выполняться, даже если некоторые из них не отработают. Условия if:
вместе с внутренним API GitHub дают гибкий интерфейс для сценария.
Основная команда run: yarn jest **/__test__/*.test.*
протестирует весь репозиторий. Наша цель — тестировать не только файлы, которые были в коммите (даже если они прошли какой-то свой тест), но протестировать файлы глобально, с учетом всего влияния на код. Поэтому мы каждый раз тестируем всё. Всегда легко переключить конфигурацию на проверку только определенных файлов. Решать вам!
Настройка тестирования пользовательского интерфейса в Storybook
Обратите внимание: этот шаг можно пропустить, если вы вообще не используете тестирование пользовательского интерфейса Storybook.
- name: Testing Storybook UI
if: always()
run: yarn storybook:build
Мы храним этот код здесь, чтобы гарантировать, что на нашу общую кодовую базу пользовательского интерфейса не повлияет какая-то новая функция в коммите. Итак, в основном, получая информацию об успехе run: yarn storybook:build
мы надеемся, что сборка идет хорошо!
Создание рабочего процесса линтинга
Переходим к линтингу. Вот где мы наконец засучим рукава и наверняка получим больше удовольствия! Пришло время познакомиться с реальной конфигурацией CI с Prettier, ESLint, TSLint и StyleLint. Все идет нормально. Как и в случае с рабочим процессом тестирования, начнем с примера:
name: Lintingon:
push:
branches: [ master ]
pull_request:
branches: [ master ]jobs:
linting:
runs-on: ubuntu-latest strategy:
matrix:
node-version: [12.x] steps:
- uses: actions/checkout@v2
- name: Staring Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }} - name: Bootstraping packages
run: yarn install - name: Get file changes
id: get_file_changes
uses: trilom/file-changes-action@v1.2.4
with:
output: ' ' - name: Echo file changes
id: hello
run: |
echo Added files: ${{ steps.get_file_changes.outputs.files_added }}
echo Changed files: ${{ steps.get_file_changes.outputs.files_modified }}
echo Removed files: ${{ steps.get_file_changes.outputs.files_removed }} - name: Prettier Checking
if: ${{ always() && (steps.get_file_changes.outputs.files_added || steps.get_file_changes.outputs.files_modified) }}
run: yarn prettier --config ./prettier.config.js --ignore-path ./.prettierignore ${{ steps.get_file_changes.outputs.files_added }} ${{ steps.get_file_changes.outputs.files_modified }} --fix - name: ESLint Checking
if: ${{ always() && (steps.get_file_changes.outputs.files_added || steps.get_file_changes.outputs.files_modified) }}
run: yarn eslint --config ./.eslintrc.js --ignore-path ./.eslintignore ${{ steps.get_file_changes.outputs.files_added }} ${{ steps.get_file_changes.outputs.files_modified }} --fix - name: TSLint Checking
if: ${{ always() && (steps.get_file_changes.outputs.files_added || steps.get_file_changes.outputs.files_modified) }}
run: yarn tslint --config ./tslint.json -e "**/*.(js|jsx|css|scss|html|md|json|yml)" ${{ steps.get_file_changes.outputs.files_added }} ${{ steps.get_file_changes.outputs.files_modified }} --fix - name: StyleLint Checking
if: ${{ always() && (steps.get_file_changes.outputs.files_added || steps.get_file_changes.outputs.files_modified) }}
run: yarn stylelint --config ./.stylelintrc --ignore-path ./.stylelintignore --allow-empty-input ${{ steps.get_file_changes.outputs.files_added }} ${{ steps.get_file_changes.outputs.files_modified }} --fix - name: Commit changes
if: always()
uses: stefanzweifel/git-auto-commit-action@v4.1.2
with:
commit_message: Apply formatting changes
Как обычно, опуская все вышеперечисленное в разделе Bootstraping, переходим к обсуждению всего остального. Для ясности уточню: главное — делать линтинг только подготовленных к коммиту файлов. Нет смысла каждый раз проверять весь репозиторий. Иначе нам потребовалось бы довольно много времени, чтобы проверить и исправить каждый конкретный файл. Это нонсенс.
Настройка изменений файлов
Мы соберем все файлы из коммита для дальнейших операций. Думаю, в нашем случае очень полезно действие trilom/file-changes-action
, которое могло бы «поднять» все измененные имена файлов.
- name: Get file changes
id: get_file_changes
uses: trilom/file-changes-action@v1.2.4
with:
output: ' '- name: Echo file changes
id: echo_file_changes
run: |
echo Added files: ${{ steps.get_file_changes.outputs.files_added }}
echo Changed files: ${{ steps.get_file_changes.outputs.files_modified }}
echo Removed files: ${{ steps.get_file_changes.outputs.files_removed }}
В свою очередь, наряду с настройкой echo
этих файлов на следующем шаге, мы могли бы создать строку, состоящую из путей к файлам. И в конце концов, создать возможность отправлять все файлы дальше в eslint и другие конвейеры. Итак, границы Get file changes
и Echo file changes
— это наша стратегически важная точка для всех дальнейших шагов проверки.
Настройка проверки Prettier
Все правильно! Мы собрали файлы, обработка которых точно необходима. Теперь пришло время линтинга. И первый шаг — оптимизация Prettier:
- name: Prettier Checking
if: ${{ always() && (steps.get_file_changes.outputs.files_added || steps.get_file_changes.outputs.files_modified) }}
run: yarn prettier --config ./prettier.config.js --ignore-path ./.prettierignore ${{ steps.get_file_changes.outputs.files_added }} ${{ steps.get_file_changes.outputs.files_modified }} --fix
Это выглядит немного пугающе, правда? Кроме того, при ближайшем рассмотрении оказывается, что здесь есть только оператор if
и скрипт run
. Не слишком много, чтобы испугаться. Начнем с if:
. Это кажется немного сложным по сравнению с тем, что мы видели внутри рабочего процесса тестирования. Здесь нет никаких сюрпризов, нужно проверять только отдельные файлы, а в рабочем процессе тестирования проверка выполняется по всей кодовой базе.
Таким образом, условие if:
указывает, на то, чтобы начать проверку, только если есть несколько новых или измененных файлов. В случае удаления проверять точно нечего. Кстати, он должен запуститься, даже если некоторые шаги не отработают (так же, как в случае с Jest или Storybook). Что же касается команды run:
, она полностью совпадает с командой, которую вы запускаете локально (на основе схемы Prettier CLI).
Настройка проверки ESLint
То же, что и на предыдущем шаге. Просто собственный интерфейс:
- name: ESLint Checking
if: ${{ always() && (steps.get_file_changes.outputs.files_added || steps.get_file_changes.outputs.files_modified) }}
run: yarn eslint --config ./.eslintrc.js --ignore-path ./.eslintignore ${{ steps.get_file_changes.outputs.files_added }} ${{ steps.get_file_changes.outputs.files_modified }} --fix
Настройка проверки TSLint
Еще один такой же интерфейс:
- name: TSLint Checking
if: ${{ always() && (steps.get_file_changes.outputs.files_added || steps.get_file_changes.outputs.files_modified) }}
run: yarn tslint --config ./tslint.json -e "**/*.(js|jsx|css|scss|html|md|json|yml)" ${{ steps.get_file_changes.outputs.files_added }} ${{ steps.get_file_changes.outputs.files_modified }} --fix
Разница здесь только в том, что вы не можете использовать глобальный файл .tslintignore
, потому что такого файла еще нет в TSLint. Нам буквально нужно поместить все правила исключений в аргументы CLI.
Настройка проверки StyleLint
Все выглядит так же, как в случае с ESLint и Prettier. Просто собственный интерфейс run:
- name: StyleLint Checking
if: ${{ always() && (steps.get_file_changes.outputs.files_added || steps.get_file_changes.outputs.files_modified) }}
run: yarn stylelint --config ./.stylelintrc --ignore-path ./.stylelintignore --allow-empty-input ${{ steps.get_file_changes.outputs.files_added }} ${{ steps.get_file_changes.outputs.files_modified }} --fix
Настройка действия Commit changes
Фух, мы сделали это! Конвейер состоит из 4 независимых проверок. Прекрасная работа! Теперь пора двигаться дальше. Поговорим о способах сохранения обработанных изменений (файлов). Есть довольно полезный пакет под названием stefanzweifel/git-auto-commit-action
:
- name: Commit changes
if: always()
uses: stefanzweifel/git-auto-commit-action@v4.1.2
with:
commit_message: Apply formatting changes
Он направлен на автоматизацию коммита подготовленных файлов. Таким образом сохраняются сразу все изменения конвейера. И вот он, второй файл рабочего процесса. Теперь в ваших руках все возможности автоматизации и улучшения кодовой базы.
Бонусы — кеширование Yarn и уведомления в Slack
На всякий случай, если вы хотите продолжить, я бы порекомендовал интегрировать еще несколько Github Actions.
Кеширование Yarn
Как и при локальной разработке, требуется какое-то время, чтобы собрать и установить все пакеты репозитория. Есть способ уменьшить время начальной загрузки с 15 до 1 минуты.
Конечно, возможно сделать кеширование каждого установленного пакета на стороне CI. Чтобы проделать этот трюк, просто замените шаг начальной загрузки действием actions/cache@v2
.
- name: Restoring Yarn cache
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}- name: Bootstraping packages
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install
Включение уведомлений в Slack
Если вы используете Slack как источник при общении с командой, важно иметь уведомления о процессах CI/CD. Благодаря 8398a7/action-slack
можно легко управлять тем, какие уведомления и их тексты нам нужны. Просто добавьте этот шаг в рабочие процессы сразу после всего вышеперечисленного.
- name: Slack Notification
uses: 8398a7/action-slack@v3.8.0
if: failure()
with:
status: custom
fields: workflow,job,commit,repo,ref,author,took
custom_payload: |
{
username: 'Awesome-CI',
icon_emoji: ':react:',
author_name: 'Linting Test',
attachments: [{
color: '${{ job.status }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning',
text: `CI Task: ${process.env.AS_WORKFLOW}\ncommit: (${process.env.AS_COMMIT}) ${{ github.event_name }} ${{ job.status }}. Initiated by ${process.env.AS_AUTHOR} in ${process.env.AS_TOOK}`,
}]
}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
MATRIX_CONTEXT: ${{ toJson(matrix) }}
Я должен сказать, что чтобы иметь возможность работать с уведомлениями в Slack, вы должны создать URL веб-хука (код показан выше) и указать его в настройках Slack. Вот так выглядят все рабочие процессы:
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Linting
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
linting:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.head_ref }}
- name: Staring Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Restoring Yarn cache
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Bootstraping packages
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install
- name: Get file changes
id: get_file_changes
uses: trilom/file-changes-action@v1.2.3
with:
output: ' '
- name: Echo file changes
id: hello
run: |
echo Added files: ${{ steps.get_file_changes.outputs.files_added }}
echo Changed files: ${{ steps.get_file_changes.outputs.files_modified }}
echo Removed files: ${{ steps.get_file_changes.outputs.files_removed }}
- name: Prettier Checking
if: ${{ always() && (steps.get_file_changes.outputs.files_added || steps.get_file_changes.outputs.files_modified) }}
run: yarn prettier --config ./prettier.config.js --ignore-path ./.prettierignore ${{ steps.get_file_changes.outputs.files_added }} ${{ steps.get_file_changes.outputs.files_modified }} --fix
- name: ESLint Checking
if: ${{ always() && (steps.get_file_changes.outputs.files_added || steps.get_file_changes.outputs.files_modified) }}
run: yarn eslint --config ./.eslintrc.js --ignore-path ./.eslintignore ${{ steps.get_file_changes.outputs.files_added }} ${{ steps.get_file_changes.outputs.files_modified }} --fix
- name: TSLint Checking
if: ${{ always() && (steps.get_file_changes.outputs.files_added || steps.get_file_changes.outputs.files_modified) }}
run: yarn tslint --config ./tslint.json -e "**/*.(js|jsx|css|scss|html|md|json|yml)" ${{ steps.get_file_changes.outputs.files_added }} ${{ steps.get_file_changes.outputs.files_modified }} --fix
- name: StyleLint Checking
if: ${{ always() && (steps.get_file_changes.outputs.files_added || steps.get_file_changes.outputs.files_modified) }}
run: yarn stylelint --config ./.stylelintrc --ignore-path ./.stylelintignore --allow-empty-input ${{ steps.get_file_changes.outputs.files_added }} ${{ steps.get_file_changes.outputs.files_modified }} --fix
- name: Commit changes
if: always()
uses: stefanzweifel/git-auto-commit-action@v4.1.2
with:
commit_message: Apply formatting changes
# branch: ${{ github.head_ref }}
- name: Slack Notification
uses: 8398a7/action-slack@v3.8.0
if: failure()
with:
status: custom
fields: workflow,job,commit,repo,ref,author,took
custom_payload: |
{
username: 'React-Apps-CI',
icon_emoji: ':react:',
author_name: 'Linting Test',
attachments: [{
color: '${{ job.status }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning',
text: `CI Task: ${process.env.AS_WORKFLOW}\ncommit: (${process.env.AS_COMMIT}) ${{ github.event_name }} ${{ job.status }}. Initiated by ${process.env.AS_AUTHOR} in ${process.env.AS_TOOK}`,
}]
}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
MATRIX_CONTEXT: ${{ toJson(matrix) }}
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Unit + UI Testing
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
unit-ui-testing:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
steps:
- uses: actions/checkout@v2
- name: Staring Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Restoring Yarn cache
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
- name: Bootstraping packages
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install
- name: Testing Shared Utils
if: always()
run: yarn jest ./shared
- name: Testing Storybook UI
if: always()
run: yarn storybook:build
- name: Slack Notification
uses: 8398a7/action-slack@v3.8.0
if: failure()
with:
status: custom
fields: workflow,job,commit,repo,ref,author,took
custom_payload: |
{
username: 'React-Apps-CI',
icon_emoji: ':react:',
author_name: 'Unit + UI Integration Test',
attachments: [{
color: '${{ job.status }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning',
text: `CI Task: ${process.env.AS_WORKFLOW}\ncommit: (${process.env.AS_COMMIT}) ${{ github.event_name }} ${{ job.status }}. Initiated by ${process.env.AS_AUTHOR} in ${process.env.AS_TOOK}`,
}]
}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
MATRIX_CONTEXT: ${{ toJson(matrix) }}
Подведение итогов
Сегодня мы создали рабочие процессы CI, которые могут выполнять множество рутинных задач, не тратя ваше личное время. Как видите достаточно просто управлять линтингом и тестированием. Благодаря GitHub CI вы почти не нуждаетесь в стороннем программном обеспечении, хуках, аналогах Actions в других CI. Попробуйте использовать свои скрипты, которые синхронизируются только с package.json
. Похоже, что в какой-то момент станет возможно жить свободно без каких-либо ручных действий, связанных с коммитом.
А на тот случай если вы задумали сменить сферу или повысить свою квалификацию — промокод HABR даст вам дополнительные 10% к скидке указанной на баннере.