DEV Community

Cover image for GitHub Reusable Workflows and Custom Actions
Saurabh Shah for Advanced

Posted on

GitHub Reusable Workflows and Custom Actions

GitHub Action Logo image

Overview

This blog is focused on taking your GitHub Workflows to next level by using GitHub Reusable Workflows and Custom Actions.
Both Options save us time, reduces duplication, and improve consistency. let's see how this works!

Why Reusable Workflows?

Let's say you have 20 repositories of the same type and they all have similar jobs in GitHub Actions. Instead of writing and maintaining these common GitHub Action jobs in each repository, you can create a centralized repository that can be reused.

You can refer to the workflows in the centralized repository from the Source Repositories. When you want to make a change to an existing job or add any new job. You would just need to update the reusable workflows in a centralized repository and then no longer need to update workflows for all 20 source repositories as in our example.

This means you can now easily manage the workflows from one place, saving a lot of time. Isn’t it?

How to Create Reusable Workflow?

Creating a Reusable workflow is very easy. you just add a workflow_call trigger, with some optional inputs and secrets. The workflow_call trigger allows your workflow to be called by other workflows.

Things to note:

  1. caller Workflow: A workflow that uses another workflow is referred to as a "caller" workflow. In our example, caller Workflow resides in SourceRepoA Repository.

  2. called Workflow: The reusable workflow is a "called" workflow. In our example, called Workflow resides in centralGitHubActions Repository.

First, let's create a simple reusable workflow (i.e "called" workflow) main.yml in centralGitHubActions Repository.

centralGitHubActions/.GitHub/workflows/main.yml

on:
  workflow_call:
    inputs:
      runs-on:
        description: The Platform to execute on
        type: string
        default: ubuntu-latest
      node-version:
        description: The version of node to be used
        type: string
        default: 14.x
      project-folder:
        description: The folder containing the project to build
        type: string
        default: .

jobs:
  release:
    runs-on: ${{ inputs.runs-on }}
    defaults:
      run:
        working-directory: ${{ inputs.project-folder }}
    steps:
      - uses: Actions/checkout@v3
      - name: Setup Node
        uses: Actions/setup-node@v3
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
          cache-dependency-path: subDir/package-lock.json
      - name: Install dependencies
      - run: npm ci
      - name: Build App
      - run: npm run build --if-present
      - name: Run Tests
      - run: npm test
      - name: Publish App
        run: npm publish
      - name: Upload a Build Artifact
        uses: Actions/upload-artifact@v2.2.2
        with:
          name: build-${{ github.event.number }}
          path: ${{ inputs.project-folder }}
          if-no-files-found: error

Enter fullscreen mode Exit fullscreen mode

Now the caller workflow main.yml in SourceRepoA Repository can refer to the called workflow main.yml in centralGitHubActions Repository as below:

SourceRepoA/.GitHub/workflows/main.yml

name: Main Build Workflow

on:
  push:
    branches:
      - main

jobs:
  release:
    uses: centralGitHubActions/.GitHub/workflows/main.yml@main
    with:
      runs-on: "ubuntu-latest"
      node-version: "14.x"
    secrets: inherit

Enter fullscreen mode Exit fullscreen mode

We declare a job (release) and then use uses to specify the called workflow. We use the format owner/repo/path@label to specify the exact location and version of the workflow. Then we use the keyword to specify values for the inputs. we have also used the inherit keyword to implicitly pass the secrets.

That's all you need to get started with reusable workflow :)

Custom Actions

If you can’t find something that fits your needs from GitHub Marketplace, you can create your own custom Action to use within your organization or even publish it to Marketplace.

There are multiple ways to create custom Actions:

  • Docker container Action
  • JavaScript Action
  • Composite Action

In This Blog, We are going to concentrate on Composite Action and JavaScript Action.

Composite Actions

A composite Action allows you to combine multiple workflow steps within one Action.
Below shows a small composite GitHub Action to get the output values of a cloud formation stack.

.GitHub/Actions/edge-info-action/action.yaml


name: Edge Info Action
Description: Determine lambda arn and it's latest version from Auth@Edge CloudFormation Stack
inputs:
  edge-stack-name:  # stack-name
    description: 'Auth@Edge CloudFormation Stack Name'
    required: true
outputs:
  CheckAuth_arn:
    description: lambda arn with latest version
    value: ${{ steps.get-CheckAuth-arn.outputs.CheckAuth-arn }}
  OriginRewrite_arn:
    description: lambda arn with latest version
    value: ${{ steps.get-OriginRewrite-arn.outputs.OriginRewrite-arn }}
runs:
  using: composite
  steps:
    - id: get-CheckAuth-arn
      run: |
        CHECKAUTH_ARN=$(aws cloudformation describe-stacks --region us-east-1 --stack-name ${{ inputs.edge-stack-name }} | jq -r ".Stacks[].Outputs | .[] | select(.OutputKey == \"LambdaEdgeFnCheckAuth\") | .OutputValue")
        echo "::set-output name=CheckAuth-arn::$CHECKAUTH_ARN"
      shell: bash
    - id: get-OriginRewrite-arn
      run: |
        ORIGINREWRITE_ARN=$(aws cloudformation describe-stacks --region us-east-1 --stack-name ${{ inputs.edge-stack-name }} | jq -r ".Stacks[].Outputs | .[] | select(.OutputKey == \"LambdaEdgeFnOriginRewrite\") | .OutputValue")
        echo "::set-output name=OriginRewrite-arn::$ORIGINREWRITE_ARN"
      shell: bash

Enter fullscreen mode Exit fullscreen mode

Below Code shows how to refer the custom Action from the main workflow:

.GitHub/workflows/main.yml

    ...
    - name: Get edge functions ARN
      uses: ./.GitHub/Actions/edge-info-action
        with:
          edge-stack-name: 'edge-functions'

Enter fullscreen mode Exit fullscreen mode

JavaScript Actions

There are a few steps we need to follow to create a Javascript Action. Let’s see that in form of an example:

This is a simple example just to give you an overview on how it works. We are writing a custom Action to get the Pull Request Number once it's merged.

  1. Create a new Repository or use an existing repository to create a custom Action.

  2. Run npm init -y in your Repository. This generates a package.json file.

  3. Run npm install @Actions/core and npm install @Actions/GitHub to install the Actions toolkit core and GitHub packages. Actions toolkit is a collection of Node.js packages that allow you to quickly build JavaScript Actions with more consistency. So now you should see a node_modules directory with the modules you just installed and a package-lock.json. your package.json should be something like this:

    centralGitHubActions/package.json

    {
      "name": "centralGitHubActions",
      "version": "1.0.0",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },  
      "dependencies": {
        "@Actions/core": "^1.10.0",
        "@Actions/GitHub": "^5.1.1"
      }
    }
    
    
  4. Create Metadata action.yml file in root path of your Repository.

    centralGitHubActions/action.yml

    name: 'Get Pull Request Number on Merge'
    
    description: 'Get Pull Request Number on Merge '
    runs:
      using: 'node16'
      main: 'dist/index.js'
    branding:
      icon: 'check'
      color: 'gray-dark'
    outputs:
      prNumber: # output will be available to future steps
        description: 'Merged PR Number'
    
    
  5. Write the Action Code in index.js.

    centralGitHubActions/index.js

    const core = require('@Actions/core');
    const GitHub = require('@Actions/GitHub');
    
    try {
      // Get the JSON webhook payload for the event that triggered 
      the workflow
      const message = GitHub.context.payload.head_commit.message
      console.log("prNumber", JSON.stringify(message.match(/#([0- 
      9]*)/)?.[1] | '0'))
      core.setOutput("prNumber", JSON.stringify(message.match(/# 
      ([0-9]*)/)?.[1] | '0'))
    } catch (error) {
      core.setFailed(error.message);
    }
    
    

    Here we have used the Actions toolkit to get access to GitHub
    Actions contexts, setting outputs, and failing exit codes.

  6. As Per Best Practice you should add a ReadMe File as well to let people know how to use your Action.

  7. node_modules folders are ideal for local repositories and should not be pushed to remote repositories as they can cause problems. As for our Action since we need the dependencies to be installed before we run the custom Action, we would need something which has dependencies pre-installed. For this reason, we are using a tool called @vercel/ncc to compile code and its dependencies into a single file.

  8. Install vercel/ncc by running this command in your terminal: npm i -g @vercel/ncc

  9. Compile your index.js file using command ncc build index.js. You'll now see a new dist/index.js file with your code and its dependencies in a single file.

  10. Let's commit our changes to the remote branch and add a version tag. It's best practice to add a version tag for releases of your Action

    git add .
    git commit -m "feat: new Action to get Merged PR number"
    git tag -a -m "Get Merged PR Number" v1.0
    git push --follow-tags
    
  11. It's time to test our Action in workflow. Below is an example on how to use it:

    SourceRepoA/.GitHub/workflows/main.yml

    name: test custom Actions workflow
    
    on:
      push:
        branches:
          - main
    jobs:
      test_Action:
        runs-on: ubuntu-latest
        name: A job to test custom Action
        steps:
          - name: test custom Action
            id: mergedPR
            uses: myOrg/centralGitHubActions@v1.0
          - name: Get the output
            run: echo "PR number is ${{ steps.mergedPR.outputs.prNumber }}"
    
    
  12. Below is the output snapshot of GitHub Workflow Result:

GitHub custom Action output image

Summary

Adopting reusable workflows and custom Actions at Advanced, has helped us to reduce redundancy and standardize GitHub Actions in our Organization. I hope this Information helps your Organization too if not using already.

That's all for today. If this content has helped you, please let us know in the comments section.

Also please let us know what interesting topics around CI/CD would you like to know more about.

Reference

Top comments (2)

Collapse
 
hobbit71 profile image
Martin Reynolds

Great article

Collapse
 
saurabh210 profile image
Saurabh Shah

Thanks Martin :)