GitHub Reusable Workflows and Custom Actions
Tags: devops, github, githubactions, aws
Published: 2022-10-06 09:47:42

![GitHub Action Logo image](https://cdn.svc.oneadvanced.com/engineering-blogs/github-reusable-workflows-and-custom-actions/title.png)

## 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_

```yaml
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

```

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_

```yaml
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

```

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_

```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

```

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

_.GitHub/workflows/main.yml_

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

### 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_

    ```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_

    ```yaml
    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_

    ```javascript
    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

    ```text
    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_

    ```yaml
    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:

<img src="https://cdn.svc.oneadvanced.com/engineering-blogs/github-reusable-workflows-and-custom-actions/workflow-output.png" alt="GitHub custom Action output image" style="height: 150px;"/><br/>

## 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

* [Reusable Workflows](https://docs.GitHub.com/en/enterprise-cloud@latest/Actions/using-workflows/reusing-workflows)
* [Custom Actions](https://docs.GitHub.com/en/enterprise-cloud@latest/Actions/creating-actions/about-custom-actions)