GitHub Actions: використання Reusable Workflows – нюанси роботи

Автор |  13/03/2024
 

В пості GitHub Actions: деплой Dev/Prod оточень з Terraform я вже трохи торкався теми Reusable Workflows та Composite Actions – прийшов час трохи більше з нею ознайомитись.

Що треба зробити: зараз на проекті ми в кожному репозиторії пишемо Workflow-файли окремо. Втім, оскільки поступово всі процеси уніфікуються – управління інфраструктурою через Terraform, та запуск сервісів в Kubernetes і деплой з Helm – то вирішили, що пора навести лад в GitHub Actions, і перестати писати “кожен для себе”.

Натомість в окремому репозиторії створимо Shared Workflow з набором Jobs, які будуть виконувати потрібні дії, і потім будемо ці Workflow включати в Wokflow проектів.

Але у Reusable Workflows виявилось кілька цікавих деталей.

Тож спочатку глянемо в чому різниця між Reusable Workflows та Composite Actions та для чого вони призначаються., а потім поглянемо на роботу з Reusable Workflows.

Порівняння Reusable Workflows та Composite Actions

Composite Actions

Composite Actions дозволяють скомбінувати кілька Steps в єдиний Action. Такі Step описуються в єдиному файлі, і можуть виконувати кілька різних runs або викликати інші Actions.

Гарний приклад роботи з ними є в тому GitHub Actions: деплой Dev/Prod оточень з TerraformСтворення Composite Action “terraform-init”.

Ідеальне рішення, коли ви хочете використати послідовність Steps в кількох Jobs або Workflows.

  • Composite Actions дозволяє комбінувати кілька steps в одному Action, щоб потім у Workflow викликати їх всі як один Step
  • в Composite Actions не можна мати кілька Jobs
  • Job, яка викликає Composite Actions може мати інші Steps

Reusable Workflows

Reusable Workflows дозволяють перевикористати цілий Workflow з усіма його Jobs та Steps. Дають більше можливостей, бо включають в себе контексти, змінні оточення та секрети.

Ідеальне рішення, коли ви хочете використати цілий CI/CD пайплайн в кількох репозиторіях.

Далі будемо використовувати такі назви:

  • Reusable Workflow: workflow, який зберігається в окремому репозиторії та викликається для виконання іншим workflow
  • Caller Workflow: workflow, який викликає Reusable Workflow

Особливості Reusable Workflows:

  • Reusable Workflows не можуть викликати інші Reusable Workflows
  • Reusable Workflows мають досить детальні логи виконання – кожна Job та Step логується окремо
  • Reusable Workflows викликаються як Jobs, але така Job не може мати інших Steps
    • через це ви не можете використати $GITHUB_ENV, щоб передати values до Jobs та Steps у Caller Workflow, який викликає Reusable Workflow
  • ви можете використовувати різні версії одного Reusable Workflow через анотацію @REF з іменем бранча або git-тегом

Див. також Limitations.

Reusable Workflows та Composite Actions: Key differences

Reusable workflows Composite actions
Can connect a maximum of four levels of workflows Can be nested to have up to 10 composite actions in one workflow
Can use secrets Cannot use secrets
Can use if: conditionals Cannot use if: conditionals
Can be stored as normal YAML files in your project Requires individual folders for each composite action
Can use multiple jobs Cannot use multiple jobs
Each step is logged in real-time Logged as one step even if it contains multiple steps

Створення Reusable Workflow

Зробимо тестові Workflow, щоб перевірити схему взагалі:

  • в репозиторії atlas-github-actions буде Reusable Workflow
  • в репозиторії atlas-test буде Caller Workflow

Створюємо репозиторій для наших Reusable Workflows – atlas-github-actions, і в ньому створюємо каталог .github/workflows з файлом test-reusable-workflow.yml:

name: Reusable Workflow

# trigger from other workflows
on:
  workflow_call:

jobs: 

  test:
    runs-on: ubuntu-latest

    steps: 
      - name: "Test: print Hello"
        run: echo "Hello, World!"

Зберігаємо, пушимо в GitHub.

Далі нам потрібно дозволити використання Workflows з цього репозиторію.

Переходимо в Setting > Actions, і внизу сторінки дозволяємо доступ з інших репозиторіїв організації:

Переходимо до Caller-репозиторія – atlas-test, також створюємо каталог .github/workflows з файлом test-caller-workflow.yml:

name: Caller Workflow

on: 
  # can be ran manually
  workflow_dispatch:

jobs:

  test:
    # call the Reusable Workflow file
    uses: <ORG_NAME>/atlas-github-actions/.github/workflows/test-reusable-workflow.yml@master

Пушимо, і запускаємо:

Тепер трохи подивимось на деталі того, як працювати з Reusable Workflows.

Permissions

Чудовий пост на тему GitHub Actions Permisssions і Security взагалі – GitHub Actions Workflow Permissions.

Якщо в двох словах:

  • при використанні Actions сторонніх девелоперів – перевіряйте їх код, та використовуйте SHA hash замість Git-тегу (ніколи так не робив, але для зовсім Security – має сенс)
  • завжди налаштовуйте permissions для $GITHUB_TOKEN явно на рівні Workflow або Job, щоб не використовувати дефолтні дозволи
  • Reusable Workflow наслідує permissions з Job або Workflow, яка викликає Reusable Workflow

Тобто якщо ми в Caller Workflow задамо permissions.pull-request: write – то зможемо створювати коментарі в Pull Requests і з нашого Reusable Workflow.

GitHub Actions envs, vars, secrets та Reusable Workflow

У нас є три типи данних, але з різними “рівнями”:

  • env context:
    • задається на рівні Workflow/Job/Step – в Reusable Workflow не передаються
  • vars context:
    • задається або на рівні GitHub Actions Environments – в Reusable Workflow не передаються
    • або на рівні Repository та Organization Variables – в Reusable Workflow доступні без додаткових дій
  • secrets context:
    • задається або на рівні GitHub Actions Environments – в Reusable Workflow не передаються
    • або на рівні Repository та Organization Secrets – в Reusable Workflow доступні через secrets: inherit

Ми взагалі не можемо використовувати Environments в Caller Workflow та Job, яка викликає Reusable Workflow – див. Supported keywords for jobs that call a reusable workflow, тож всі vars та secrets, які задані конкретному Evnironment – ми в Reusable Workflow не побачимо.

Тобто в Caller Workflow не можна зробити щось типу:

...
jobs:

  test:
    # using 'environment' will fail
    environment: test
    uses: <ORG_NAME>/atlas-github-actions/.github/workflows/test-reusable-workflow.yml@master
...

Ну й давайте перевіримо що ми зможемо побачити в Caller Workflow, та в Reusable Workflow.

В репозиторії atlas-test з Caller Workflow додаємо Environment, і в ньому Environment secrets та Environment variables:

В тому ж репозиторії додаємо звичайні Repository secrets:

Та Repository variables:

В цьому ж репозиторії оновлюємо файл Caller Workflow – test-caller-workflow.yml:

  • на рівні Workflow додаємо env: CALLER_WORKFLOW_ENV
  • до Job з нашою Reusable Workflow:
    • додаємо передачу test-input в Reusable Workflow
    • додаємо передачу secrets: inherit
  • на рівні Workflow додаємо Job prints-envs
name: Caller Workflow

on: 
  # can be ran manually
  workflow_dispatch:

env:
  CALLER_WORKFLOW_ENV: "Caller Env String"

jobs:

  test:
    # call the Reusable Worfklow file
    uses: <ORG_NAME>/atlas-github-actions/.github/workflows/test-reusable-workflow.yml@master
    with: 
      test-input: "Test Input String"
    secrets: inherit
  

  prints-envs:
    
    environment: test
    runs-on: ubuntu-latest

    steps:

      # Can use Envs from the Workflow level
      - name: "Test: print Caller Workflow Env"
        run: echo ${{ env.CALLER_WORKFLOW_ENV }}

      # can use Variables from the Workflow Environments level
      - name: "Test: print Caller Repository Env Variable"
        run: echo ${{ vars.CALLER_ENV_VAR }}

      # can use Variables from the Reposiotiry level
      - name: "Test: print Caller Repository Repo Variable"
        run: echo ${{ vars.CALLER_REPO_VAR }}

      # CAN'T use Secrets from the Workflow Environments level
      - name: "Test: print Caller Env Secret"
        run: echo ${{ secrets.CALLER_ENV_SECRET }}

      # can use Secrets from the Reposiotiry level
      - name: "Test: print Caller Repo Secret"
        run: echo ${{ secrets.CALLER_REPO_SECRET }}

В репозиторії atlas-github-actions оновимо наш Reusable Workflow – файл test-reusable-workflow.yml.

Додаємо inputs та steps, в яких спробуємо вивести env, vars та secrets з Caller Workflow/Repository/Environment:

name: Reusable Workflow

# trigger from other workflows
on:
  workflow_call:
    inputs:
      test-input:
        required: true
        type: string      

jobs: 

  test:
    runs-on: ubuntu-latest

    steps: 
      - name: "Test: print Hello"
        run: echo "Hello, World!"

      # CAN'T use Envs from the Caller Workflow
      - name: "Test: print Caller Workflow Env"
        run: echo ${{ env.CALLER_WORKFLOW_ENV }}

      # CAN'T use Variables from the Caller Workflow Environments level
      - name: "Test: print Caller Repository Env Variable"
        run: echo ${{ vars.CALLER_ENV_VAR }}

      # can use Variables from the Caller Repository Variables
      - name: "Test: print Caller Repository Repo Variable"
        run: echo ${{ vars.CALLER_REPO_VAR }}

      # CAN'T use Secrets from the Caller Workflow Environments Secrets
      - name: "Test: print Caller Env Secret"
        run: echo ${{ secrets.CALLER_ENV_SECRET }}

      # can use Secrets from the Caller Reposiotiry
      - name: "Test: print Caller Repo Secret"
        run: echo ${{ secrets.CALLER_REPO_SECRET }}

      # can use Inputs from the Caller Workflow
      - name: "Test: print Caller Repo Input"
        run: echo ${{ inputs.test-input }}

Передача Secrets

Додам про передачу Secrets:

Перший варіант – використати secrtes: inherit – тоді в Reusable Workflow будуть доступні всі змінні в Repository secrets та Orgznization secrets з Caller Workflow.

Крім того, в Reusable Workflow можна їх задати в env:

...
on:
  workflow_call:
    inputs:
      test-input:
        required: true
        type: string  

env:
  reusable_wf_local_secret: ${{ secrets.CALLER_REPO_INHERITED_SECRET }}

jobs:
  
  test:
...

Другий варіант – замість використання secrets: inherit передавати конкретний Secret:

...
  test:
    # call the Reusable Worfklow file
    uses: <ORG_NAME>/atlas-github-actions/.github/workflows/test-reusable-workflow.yml@master
    with: 
      test-input: "Test Input String"
    secrets:
      REUSAVBLE_WF_SECRET_NAME: ${{ secrets.CALLER_WF_SECRET_NAME }}
...

В такому випадку в Reusable Workflow REUSAVBLE_WF_SECRET_NAME має бути заданий в разом з inputs:

...
on:
  workflow_call:
    secrets:
      REUSABLE_WF_SECRET_NAME:
        required: false
    inputs:
      test-input:
        required: true
        type: string 
...

Тоді далі в Reusable Workflow його можна використовувати як ${{ secrets.REUSABLE_WF_SECRET_NAME }}.

Все пушимо в репозиторії, і запускаємо Workflow.

В Job, яка викликає Reusable Workflow частини даних нема:

В Job, яка викликається напряму в Caller Workflow всі дані є:

GitHub Context

Коли Reusable Workflows викликається з Caller Workflow, github контекст завжди буде мати дані з Caller Workflow.

Наприклад, в Reusable Workflow додамо відображення імені репозиторію:

name: Reusable Workflow

# trigger from other workflows
on:
  workflow_call:
    inputs:
      test-input:
        required: true
        type: string      

jobs: 

  test:
    runs-on: ubuntu-latest

    steps: 
      - name: "Test: print Hello"
        run: echo "Hello, World!"

      ...

      - name: "Test: print Repository Name from the github context"
        run: echo ${{ github.repository }}

І маємо ім’я atlas-test – репозиторій з Caller Workflow:

Тепер можна починати робити Worfklow для Terraform та Helm, але це вже зовсім інша історія.

Корисні посилання