Пишем простейший GitHub Action на TypeScript

Недавно я решил немного привести в порядок несколько своих .NET pet-проектов на GitHub, настроить для них нормальный CI/CD через GitHub Actions и вынести всё в отдельный репозиторий, чтобы все скрипты лежали в одном месте. Для этого пришлось немного поизучать документацию, примеры и существующие GitHub Actions, выложенные в Marketplace.

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

Статья в первую очередь рассчитана на начинающих, тех, кто никогда не использовал GitHub Actions, но хотел бы быстро начать. Тем не менее, даже если у вас уже есть подобный опыт, но вы, например, не использовали ncc, то, возможно, и для вас в ней будет что-то полезное.

Краткое введение в GitHub Actions

В основе GitHub Actions лежит понятие Workflow — некий рабочий процесс, который запускается по определённым триггерам. В одном репозитории может быть как один рабочий процесс, так и несколько отдельных, срабатывающий при разных условиях.

Чтобы добавить рабочий процесс, нужно создать в репозитории один или несколько yml-файлов в папке .github/workflows. Простейший файл может выглядеть следующим образом:

name: Hello

on: [push]

jobs:
  Hello:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Hello
        run: echo "Hello, GitHub Actions!"

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

Сам рабочий процесс мы можем найти на вкладке Actions в интерфейсе GitHub и посмотреть детальную информацию по нему:

0365571eb7d72d99caa8ea1d52c9f949.PNG

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

Каждый джоб состоит из набора шагов, которые последовательно запускаются внутри одной виртуальной машины. Каждый такой шаг в свою очередь может либо выполнять произвольную команду через run, либо может запускать какое-то действие через uses.

Сами действия представляют из себя отдельный кусок рабочего процесса, который можно повторно использовать в нескольких проектах. Они могут быть трёх видов: докер контейнер, действие JavaScript, или составные действия (которые по сути просто переиспользуемый набор других действий). Вариант с действием JavaScript мы как раз и рассмотрим в этой статье, но писать его будем на TypeScript для большего удобства.

(Более подробную информацию по возможностям рабочих процессов можно найти в документации)

Пара слов о месте размещения действий в репозитории

Действия можно разместить в репозитории в нескольких местах в зависимости от потребностей:

  • В подпапке .github/actions. Такой способ обычно используется когда вы хотите использовать эти действия из того же репозитория. В этом случае ссылаться на них необходимо по пути без указания ветки или тега:

    steps:
      - uses: ./.github/actions/{path}
  • В произвольном месте репозитория. Например, можно разместить несколько действий в корне репозитория, каждое в своей подпапке. Такой способ хорошо подходит, если вы хотите сделать что-то вроде личной библиотеки с набором действий, которые собираетесь использовать из разных проектов. В этом случае ссылаться на такие действия нужно по названию репозитория, пути и ветке или тегу:

    steps:
      - uses: {owner}/{repo}/{path}@{ref}
  • В корне репозитория. Такой способ позволяет разместить в одном репозитории только одно действие, и обычно используется если вы хотите позже опубликовать его в Marketplace. В этом случае ссылаться на такие действия нужно по названию репозитория и ветке или тегу:

    steps:
      - uses: {owner}/{repo}@{ref}

Создаём действие на TypeScript

В качестве примера создадим очень простое действие, которое просто выводит сообщение Hello, GitHub Actions!. Для разработки действия нам потребуется установленная версия Node.js (я использовал v14.15.5).

Создадим в репозитории папку .github/actions. В ней создадим подпапку hello, в которой будем далее создавать все файлы, относящиеся к нашему действию. Нам потребуется создать всего четыре файла.

Создаём файл action.yml:

name: Hello
description: Greet someone

runs:
  using: node12
  main: dist/index.js

inputs:
  Name:
    description: Who to greet
    required: true

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

Также мы указываем, что это именно JavaScript действие, а не докер контейнер и указываем точку входа: файл dist/index.js. Этого файла у нас пока нет, но он будет автоматически создан чуть позже.

Кроме этого мы можем указать в этом файле набор входных и выходных параметров. В данном случае мы передаём в качестве параметра имя для приветствия.

(Более подробную информацию по возможностям файла метаданных можно найти в документации)

Создаём файл package.json:

{
  "private": true,
  "scripts": {
    "build": "npx ncc build ./src/main.ts"
  },
  "dependencies": {
    "@actions/core": "^1.2.7",
    "@actions/exec": "^1.0.4",
    "@actions/github": "^4.0.0",
    "@actions/glob": "^0.1.2",
    "@types/node": "^14.14.41",
    "@vercel/ncc": "^0.28.3",
    "typescript": "^4.2.4"
  }
}

Это стандартный файл для Node.js. Чтобы не указывать бесполезные атрибуты типа автора, лицензии и т.п. можно просто указать, что пакет private. (При желании можно конечно всё это указать, кто я такой, чтобы вам это запрещать =)

В скриптах мы указываем один единственный скрипт сборки, который запускает утилиту ncc. Эта утилита на вход получает файл src/main.ts и создаёт файл dist/index.js, который является точкой входа для нашего действия. Я вернусь к этой утилите чуть позже.

В качестве зависимостей мы указываем typescript и @types/node для работы TypeScript. Зависимость @vercel/ncc нужна для работы ncc.

Зависимости @actions/* опциональны и являются частью GitHub Actions Toolkit — набора пакетов для разработки действий (я перечислил самые на мой взгляд полезные, но далеко не все):

  • Зависимость @actions/core понадобится вам вероятнее всего. Там содержится базовый функционал по выводу логов, чтению параметров действия и т.п. (документация)

  • Зависимость @actions/exec нужна для запуска других процессов, например dotnet. (документация)

  • Зависимость @actions/github нужна для взаимодействия с GitHub API. (документация)

  • Зависимость @actions/glob нужна для поиска файлов по маске. (документация)

Стоит также отметить, что формально, поскольку мы будем компилировать наше действие в один единственный файл dist/index.js через ncc, все зависимости у нас будут зависимостями времени разработки, т.е. их правильнее помещать не в dependencies, а в devDependencies. Но по большому счёту никакой разницы нет, т.к. эти зависимости вообще не будут использоваться во время выполнения.

Создаём файл tsconfig.json:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "CommonJS",
    "moduleResolution": "Node",
    "strict": true
  }
}

Тут всё достаточно просто. Это минимальный файл, с которым всё хорошо работает, включая нормальную подсветку синтаксиса и IntelliSense в Visual Studio Code.

Создаём файл src/main.ts:

import * as core from '@actions/core'

async function main() {
  try {

    const name = core.getInput('Name');
    core.info(`Hello, ${name}`);

  } catch (error) {
    core.setFailed(error.message)
  }
}

main();

В качестве точки входа мы используем синхронную или асинхронную функцию main, которую вызываем в этом же файле. (Название функции также может быть любым).

Тело функции необходимо обернуть в блок try-catch, чтобы в случае любых ошибок сборка в GitHub прерывалась. Без этого она всегда будет считаться успешной.

В данном конкретном случае мы также используем пакет @actions/core для чтения параметров и вывода сообщения в лог.

Собираем действие при помощи ncc

После того, как все файлы созданы нам первым делом необходимо восстановить все пакеты из npm. Для этого нам нужно перейти в папку действия (не в корень репозитория) и выполнить команду:

npm install

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

В качестве альтернативы можно воспользоваться пакетом @vercel/ncc, который позволяет собрать js или ts-файлы в один единственный js-файл, который уже можно закоммитить в репозиторий.

Поскольку мы уже указали скрипт для сборки в файле package.json, нам осталось только запустить команду:

npm run build

В результате мы получим файл dist/index.js, который нужно будет закоммитить в репозиторий вместе с остальными файлами. Папка node_modules при этом может быть спокойно отправлена в .gitignore.

Используем действие

Чтобы протестировать действие, создадим в папке .github/workflows файл рабочего процесса. Для разнообразия сделаем так, чтобы он запускался не по пушу, а вручную из интерфейса:

name: Hello

on:
  workflow_dispatch:
    inputs:
      Name:
        description: Who to greet
        required: true
        default: 'GitHub Actions'

jobs:
  Hello:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Hello
        uses: ./.github/actions/hello
        with:
          Name: ${{ github.event.inputs.Name }}

Здесь настройки workflow_dispatch описывают форму в интерфейсе GitHub в которую пользователь сможет ввести данные. Там у нас будет одно единственное поле для ввода имени для приветствия.

Данные, введённые в форму через событие передаются в действие, которое мы запускаем в рабочем процессе через параметр github.event.inputs.Name.

Кроме этого, поскольку действие находится в том же репозитории, что и рабочий процесс, нам обязательно необходимо сделать checkout, чтобы все скрипты были доступны в рабочей папке.

После того, как мы запушим наш рабочий процесс, мы можем перейти в интерфейс GitHub, на странице Actions выбрать наш рабочий процесс и запустить его выполнение, указав параметры:

f8b92d0ec6e3136d9d2d3c63022c42ed.PNG

После того, как мы запустим рабочий процесс, можем зайти в него и посмотреть, что получилось:

226595813be400aa53b129c4ccb7974c.png

Настраиваем GitHooks для автоматической сборки

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

Каждый раз, когда мы будем изменять код действия нам нужно не забыть вызвать команду:

npm run build

По моему опыту такой ручной запуск постоянно будет приводить к ситуации «Я вроде поменял код и запушил его на сервер, но у меня ничего не поменялось. А, блин, я же забыл пересобрать файлы…».

Решить эту проблему можно несколькими способами. Можно не париться и действительно каждый раз запускать сборку руками. В некоторых проектах люди подходят к этому основательно и делают специальный рабочий процесс через тот же GitHub Actions, который на каждый коммит пытается пересобрать действие и коммитит изменения.

Я для себя себя решил проблему проще и просто добавил гит хук, который автоматически пересобирает все действия в репозитории на каждый коммит. Для этого необходимо создать в репозитории папку .githooks, а в ней файл pre-commit следующего вида:

#!/bin/sh

for action in $(find ".github/actions" -name "action.yml"); do
    action_path=$(dirname $action)
    action_name=$(basename $action_path)

    echo "Building \"$action_name\" action..."
    pushd "$action_path" >/dev/null
    npm run build
    git add "dist/index.js"
    popd >/dev/null
    echo
done

Здесь мы находим все файлы action.yml в папке .github/actions и для всех найденных файлов запускаем сборку из их папки. Теперь нам не нужно будет думать о явной пересборке наших действий, она будет делаться автоматически.

Чтобы хуки из папки .githooks запускались, нам необходимо поменять конфигурацию для текущего репозитория:

git config core.hooksPath .githooks

Или можно сделать это глобально (я сделал именно так):

git config --global core.hooksPath .githooks

Заключение

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

Репозиторий с примером можно найти тут

© Habrahabr.ru