Найти в Дзене
Linux | Network | DevOps

GitHub Actions: быстрый старт CI/CD Часть 2

Хорошо, общее представление о GitHub Actions у нас есть, теперь хотелось
бы применить эти знания на практике. Давайте создадим небольшой проект,
выложим его на GitHub, создадим для него workflow-файл, который будет
запускать проверку кода линтером, выполнять сборку и деплоить на
production сервер. Итак, в директории проекта есть файл 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
Оглавление

Хорошо, общее представление о 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

Внимательный читатель возможно уже заметил, что работа идет под Windows 10. Тогда откуда у меня утилита 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.

Исходные коды примеров здесь.

Обсудить эту статью можно в Телеграм канале: https://t.me/linautonet