The Power of Automation with GitHub Action - How to Create your Action
Tags: javascript, github, ci, docker
Published: 2020-05-29 01:57:55

---
title: The Power of Automation with GitHub Action - How to Create your Action
cover_image: https://i.morioh.com/76fc68b1e3.png
published: true
description: Story of How I developed my first GitHub Action
tags: #javascript, #github, #ci, #docker
---

In the past two to four months I started to manage a new project where thankfully I was able to apply a lot of the things that I've been learning from courses and readings while having in mind the experience of the team members that I'm working with to make things easy but at the same time a little bit challenging so I can encourage them to learn new things or reinforced the knowledge that they currently have.

In the first two weeks of the project, we had to deliver an MVP so we decide to host it in [Heroku](https://heroku.com) where I created a pipeline for multi-environment, which now that I think about it was an overkilled :sweat_smile: since it was just an MVP.

Moving on, I wanted to be able to push my Docker images to the Heroku Registry so every little piece of code that was merged I manually built the image and pushed it to Heroku.

So far so good, but I was getting tired of doing the same thing over and over again, so that's when I remember that I can use GitHub Actions to automate this process :bulb:. I search the GitHub Marketplace for something that allows me to build and push my docker images to Heroku, I found some things, but it wasn't what I wanted. So I did whatever an engineer would do, create its action :sunglasses:.

## Read the Docs!

Since I've never worked with Action I have to go and read the [documentation](https://help.github.com/en/actions) which I found out that it is a well-documented feature.

Something that caught my attention was that one can write their actions for some of the common programming languages such as JavaScript, Python, and Java. You can read more about the supported languages and framework [here](https://help.github.com/en/actions/language-and-framework-guides).

Now that I know that I can write an action for my project, I then went ahead and landed on the [create actions](https://help.github.com/en/actions/creating-actions) page, here I noticed that you can write your Actions with JavaScript or Bash, which is cool :wink: for me.

## Building the action

I decided to use JavaScript to write my action so as usual, create a folder for your project:

```bash
mkdir my-action && cd my-action
```

### Add the `action.yml`

Open your project directory with your favorite IDE or Code Editor and create a new file called `action.yml`. This file is where you are going to define your action metadata and should have the following structure:

```yml
name: # Name of your action
description: # Some Fancy description explaining what this does
inputs: # User input for you action
  id_of_your_input:
    description: # What is this input about
    required: # Set this to true if the input is required or set it to fall if otherwise
    default: # Some default value
outputs:
  time: # id of output
    description: 'The time we greeted you'
runs:
  using: 'node12'
  main: 'index.js'
```

So I created my `action.yml` and it looks something like this:

```yml
name: 'Deploy Docker Image to Heroku App'
author: 'Jean Carlos Taveras'
description: 'A simple action to build, push and Deploy a Docker Image to your Heroku app.'
inputs:
  email:
    description: 'Email Linked to your Heroku Account'
    required: true
  api_key:
    description: 'Your Heroku API Key'
    required: true
  app_name:
    description: 'Your Heroku App Name'
    required: true
  dockerfile_path:
    description: 'Dokerfile path'
    required: true
  options:
    description: 'Optional build parameters'
    required: false
runs:
  using: 'node12'
  main: 'dist/index.js'
```

### Install Dependencies

Before you can start coding you need to install two dependencies

  - `@actions/core`
  - `@actions/github`

The `@actions/core` is required for you to be able to pull the declared input and output variables and more from the `action.yml`. On the other hand, the `@actions/github` is used to get information about the Action Context and more.

```bahs
npm install -s @actions/core @actions/github
```

### Write the core of the action

Create an `index.js` file and let's import the dependencies:

```javascript
const core = require('@actions/core');
const github = require('@actions/github'); // In case you need it
```

Since I'm going to need to execute docker and Heroku commands I'll need to add the `child_process` and the `util` modules and get the `promisify` function from the latter one.

```javascript
...
const { promisify } = require('util');

const exec = promisify(require('child_process').exec);
```

Nice! Now I have to create a function to allow authentication to the Heroku Registry.

```javascript
...

async function loginHeroku() {
  const login = core.getInput('email');
  const password = core.getInput('api_key');

  try {	
    await exec(`echo ${password} | docker login --username=${login} registry.heroku.com --password-stdin`);	
    console.log('Logged in succefully ✅');	
  } catch (error) {	
    core.setFailed(`Authentication process faild. Error: ${error.message}`);	
  }	
}
```

Good! Now I need to build the Docker image, push it to the Heroku Registry and deploy it to the Heroku App


```javascript
...

async function buildPushAndDeploy() {
  const appName = core.getInput('app_name');
  const dockerFilePath = core.getInput('dockerfile_path');
  const buildOptions = core.getInput('options') || '';
  const herokuAction = herokuActionSetUp(appName);
  
  try {
    await exec(`cd ${dockerFilePath}`);

    await exec(`docker build . --file Dockerfile ${buildOptions} --tag registry.heroku.com/${appName}/web`);
    console.log('Image built 🛠');

    await exec(herokuAction('push'));
    console.log('Container pushed to Heroku Container Registry ⏫');

    await exec(herokuAction('release'));
    console.log('App Deployed successfully 🚀');
  } catch (error) {
    core.setFailed(`Something went wrong building your image. Error: ${error.message}`);
  } 
}
```

Now that I see this, I need to refactor this function :sweat_smile:. I think I took it too seriously when I said let's Write the core of our action. 

As you might notice there's a function called `herokuActionSetUp` which is just a helper function that returns the Heroku action (push or release).

```javascript
...

/**
 * 
 * @param {string} appName - Heroku App Name
 * @returns {function}
 */
function herokuActionSetUp(appName) {
  /**
   * @typedef {'push' | 'release'} Actions
   * @param {Actions} action - Action to be performed
   * @returns {string}
   */
  return function herokuAction(action) {
    const HEROKU_API_KEY = core.getInput('api_key');
    const exportKey = `HEROKU_API_KEY=${HEROKU_API_KEY}`;
  
    return `${exportKey} heroku container:${action} web --app ${appName}` 
  }
}
```

We are almost done. We just need to call our functions and since these functions are asynchronous then we can chain them together as follow:

```javascript
...

loginHeroku()
  .then(() => buildPushAndDeploy())
  .catch((error) => {
    console.log({ message: error.message });
    core.setFailed(error.message);
  })
```

### Bundle your code

To prevent committing your `node_modules/` folder you can run:

```bash
npx zeit/ncc build index.js
```

This will create a `dist` folder with a bundle `index.js` file bare in mind that you have to change the `runs` section in your `action.yml` file to point to the bundled JavaScript file:

```yml
runs:
  using: 'node12'
  main: 'dist/index.js'
```

### Add a README

You should add a `README.md` file to let users to how to use your action.

## Testing your Action

You can follow the instructions in the GitHub documentation [Testing out your action in a workflow](https://help.github.com/en/actions/creating-actions/creating-a-javascript-action#testing-out-your-action-in-a-workflow). However, I found this method of testing really painful since you have to push your code every time you make a change. What you can do then is run your actions locally by using [nektos/act](https://github.com/nektos/act) is a well-documented tool and easy to use.

---

That's it, that's all you need to know to create an action with JavaScript. This post turned out to be a little bit longer than I thought it would since this is my first post.

Thanks and check this action in the GitHub Marketplace [Deploy Docker Image to Heroku App](https://github.com/marketplace/actions/deploy-docker-image-to-heroku-app) as well as the repo at [jctaveras/heroku-deploy](https://github.com/jctaveras/heroku-deploy).