loading...
Cover image for The Power of Automation with GitHub Action - How to Create your Action

The Power of Automation with GitHub Action - How to Create your Action

jctaveras profile image Jean Carlos Taveras ・5 min read

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 where I created a pipeline for multi-environment, which now that I think about it was an overkilled 😅 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 💡. 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 😎.

Read the Docs!

Since I've never worked with Action I have to go and read the documentation 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.

Now that I know that I can write an action for my project, I then went ahead and landed on the create actions page, here I noticed that you can write your Actions with JavaScript or Bash, which is cool 😉 for me.

Building the action

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

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:

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:

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.

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

Write the core of the action

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

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.

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

...

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

...

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 😅. 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).

...

/**
 * 
 * @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:

...

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:

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:

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. 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 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 as well as the repo at jctaveras/heroku-deploy.

Posted on May 29 by:

jctaveras profile

Jean Carlos Taveras

@jctaveras

I'm a Senior Software Engineer and a technology passionate who is really interested in Full-Stack Development, AI, DevOps, and all the cool technology stuff out there

Discussion

markdown guide