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

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

GitHub Actions — бесплатная для публичных репозиториев система
непрерывной интеграции. Позволяет запустить проверку кода линтером и
тестами, выполнить деплой проекта, опубликовать новую версию пакета,
отправлять оповещения в мессенджер о событиях в репозитории и многое
другое. Для начала создадим репозиторий и разместим в нем файлы проекта — хотя бы README.md. Общий принцип работы Github Actions такой — в репозитории создается директория .github/workflows, внутри которой размещаются yml-файлы, с описанием шагов, которые нужно выполнить на различные события. Давайте в репозитории перейдем на вкладку Actions — там есть много образцов yml-файлов под разные задачи, которые можно использовать. Но мы создадим yml-файл сами, для этого переходим по ссылке «Set up a workflow yourself» — будет создана директория .github/workflows и открыт на редактирование файл main.yml. # имя, которое будет показано в интерфейсе github.com
name: learn-github-actions
# список событий, при которых будут зап
Оглавление

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

Workflow файл

Общий принцип работы Github Actions такой — в репозитории создается директория .github/workflows, внутри которой размещаются yml-файлы, с описанием шагов, которые нужно выполнить на различные события. Давайте в репозитории перейдем на вкладку Actions — там есть много образцов yml-файлов под разные задачи, которые можно использовать. Но мы создадим yml-файл сами, для этого переходим по ссылке «Set up a workflow yourself» — будет создана директория .github/workflows и открыт на редактирование файл main.yml.

# имя, которое будет показано в интерфейсе github.com
name: learn-github-actions

# список событий, при которых будут запускаться задания
on:
# на push и pull_request, только для ветки master
push:
branches: [master]
pull_request:
branches: [master]

# позволяет запускать workflow вручную с вкладки actions в интерфейсе github.com
workflow_dispatch:

# одно или несколько заданий, которые могут быть запущены параллельно или последовательно
jobs:
# у этого workflow всего одна задача single
single:
# задание будет выполняться на последней версии Ubuntu
runs-on: ubuntu-latest

# шаги задания запускаются последовательно
steps:
# при выполнении задания будет доступен наш репозиторий
- uses: actions/checkout@v3

# запуск одной shell-команды
- name: Run step one
run: echo "Single job, step one, single command"

# запуск двух shell-команд
- name: Run step two
run: |
echo "Single job, step two, command one"
echo "Single job, step two, command two"

Workflow имеет имя name, условия для запуска on, одно или несколько заданий jobs. У каждого задания есть имя, каждое задание содержит один или несколько шагов steps.
По умолчанию все задания выполняются параллельно, однако между ними
можно определить зависимость, чтобы они выполнялись последовательно.
Каждое задание выполняется на определённом раннере runs-on — временном сервере на GitHub с выбранной операционной системой (Linux, MacOS или Windows).

Доступны следующие операционные системы:

  • Windows Server 2019 — для этого нужно использовать windows-2019
  • Windows Server 2016 — для этого нужно использовать windows-2016
  • Ubuntu 20.04 — для этого нужно использовать ubuntu-20.04
  • Ubuntu 18.04 — для этого нужно использовать ubuntu-18.04
  • MacOS Big Sur 11 — для этого нужно использовать macos-11
  • MacOS Catalina 10.15 — для этого нужно использовать macos-10.15

Понятно, что внутри сервера, когда запускается workflow, нам нужны файлы нашего репозитория. Для этого предназначена директива uses, где указывается готовый action, который предлагает GitHub на Marketplace. По сути, директива выполняет команду git fetch,
чтобы внутри Ubuntu получить файлы нашего репозитория в состоянии
последнего коммита (той ветки, на которой произошло событие, запустившее
workflow).

Чтобы получить внутри Ubuntu всю историю репозитория для всех существующих веток

steps:
# при выполнении задания будет доступен наш репозиторий
- uses: actions/checkout@v3
with:
# количество последних коммитов при выполнении fetch, значение ноль
# означает все коммиты, по умолчанию — 1 (только последний коммит)
fetch-depth: 0

Чтобы получить внутри Ubuntu другую ветку вместо той, которая вызвала этот workflow

steps:
# при выполнении задания будет доступен наш репозиторий
- uses: actions/checkout@v3
with:
# получаем другую ветку вместо той, которая вызвала этот workflow
ref: other-branch

Переменные среды

В yml-файле можно использовать переменные среды, которые будут доступны внутри Ubuntu — некоторые из них предоставляет GitHub, а остальные можно создать самостоятельно. У пользовательских переменных среды разные области видимости — переменные уровня step видны только на этом шаге, переменные уровня job видны только для этого задания. То есть, область действия пользовательской переменной среды ограничена элементом, в котором она определена.

name: learn-github-actions

on:
push:
branches: [master]
pull_request:
branches: [master]

workflow_dispatch:

# эта переменная на верхнем уровне (доступна везде)
env:
WORKFLOW_NAME: Learn github actions

jobs:
single:
runs-on: ubuntu-latest
# это переменная уровня задания single
env:
job_name: single job

steps:
- uses: actions/checkout@v3

- name: Run step one
run: echo "Single job, step one, single command"

- name: Run step two
# это переменные уровня этого шага
env:
step_name: step two
command1: command one
command2: command two
run: |
# можем использовать переменные верхнего уровня, переменные уровня задания single и уровня этого шага
echo "$WORKFLOW_NAME: $job_name, $step_name, $command1"
echo "$WORKFLOW_NAME: $job_name, $step_name, $command2"

При запуске очередного шага, например Run step two — запускается новый экземпляр оболочки bash внутри ОС Ubuntu, которой будут доступны переменные WORKFLOW_NAME, job_name, step_name, command1 и command2.

-2

Некоторые переменные среды, которые предоставляет GitHub (подробнее здесь):

  • GITHUB_WORKSPACE — путь к директории с файлами проекта
  • GITHUB_WORKFLOW — имя workflow, который сейчас работает
  • GITHUB_REPOSITORY — имя владельца репозитория + имя самого репозитория
  • GITHUB_EVENT_NAME — имя события, которое запустило этот workflow
  • GITHUB_JOB — идентификатор задания, которое сейчас выполняется
  • GITHUB_REF — ветка (тег), где произошло событие, запустившее workflow

run: |
echo "GITHUB_WORKSPACE = $GITHUB_WORKSPACE"
echo "GITHUB_WORKFLOW = $GITHUB_WORKFLOW"
echo "GITHUB_REPOSITORY = $GITHUB_REPOSITORY"
echo "GITHUB_EVENT_NAME = $GITHUB_EVENT_NAME"
echo "GITHUB_JOB = $GITHUB_JOB"
echo "GITHUB_REF = $GITHUB_REF"

GITHUB_WORKSPACE = /home/runner/work/learn-github-actions/learn-github-actions
GITHUB_WORKFLOW = learn-github-actions
GITHUB_REPOSITORY = tokmakov/learn-github-actions
GITHUB_EVENT_NAME = push
GITHUB_JOB = single
GITHUB_REF = refs/heads/master

Контекст workflow

Переменные среды доступны только внутри Ubuntu, когда идет выполнение очередного step. Контексты — это наборы переменных, которые доступны вне run директив. Можно думать о них как о переменных, которые могут быть встроены в сам yml-файл рабочего процесса.

name: learn-github-actions
on: push
jobs:
single:
runs-on: ubuntu-latest
steps:
- name: Echo GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo $GITHUB_CONTEXT

{
"token": "***",
"job": "single",
"ref": "refs/heads/master",
"sha": "3b612aebbc7a8ba9a461da6facf81e7e0cbe0b5e",
"repository": "tokmakov/learn-github-actions",
"repository_owner": "tokmakov",
"repository_owner_id": "8893826",
"repositoryUrl": "git://github.com/tokmakov/learn-github-actions.git",
"run_id": "2508294682",
"run_number": "15",
"retention_days": "90",
"run_attempt": "1",
"artifact_cache_size_limit": "10",
"repository_id": "503696545",
"actor_id": "8893826",
"actor": "tokmakov",
"workflow": "learn-github-actions",
"head_ref": "",
"base_ref": "",
"event_name": "push",
..........
}

Теперь, когда мы знаем все ключи объекта github — можем
использовать их более осмысленно. Например, запускать то или иное
задание в зависимости от выполнения того или иного условия.

name: learn-github-actions
on: push
jobs:
prod-deploy:
if: ${{ github.ref == 'refs/heads/master' }}
runs-on: ubuntu-latest
steps:
- run: echo "Deploying to prod server on branch $GITHUB_REF"

test-deploy:
if: ${{ github.ref == 'refs/heads/test' }}
runs-on: ubuntu-latest
steps:
- run: echo "Deploying to test server on branch $GITHUB_REF"

Кроме объекта контекста github, доступны объекты env, job, steps, runner, secrets, strategy, matrix, needs и inputs.

Особености контекста

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

  • Переменные среды по умолчанию существуют только внутри операционной системы, которая выполняет задание workflow
  • Контекст можно использовать в любой момент workflow, в том числе, когда переменные среды по умолчанию недоступны

С помощью контекста можно выполнить проверку еще до того, как задание
будет отправлено на выполнение. И, по результату это проверки —
пропустить некоторые шаги.

name: learn-github-actions
on: [push, pull_request]
jobs:
single:
runs-on: ubuntu-latest

steps:
- name: Step one
if: ${{ github.event_name == 'push' }}
run: echo "Run only on push event"

- name: Step two
if: ${{ github.event_name == 'pull_request' }}
run: echo "Run only on pull_request event"

-3

Задания по порядку

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

jobs:
job-one:
runs-on: ubuntu-latest
steps:
- run: echo "Run job-one"
job-two:
runs-on: ubuntu-latest
needs: job-one
steps:
- run: echo "Run job-two (after job-one)"
job-three:
runs-on: ubuntu-latest
needs: [job-one, job-two]
steps:
- run: echo "Run job-three (after job-one and job-two)"

В этом примере задание job-one должно успешно завершиться до начала job-two, а job-three ожидает успешного завершения job-one и job-two.

Если задание job-two зависит от задания job-one, то когда result задания job-one будет skipped — то задание job-two тоже будет пропущено (см. ниже, что такое результат job).

Директива matrix

Директива matrix позволяет использовать переменные (например version), чтобы запускать одно задание несколько раз с разными значениями этой переменной. Например можем установить несколько версий Node.js и проверить работу приложения на каждой версии.

jobs:
example:
strategy:
matrix:
version: [12, 14, 16]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.version }}

Или, проверить работу приложения на разных версиях Node.js и на разных версиях Ubuntu:

jobs:
example:
strategy:
matrix:
os: [ubuntu-18.04, ubuntu-20.04]
version: [12, 14, 16]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.version }}

Step outputs

Каждый шаг задания может иметь какое-то выходное значение (или несколько
значений). Каждый шаг задания имеет доступ к выходным значениям всех
предыдущих шагов.

jobs:
example:
runs-on: ubuntu-latest
steps:
- name: Step one
id: step-one
# задаем имя и значение output для этого шага
run: echo "::set-output name=step-one-output::hello from step one"

- name: Step two
id: step-two
# 1. задаем имя и значение output для этого шага
# 2. получаем доступ к значению output первого шага
run: |
echo "::set-output name=step-two-output::hello from step two"
echo "Step one output: ${{ steps.step-one.outputs.step-one-output }}"

- name: Step three
id: step-three
# 1. задаем имя и значение output для этого шага
# 2. получаем доступ к значению output первого шага
# 3. получаем доступ к значению output второго шага
run: |
echo "::set-output name=step-three-output::hello from step three"
echo "Step one output: ${{ steps.step-one.outputs.step-one-output }}"
echo "Step two output: ${{ steps.step-two.outputs.step-two-output }}"

-4

Job outputs

У заданий тоже могут быть выходные значения и их можно сделать доступными для зависимых заданий:

jobs:
job-one:
runs-on: ubuntu-latest
# задаем имена и значения для выходных значений этого задания
outputs:
output-one: ${{ steps.step-one.outputs.step-one-output }}
output-two: ${{ steps.step-two.outputs.step-two-output }}
steps:
- id: step-one
run: echo "::set-output name=step-one-output::hello from step one"
- id: step-two
run: echo "::set-output name=step-two-output::hello from step two"
job-two:
runs-on: ubuntu-latest
needs: job-one
steps:
# получаем доступ к выходным значениям задания job-one
- run: echo "${{needs.job-one.outputs.output-one}}, ${{needs.job-one.outputs.output-two}}"

Условие для step

Для шагов можно задавать условия выполнения с помощью функций failure(), always(), cancelled() и success().
Последнюю функцию применять особого смысла нет, потому что она
применяется неявно — следующий шаг запускается, если все предыдущие были
успешны.

jobs:
job1:
runs-on: ubuntu-latest
steps:
- name: Step one
id: step-one
# steps.step-one.conclusion = skipped
if: ${{ false }}
run: echo "run step one"

- name: Step two
id: step-two
run: |
echo "run step two"
echo "step one conclusion = ${{ steps.step-one.conclusion }}"

job2:
runs-on: ubuntu-latest
steps:
- name: Step one
id: step-one
# steps.step-one.conclusion = failure
run: |
echo "run step one"
exit 1

- name: Step two
id: step-two
# этот шаг будем запускать всегда
if: ${{ always() }}
run: |
echo "run step two"
echo "step one conclusion = ${{ steps.step-one.conclusion }}"

job3:
runs-on: ubuntu-latest
steps:
- name: Step one
id: step-one
# steps.step-one.conclusion = failure
run: |
echo "run step one"
exit 1

- name: Step two
id: step-two
# при неудаче любого шага этого job
if: ${{ failure() }}
run: |
echo "run step two"
echo "step one conclusion = ${{ steps.step-one.conclusion }}"

job4:
runs-on: ubuntu-latest
steps:
- name: Step one
id: step-one
# 30 секунд на отмену workflow
run: |
echo "run step one"
sleep 30

- name: Step two
id: step-two
# при отмене рабочего процесса
if: ${{ cancelled() }}
run: |
echo "run step two"
echo "workflow cancelled"

-5
-6
-7
-8

Условие для job

Для заданий тоже можно задавать условия запуска, но только для тех
заданий, которые зависят от других. При использовании директивы needs задание запускается только в том случае, если все задания, от которых оно зависит, завершились успешно (функция success() применяется неявно).

jobs:
job-one:
runs-on: ubuntu-latest
steps:
- run: exit 1

job-two:
needs: job-one
runs-on: ubuntu-latest
steps:
- run: echo "Function success()"

job-three:
needs: [job-one, job-two]
if: ${{ failure() }}
runs-on: ubuntu-latest
steps:
- run: echo "Function failure()"

job-four:
needs: [job-one, job-two, job-three]
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- run: |
echo "Function always()"
echo "Result job-one: ${{ needs.job-one.result }}"
echo "Result job-two: ${{ needs.job-two.result }}"
echo "Result job-three: ${{ needs.job-three.result }}"

Задание job-two не будет запущено, потому что зависит от успешного выполнения задания job-one. Задание job-three будет запущено, потому что задание job-one завершилось неудачей. Задание job-four будет запущено при любом результате job-one, job-two, job-three.

-9

Результат step

У каждого шага есть результат (статус), с которым он завершился — это success, failure, skipped или cancelled. Каждый шаг может получить результат (статус) каждого предыдущего шага через outcome.

jobs:
example:
runs-on: ubuntu-latest
steps:
- id: step-one
run: echo "run step one"

- id: step-two
run: |
echo " run step two"
exit 1

- id: step-three
run: |
echo "run step three"
echo "function success()"

- id: step-four
if: ${{ failure() }}
run: |
echo "run step four"
echo "function failure()"

- id: step-five
if: ${{ always() }}
run: |
echo "run step five"
echo "function always()"
echo "outcome step one: ${{ steps.step-one.outcome }}"
echo "outcome step two: ${{ steps.step-two.outcome }}"
echo "outcome step three: ${{ steps.step-three.outcome }}"
echo "outcome step four: ${{ steps.step-four.outcome }}"

-10

Кроме outcome есть еще conclusion — значение совпадает с outcome, если не используется директива continue-on-error. Использование этой директивы изменяет значение conclusion на success, когда outcome принимает значение failure.

jobs:
example:
runs-on: ubuntu-latest
steps:
- id: step-one
run: echo "run step one"

- id: step-two
continue-on-error: true
run: |
echo " run step two"
exit 1

- id: step-three
run: |
echo "run step three"
echo "outcome step one: ${{ steps.step-one.outcome }}"
echo "outcome step two: ${{ steps.step-two.outcome }}"
echo "conclusion step one: ${{ steps.step-one.conclusion }}"
echo "conclusion step two: ${{ steps.step-two.conclusion }}"

-11

Хотя шаг step-two провалился — шаг step-three был запущен и все задание example считается успешным.

Результат job

У задания тоже может быть результат (статус) — success, failure, skipped, cancelled — и этот результат можно получить в зависимом задании.

jobs:
job-one:
runs-on: ubuntu-latest
steps:
- run: echo "run job-one"

job-two:
needs: job-one
runs-on: ubuntu-latest
steps:
- run: |
echo "run job-two"
exit 1

job-three:
needs: [job-one, job-two]
if: ${{ false }}
runs-on: ubuntu-latest
steps:
- run: echo "run job-three"

job-four:
needs: [job-one, job-two, job-three]
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- run: |
echo "run job-four"
echo "result job-one: ${{ needs.job-one.result }}"
echo "result job-two: ${{ needs.job-two.result }}"
echo "result job-three: ${{ needs.job-three.result }}"

run job-four
result job-one: success
result job-two: failure
result job-three: skipped

Для заданий тоже можно использовать директиву continue-on-error — например, если добавить ее для job-two, то задание будет считаться успешным. А поскольку все задания workflow были success или skipped — то и workflow в целом будет считаться успешным (отмечен в интерфейсе GitHub зеленым кружком с галочкой).

run job-four
result job-one: success
result job-two: success
result job-three: skipped

Временные файлы

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

steps:
- name: Set the value
id: step_one
run: |
echo "action_state=yellow" >> $GITHUB_ENV
- name: Use the value
id: step_two
run: |
echo "${{ env.action_state }}" # This will output 'yellow'

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

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