GitHub Actions. Начало работы, часть 2 из 2
19.06.2022
Теги: Git • GitHub • Linux • Web-разработка • Конфигурация • Сервер
Простой проект
Хорошо, общее представление о GitHub Actions у нас есть, теперь хотелось бы применить эти знания на практике. Давайте создадим небольшой проект, выложим его на GitHub, создадим для него workflow-файл, который будет запускать проверку кода линтером, выполнять сборку и деплоить на production сервер.
1. Деплой без GitHub
Итак, в директории проекта есть файл index.html
и несколько css-файлов. Установлен npm-пакет browser-sync
, чтобы сразу видеть изменения страницы при редактировании html и css файлов + пакеты для сборки проекта в директорию dist
с минификацией html и css файлов. Файл package.json
:
{ "scripts": { "start": "browser-sync start --server src --no-notify --no-ui --cwd src --files index.html,css/**/*", "lint": "editorconfig-checker --exclude node_modules", "html": "html-minifier --remove-comments --collapse-whitespace --input-dir src --output-dir dist --file-ext html", "css": "postcss src/css/index.css --use postcss-import --use postcss-csso --no-map --output dist/css/index.css", "build": "npm run html && npm run css", "deploy": "rsync --archive --delete ./dist/ timeweb-hosting:/home/c/ca12345/github-workflow-example/public_html" }, "devDependencies": { "browser-sync": "^2.27.10", "editorconfig-checker": "^4.0.2", "html-minifier": "^4.0.0", "postcss-cli": "^9.1.0", "postcss-csso": "^6.0.0", "postcss-import": "^14.1.0" } }
Для деплоя проекта на хостинг TimeWeb используется утилита rsync
. Строка timeweb-hosting
определена в файле конфигурации ssh-клиента ~/.ssh/config
.
Host timeweb-hosting User ca12345 HostName tokmakov.msk.ru Port 22 IdentityFile ~/.ssh/timeweb-hosting
Подключение к серверу с использованием ключей, которые предварительно надо создать и скопировать на сервер:
$ cd ~/.ssh/ $ ssh-keygen Generating public/private rsa key pair. Enter file in which to save the key (/c/Users/Evgeniy/.ssh/id_rsa): timeweb-hosting Enter passphrase (empty for no passphrase): Enter Enter same passphrase again: Enter Your identification has been saved in timeweb-hosting Your public key has been saved in timeweb-hosting.pub The key fingerprint is: SHA256:Lo+iUk+SHnxMwPfJkeMny6vrYDPpNxArrLqrWpjwNm8 Evgeniy@TKMCOMP
$ ssh-copy-id -i ~/.ssh/timeweb-hosting.pub ca12345@tokmakov.msk.ru /usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/c/Users/Evgeniy/.ssh/timeweb-hosting.pub" /usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed /usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys ca12345@tokmakov.msk.ru's password: пароль Number of key(s) added: 1 Now try logging into the machine, with: "ssh ca12345@tokmakov.msk.ru" and check to make sure that only the key(s) you wanted were added.
$ ssh timeweb-hosting ########################## Welcome to TimeWeb server! ########################## Last login: Sat Jun 18 11:35:54 2022 from 123.123.123.123
Для запуска линтера, сборки проекта, деплоя на сервер:
$ npm run lint $ npm run build $ nmp run deploy
rsync
? При установке Git для Windows устанавливается терминал, позволяющий работать с git из командной строки. Заодно из терминала можно запускать некоторые Linux-команды. Но можно расширить возможности терминала, добавив другие полезные утилиты. Как это сделать — читайте здесь.
2. GitHub Actions
Создаем новый репозиторий на GitHub, на локальном компьютере в директории проекта выполняем команды:
$ git init # создаем пустой репозиторий $ git add --all # добавляем все файлы проекта $ git commit -m "Initial сommit" # первый коммит
Добавляем себе ссылку (pointer) на удаленный репозиторий:
$ git remote add origin git@github.com:tokmakov/github-workflow-example.git
Теперь удаленный репозиторий доступен как origin
:
$ git remote origin
Отправляем текущую ветку master
(которая у нас сейчас всего одна) в удаленную ветку master
репозитория origin
:
$ git push origin master
Настроим текущую ветку master
на отслеживание удаленной ветки master
:
$ git branch -u origin/master Branch master set up to track remote branch master from origin.
Теперь самое интересное — создаем локально yml-файл и выкладываем его на GitHub:
name: Lint and deploy on: push: jobs: lint: # проверка кода линтером runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 16 - name: Lint new code run: | npm ci npm run lint deploy: # деплой на prod сервер # на сервер выкладываем только ветку master if: ${{ github.ref == 'refs/heads/master' }} runs-on: ubuntu-latest # выкладываем, только если проверка успешна needs: [lint] steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 16 - name: Build project run: | npm ci npm run build - name: Add ssh key run: | mkdir ~/.ssh echo "${{ secrets.KEY }}" > ~/.ssh/key chmod 600 ~/.ssh/key - name: Deploy project env: options: ssh -i ~/.ssh/key -o StrictHostKeyChecking=no run: | rsync -e "$options" --archive --delete ./dist/ ${{ secrets.USER }}@${{ secrets.HOST }}:${{ secrets.WWW }}
$ git add .github/workflows/deploy.yml $ git commit -m "Add workflow file deploy.yml" $ git push
Теперь, каждый раз, когда мы выполняем на своем компе команду git push
, будет запускаться задание lint
, которое проверит наш код. И если это задание отработает успешно — будет запущено задание deploy
, которое соберет проект и выложит все файлы из директории dist
на удаленный веб-сервер.
В workflow-файле используется контекст secrets
— приватный ssh-ключ, имя пользователя, ip-адрес (домен) сервера, корневая директория веб-сервера. Так что все эти переменные контекста надо создать — на GitHub идем в Settings репозитория, слева находим ссылку Secrets, дальше Actions.
Чтобы получить приватный ключ, выполняем команду cat ~/.ssh/timeweb-hosting
и копируем вывод в буфер обмена.
Кастомный action
В GitHub Marketplace есть отдельный раздел, посвященный Actions. Некоторые из них созданы известными компаниями, но бóльшая часть публикуется разработчиками. Но допустим, не удалось найти готовое решение для решения задачи. В таком случае можно воспользоваться инструкцией и содать кастомный action
. Код action
может написан на языке JavaScript или быть собран в виде Docker-образа.
Общий принцип создания action
— нужен отдельный репозиторий, который будет содержать файл action.yml
и все прочие файлы, необходимые для работы action
.
name: Название action author: Автор action description: Описание action inputs: # входные данные outputs: # выходные данные runs: # директивы запуска
Кроме того, action
можно создать в отдельной директории внутри .github/actions
. Эта директория должна содержать файл action.yml
и все прочие файлы, необходимые для работы action
. В этом случае обращение к этому action
из workflow-файла будет иметь вид ./path/to/dir
. Также необходимо скопировать файлы репозитрия внутрь временного сервера, где будет работать workflow.
|-- hello-world (repository) | |__ .github | └── workflows | └── my-first-workflow.yml | └── actions | |__ hello-world-action | └── action.yml
jobs: example: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: ./.github/actions/hello-world-action
1. Action как Docker-образ
Наш кастомный action
будет выполнять простой bash-скрипт. Этот скрипт будет обращаться к API сервиса JSON Placeholder и получать имя пользователя по идентификатору. Пользователей всего 10, идентификаторы — числа от 1 до 10.
https://jsonplaceholder.typicode.com/users
[ { "id": 1, "name": "Leanne Graham", "username": "Bret", "email": "Sincere@april.biz", "address": { "street": "Kulas Light", "suite": "Apt. 556", "city": "Gwenborough", "zipcode": "92998-3874", "geo": { "lat": "-37.3159", "lng": "81.1496" } }, "phone": "1-770-736-8031 x56442", "website": "hildegard.org", "company": { "name": "Romaguera-Crona", "catchPhrase": "Multi-layered client-server neural-net", "bs": "harness real-time e-markets" } }, { "id": 2, "name": "Ervin Howell", "username": "Antonette", "email": "Shanna@melissa.tv", "address": { "street": "Victor Plains", "suite": "Suite 879", "city": "Wisokyburgh", "zipcode": "90566-7771", "geo": { "lat": "-43.9509", "lng": "-34.4618" } }, "phone": "010-692-6593 x09125", "website": "anastasia.net", "company": { "name": "Deckow-Crist", "catchPhrase": "Proactive didactic contingency", "bs": "synergize scalable supply-chains" } }, { "id": 3, "name": "Clementine Bauch", "username": "Samantha", "email": "Nathan@yesenia.net", "address": { "street": "Douglas Extension", "suite": "Suite 847", "city": "McKenziehaven", "zipcode": "59590-4157", "geo": { "lat": "-68.6102", "lng": "-47.0653" } }, "phone": "1-463-123-4447", "website": "ramiro.info", "company": { "name": "Romaguera-Jacobson", "catchPhrase": "Face to face bifurcated interface", "bs": "e-enable strategic applications" } }, { "id": 4, "name": "Patricia Lebsack", "username": "Karianne", "email": "Julianne.OConner@kory.org", "address": { "street": "Hoeger Mall", "suite": "Apt. 692", "city": "South Elvis", "zipcode": "53919-4257", "geo": { "lat": "29.4572", "lng": "-164.2990" } }, "phone": "493-170-9623 x156", "website": "kale.biz", "company": { "name": "Robel-Corkery", "catchPhrase": "Multi-tiered zero tolerance productivity", "bs": "transition cutting-edge web services" } }, { "id": 5, "name": "Chelsey Dietrich", "username": "Kamren", "email": "Lucio_Hettinger@annie.ca", "address": { "street": "Skiles Walks", "suite": "Suite 351", "city": "Roscoeview", "zipcode": "33263", "geo": { "lat": "-31.8129", "lng": "62.5342" } }, "phone": "(254)954-1289", "website": "demarco.info", "company": { "name": "Keebler LLC", "catchPhrase": "User-centric fault-tolerant solution", "bs": "revolutionize end-to-end systems" } }, { "id": 6, "name": "Mrs. Dennis Schulist", "username": "Leopoldo_Corkery", "email": "Karley_Dach@jasper.info", "address": { "street": "Norberto Crossing", "suite": "Apt. 950", "city": "South Christy", "zipcode": "23505-1337", "geo": { "lat": "-71.4197", "lng": "71.7478" } }, "phone": "1-477-935-8478 x6430", "website": "ola.org", "company": { "name": "Considine-Lockman", "catchPhrase": "Synchronised bottom-line interface", "bs": "e-enable innovative applications" } }, { "id": 7, "name": "Kurtis Weissnat", "username": "Elwyn.Skiles", "email": "Telly.Hoeger@billy.biz", "address": { "street": "Rex Trail", "suite": "Suite 280", "city": "Howemouth", "zipcode": "58804-1099", "geo": { "lat": "24.8918", "lng": "21.8984" } }, "phone": "210.067.6132", "website": "elvis.io", "company": { "name": "Johns Group", "catchPhrase": "Configurable multimedia task-force", "bs": "generate enterprise e-tailers" } }, { "id": 8, "name": "Nicholas Runolfsdottir V", "username": "Maxime_Nienow", "email": "Sherwood@rosamond.me", "address": { "street": "Ellsworth Summit", "suite": "Suite 729", "city": "Aliyaview", "zipcode": "45169", "geo": { "lat": "-14.3990", "lng": "-120.7677" } }, "phone": "586.493.6943 x140", "website": "jacynthe.com", "company": { "name": "Abernathy Group", "catchPhrase": "Implemented secondary concept", "bs": "e-enable extensible e-tailers" } }, { "id": 9, "name": "Glenna Reichert", "username": "Delphine", "email": "Chaim_McDermott@dana.io", "address": { "street": "Dayna Park", "suite": "Suite 449", "city": "Bartholomebury", "zipcode": "76495-3109", "geo": { "lat": "24.6463", "lng": "-168.8889" } }, "phone": "(775)976-6794 x41206", "website": "conrad.com", "company": { "name": "Yost and Sons", "catchPhrase": "Switchable contextually-based project", "bs": "aggregate real-time technologies" } }, { "id": 10, "name": "Clementina DuBuque", "username": "Moriah.Stanton", "email": "Rey.Padberg@karina.biz", "address": { "street": "Kattie Turnpike", "suite": "Suite 198", "city": "Lebsackbury", "zipcode": "31428-2261", "geo": { "lat": "-38.2386", "lng": "57.2232" } }, "phone": "024-648-3804", "website": "ambrose.net", "company": { "name": "Hoeger LLC", "catchPhrase": "Centralized empowering task-force", "bs": "target end-to-end models" } } ]
https://jsonplaceholder.typicode.com/users/5
{ "id": 5, "name": "Chelsey Dietrich", "username": "Kamren", "email": "Lucio_Hettinger@annie.ca", "address": { "street": "Skiles Walks", "suite": "Suite 351", "city": "Roscoeview", "zipcode": "33263", "geo": { "lat": "-31.8129", "lng": "62.5342" } }, "phone": "(254)954-1289", "website": "demarco.info", "company": { "name": "Keebler LLC", "catchPhrase": "User-centric fault-tolerant solution", "bs": "revolutionize end-to-end systems" } }
Как всегда, создаем директорию проекта, размещаем в ней файлы Dockerfile
, entrypoint.sh
и action.yml
:
# Base Docker image FROM alpine:latest # installes required packages for our script RUN apk add --no-cache \ bash \ ca-certificates \ curl \ jq # copy bash script to filesystem alpine OS COPY entrypoint.sh /entrypoint.sh # change permission to execute our script RUN chmod +x /entrypoint.sh # execute bash script when container start ENTRYPOINT ["/entrypoint.sh"]
#!/bin/bash
set -e
api_url="https://jsonplaceholder.typicode.com/users/${INPUT_USER_ID}"
echo $api_url
user_name=$(curl "${api_url}" | jq ".name")
echo $user_name
echo "::set-output name=user_name::$user_name"
name: Custom Github Docker Action description: Call API and get user name inputs: user_id: description: User ID, from 1 to 10 required: true default: 1 outputs: user_name: description: Getted user name runs: using: docker image: Dockerfile args: - ${{ inputs.user_id }}
Чтобы проверить наш новый action
— создаем workflow-файл, который использует этот action
в работе:
name: Test custom docker action on: [push] jobs: get_user_name_job: runs-on: ubuntu-latest # задаем значение output для этого задания outputs: user_name: ${{ steps.get_user_name_step.outputs.user_name }} steps: - name: Get user name id: get_user_name_step uses: tokmakov/custom-docker-action@master with: user_id: 5 - name: Echo user name # получаем доступ к output предыдущего шага run: echo ${{ steps.get_user_name_step.outputs.user_name }} use_user_name_job: runs-on: ubuntu-latest needs: get_user_name_job steps: - name: Echo user name # получаем доступ к output предыдущего задания run: echo ${{needs.get_user_name_job.outputs.user_name}}
Создаем репозиторий на GitHub, выкладываем файлы проекта и смотрим результат работы нового action
:
2. Action на JavaScript
Создаем директорию проекта, размещаем в ней файлы index.js
и action.yml
:
const core = require('@actions/core') const fetch = require('node-fetch') const fetchUser = async (id) => { const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`) const data = await response.json() return data } try { const id = core.getInput('user_id') // входные данные fetchUser(id).then(user => { console.log(user.name) core.setOutput('user_name', user.name) // выходные данные }) } catch(error) { core.setFailed(error.message) }
name: Custom Github JavaScript Action description: Call API and get user name inputs: user_id: description: User ID, from 1 to 10 required: true default: 1 outputs: user_name: description: Getted user name runs: using: node16 main: index.js
Инициализируем проект, устанавливаем пакеты @actions/core
и node-fetch
(2-ой версии, чтобы использовать require
, а не import
):
$ npm init -y $ npm install @actions/core $ npm install node-fetch@2
Чтобы проверить наш новый action
— создаем workflow-файл, который использует этот action
в работе:
name: Test custom javascript action on: [push] jobs: get_user_name_job: runs-on: ubuntu-latest # задаем значение output для этого задания outputs: user_name: ${{ steps.get_user_name_step.outputs.user_name }} steps: - name: Get user name id: get_user_name_step uses: tokmakov/custom-javascript-action@master with: user_id: 5 - name: Echo user name # получаем доступ к output предыдущего шага run: echo ${{ steps.get_user_name_step.outputs.user_name }} use_user_name_job: runs-on: ubuntu-latest needs: get_user_name_job steps: - name: Echo user name # получаем доступ к output предыдущего задания run: echo ${{needs.get_user_name_job.outputs.user_name}}
Создаем новый репозиторий на GitHub, выкладываем все файлы (включая node_modules
) в этот репозиторий:
$ git init # создаем пустой репозиторий $ git add --all # добавляем все файлы проекта $ git commit -m "Initial сommit" # первый коммит
$ git remote add origin git@github.com:tokmakov/custom-javascript-action.git $ git push origin master # выкладываем проект на GitHub $ git branch -u origin/master # отслеживаем удаленную ветку
Не очень хорошо, что мы выкладываем на GitHub директорию node_modules
. Этого можно избежать, если использовать пакет @vercel/ncc
. Он позволяет упаковать наш код вместе с зависимостями в один файл.
$ npm i -g @vercel/ncc # установка пакета ncc $ ncc build index.js # будет создан dist/index.js
Теперь в action.yml
изменяем значение директивы main
на dist/index.js
.
$ rm -rf node_modules/* $ git add action.yml dist/index.js node_modules/* $ git commit -m "Use vercel/ncc" $ git push
Кроме пакета @actions/core
можно еще использовать @actions/github
для взаимодействия с GitHub, но об этом как-нибудь в другой раз.
Еще два action
Мы создали два action
, каждый в отдельном репозитории, подразумевая их многократное использование или даже публикацию на Marketplace. Теперь создадим два action
, с использованием Docker образа и javascript-кода, которые будут размещаться локально. Эти два action
будут отправлять сообщение в Telegram по результатам работы задания — success
, failure
, skipped
или cancelled
.
1. Action как Docker-образ
Workflow файл .github/workflows/test-docker-action.yml
:
name: Test send telegram message on: [push] jobs: some-job: runs-on: ubuntu-latest steps: - run: | echo "do something" sleep 5 echo "job complete" exit $(( $RANDOM % 2 )) report: needs: some-job if: ${{ always() }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: ./.github/actions/telegram-message with: chat: ${{ secrets.CHAT }} token: ${{ secrets.TOKEN }} result: ${{ needs.some-job.result }}
Action файл .github/actions/telegram-message/action.yml
:
name: Send telegram message description: Send message to telegram (Docker image) inputs: chat: description: Telegram chat ID required: true token: description: Telegram API token required: true result: description: Result (success, failure, skipped, failure) required: true default: success runs: using: docker image: Dockerfile args: - ${{ inputs.chat }} - ${{ inputs.token }}
Файлы .github/actions/telegram-message/Dockerfile
и .github/actions/telegram-message/entrypoint.sh
:
# Base Docker image FROM alpine:latest # installes required packages for our script RUN apk add --no-cache \ bash \ ca-certificates \ curl # copy bash script to filesystem alpine OS COPY entrypoint.sh /entrypoint.sh # change permission to execute our script RUN chmod +x /entrypoint.sh # execute bash script when container start ENTRYPOINT ["/entrypoint.sh"]
#!/bin/bash
set -e
api_url="https://api.telegram.org/bot${INPUT_TOKEN}/sendMessage"
header='Content-Type: application/json; charset=utf-8'
message='Задание завершилось успешно'
[[ $INPUT_RESULT == 'failure' ]] && message='Задание завершилось ошибкой'
[[ $INPUT_RESULT == 'skipped' ]] && message='Задание было пропущено'
[[ $INPUT_RESULT == 'cancelled' ]] && message='Задание было отменено'
json='{\"chat_id\":\"%s\",\"text\":\"%s\"}'
printf -v data "$json" "$INPUT_CHAT" "$message"
curl -X POST -H "$header" -d "$data" $api_url > /dev/null 2>&1
2. Action на JavaScript
Workflow файл .github/workflows/test-local-action.yml
:
name: Test send telegram message on: [push] jobs: some-job: runs-on: ubuntu-latest steps: - run: | echo "do something" sleep 5 echo "job complete" exit $(( $RANDOM % 2 )) report: needs: some-job if: ${{ always() }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: ./.github/actions/telegram-message with: chat: ${{ secrets.CHAT }} token: ${{ secrets.TOKEN }} result: ${{ needs.some-job.result }}
Action файл .github/actions/telegram-message/action.yml
:
name: Send telegram message description: Send message to telegram (JavaScript code) inputs: chat: description: Telegram chat ID required: true token: description: Telegram API token required: true result: description: Result (success, failure, skipped, cancelled) required: true default: success runs: using: node16 main: dist/index.js
Файлы .github/actions/telegram-message/index.js
и .github/actions/telegram-message/package.json
:
const core = require('@actions/core') const fetch = require('node-fetch') try { const chat = core.getInput('chat') const token = core.getInput('token') const result = core.getInput('result') let message = 'Задание завершилось успешно' if (result == 'failure') message = 'Задание завершилось ошибкой' if (result == 'skipped') message = 'Задание было пропущено' if (result == 'cancelled') message = 'Задание было отменено' fetch(`https://api.telegram.org/bot${token}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=utf-8' }, body: JSON.stringify({ chat_id: chat, text: message }) }) } catch(error) { core.setFailed(error.message) }
{ "name": "telegram-javascript-action", "version": "1.0.0", "description": "", "main": "index.js", "keywords": [], "author": "", "license": "ISC", "dependencies": { "@actions/core": "^1.9.0", "node-fetch": "^2.6.7" } }
Здесь все сделано по аналогии с первым javascript action — переходим в директорию .github/actions/telegram-message
, устанавливаем пакеты @actions/core
и node-fetch
, потом запускаем ncc build index.js
. Удаляем директорию node_modules
, переходим на три уровня выше cd ../../..
, создаем репозиторий git init
, добавляем в него все файлы, выкладываем на GitHub.
Исходные коды примеров здесь.
Поиск: Git • GitHub • Linux • Web-разработка • Конфигурация • Сервер