[Перевод] Как сделать CI на Github для современного фронтенда

aaj0lbmtmx7uv9lx5yba1euf6va.jpeg


Совсем скоро, 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) }}


nbgvpjshhceotacr-ogedgozwfi.jpeg

Подведение итогов


Сегодня мы создали рабочие процессы CI, которые могут выполнять множество рутинных задач, не тратя ваше личное время. Как видите достаточно просто управлять линтингом и тестированием. Благодаря GitHub CI вы почти не нуждаетесь в стороннем программном обеспечении, хуках, аналогах Actions в других CI. Попробуйте использовать свои скрипты, которые синхронизируются только с package.json. Похоже, что в какой-то момент станет возможно жить свободно без каких-либо ручных действий, связанных с коммитом.

А на тот случай если вы задумали сменить сферу или повысить свою квалификацию — промокод HABR даст вам дополнительные 10% к скидке указанной на баннере.

image


Eще курсы


Рекомендуемые статьи


© Habrahabr.ru