Reusable Github Workflows

Posted on April 15, 2023 by Adrian Wyssmann ‐ 4 min read

Do you have multiple projects of the same topic, whicch use the same workflows? Then you might have a look into reusable workflows.

I have multiple ansible roles and I want to use the same workflows for all of these roles. This is when reusable workflows comes into play:

Rather than copying and pasting from one workflow to another, you can make workflows reusable. You and anyone with access to the reusable workflow can then call the reusable workflow from another workflow.

This consists of two elements:

  • shared workflow: The actuall workflow which does all the work
  • caller workflow: A workflow that uses another workflow

Creating a shared workflow

Firs we are starting to create our shared workflow in a dedicate repository “github-actions-workflows”. The workflow are filed under the default path for workflows: .github/workflows/, e.g. .github/workflows/ansible-roles-release.yml. In my case the workflow will do some linting, release preparation, creating and publishing the release (ansible role).

name: Create release

on:
  workflow_call:
    secrets:
      gh_token:
        required: true
      galaxy_api_key:
        required: true

jobs:
  build:
    name: Ansible linting
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Lint Ansible Playbook
      uses: ansible/ansible-lint-action@main
    - name: Install Dependencies
      run: pip install ansible
    - name: Create ansible.cfg with correct roles_path
      run: printf '[defaults]\nroles_path=../:./' >ansible.cfg
    # - name: Run ansible syntax-check
    #   run: ansible-playbook tests/test.yml -i tests/inventory --syntax-check

  prepare-release:
    name: Prepare Release
    runs-on: ubuntu-latest
    steps:
      - name: Set RELEASE_VERSION
        run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 
      - uses: actions/checkout@v2
        with:
          ref: ${{ github.ref }}
          fetch-depth: 0
      - name: Create release branch
        run: git checkout -b release/$RELEASE_VERSION &&  git push --set-upstream origin release/$RELEASE_VERSION
      - name: Install auto-changelog
        run: sudo npm install -g auto-changelog
      - name: Set current version
        run: echo $RELEASE_VERSION > ./VERSION
      - name: Create changelog
        run: auto-changelog --ignore-commit-pattern "^\[?ci|docu|Merge|meta\]?|fixup" --release-summary 
      - uses: stefanzweifel/git-auto-commit-action@v4
        with:
          commit_message: "meta: Update changelog, bump version"
          file_pattern: ./VERSION ./CHANGELOG.md
          commit_user_name: GitHub Actions
          commit_user_email: [email protected]
          commit_author: Papanito <[email protected]>
          push_options: --force

  create-release:
    name: Create Release
    runs-on: ubuntu-latest
    steps:
      - name: Set env
        run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Install auto-changelog
        run: sudo npm install -g auto-changelog
      - name: Create release notes
        run: auto-changelog --ignore-commit-pattern "^\[?ci|docu|Merge|meta\]?|fixup" --starting-version $RELEASE_VERSION -o RELEASENOTES.md --release-summary 
      - name: Create Release
        id: create_release
        uses: actions/create-release@v1
        env:
          GITHUB_TOKEN: ${{ secrets.gh_token }} # This token is provided by Actions, you do not need to create your own token
        with:
          tag_name: ${{ github.ref }}
          release_name: Release ${{ github.ref }}
          body_path: ./RELEASENOTES.md
          draft: false
          prerelease: false
    needs: 
    - build
    - prepare-release

  import-role:
    runs-on: ubuntu-latest
    needs: create-release
    steps:
    - name: Publish Ansible role to Galaxy
      uses: hspaans/ansible-galaxy-action@v1
      with:
        api_key: ${{ secrets.galaxy_api_key }}

As you can see there are two important things:

  • the workflow will be triggered on [workflow_call]
  • the workflow defines secrets (but also could define inputs)
  • the secrets are reference accordingly in the steps like api_key: ${{ secrets.galaxy_api_key }}

Workflows that call reusable workflows in the same organization or enterprise can use the inherit keyword to implicitly pass the secrets.

Caller Workflow

In the repositroy where you want to run the reusable workflow, you have to add the calling workflow under .github/workflows/, e.g. .github/workflows/ansible-roles-release.yml.

name: Create release

on:
  push:
    tags:
      - 'v*.*.*'

jobs:
  call-workflow-passing-data:
    uses: papanito/github-actions-workflows/.github/workflows/ansible-roles-release.yml@main
    secrets:
      gh_token: ${{ secrets.GITHUB_TOKEN }}
      galaxy_api_key: ${{ secrets.ANSIBLE_GALAXY_APIKEY }}

As part of the jobs you have to add call-workflow-passing-data.uses, which refers to the workflow which as to run, which is my ansible-roles-release.yaml on main-branch. I also pass the required secrets under secrets. Important is that these secrets (e.g GITHUB_TOKEN) are defined in the repository where the caller workflow is.

That’s all, pretty simple isn’t it, and indeed very helpful. However, keep in mind, there are certain limitations:

  • You can connect up to four levels of workflows. For more information, see “Nesting reusable workflows.”
  • You can call a maximum of 20 reusable workflows from a single workflow file. This limit includes any trees of nested reusable workflows that may be called starting from your top-level caller workflow file.
  • For example, top-level-caller-workflow.yml → called-workflow-1.yml → called-workflow-2.yml counts as 2 reusable workflows.
  • Any environment variables set in an env context defined at the workflow level in the caller workflow are not propagated to the called workflow. For more information, see “Variables” and “Contexts.”
  • To reuse variables in multiple workflows, set them at the organization, repository, or environment levels and reference them using the vars context. For more information see “Variables” and “Contexts.”

Workflow publisher

In order to deploy the caller workflow to all the repositories, I also use a workflow publisher, which is stored under .github/workflows/workflow-publisher.yml of the repository “github-actions-workflows”. This job runs when changes are made to the calling workflow.

name: Workflow Publisher
on:
  push:
    branches: ['develop', 'main']
    paths:
      - 'ansible-roles/**'
  workflow_dispatch:

jobs:
  publish-workflow-template:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        target_repo: [
          'ansible-role-cloudflared',
          'ansible-role-crowdsec',
          'ansible-role-diskmounter',
          'ansible-role-systemd_notifiers',
          'ansible-role-ttrss',
          'ansible-role-backup',
          'ansible-role-git'
        ]
    steps:
      - name: Checkout source repo
        uses: actions/checkout@v3
        with:
          path: main
      - name: Checkout target repo
        uses: actions/checkout@v3
        with:
          repository: ${{ github.repository_owner }}/${{ matrix.target_repo }}
          path: ${{ matrix.target_repo }}
          token: ${{ secrets.GH_TOKEN}}
          ref: main
      - name: Update workflows
        shell: bash
        run: |
          mkdir -p .github/workflows/
          cp ../main/ansible-roles/* .github/workflows/          
        working-directory: ./${{ matrix.target_repo }}
      - name: Publish workflows
        uses: stefanzweifel/git-auto-commit-action@v4
        with:
          commit_message: "ci: update workflows"
          commit_user_email: [email protected]
          commit_user_name: GitHub Actions
          repository: ./${{ matrix.target_repo }}
        continue-on-error: false