Setting up GitHub Actions for a React/Node Project

This week I had a chance to try out GitHub Actions — GitHub’s continuous integration solution. I was at the point in a project where I would normally turn to CircleCI, so I thought I’d give GitHub Actions a try. If it worked out, that would be one less service I’d have to sign up and pay for.

github banner

At first glance, GitHub Actions looks a lot like CircleCI. You have similar concepts like workflows, jobs, and steps defined in a config file using YAML syntax. (The web project I’m trying this out on uses React, Node, Postgres, Yarn, and Typescript running on Heroku.)

GitHub Actions provides many workflow templates to get you started. I didn’t find one that exactly matched my scenario, so I started with the “Simple Workflow” template.

starter templates

When to Trigger the Workflow

I want to trigger my workflow in two cases:

  • When someone opens a pull request from any branch.
  • When anyone checks in code to my Develop or Master branch.

This requires a simple on: statement at the top of the file:


name: CI
on:
  pull_request:
  push:
    branches:
      - master
      - dev

Jobs

I want the workflow to run two jobs: one job runs my tests, and the other deploys to Heroku. I want the test job to run every time the workflow is triggered. I want the deploy job to run:

    jobs: test: name: Test # ... deploy: name: Deploy needs: test if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev') # ...

    The deploy job has a needs: test statement that will only run the Deploy job if the Test job was successful. The if: statement applies to the entire job and will only run it if we are pushing to the Develop or Master branches.

    Testing

    Now let’s get into more detail on how the test job is configured. I need to check out the code, set up Node and Postgres, install the dependencies utilizing a cache to speed up the process, and run the tests.

    
    test:
      name: Test
      runs-on: ubuntu-latest
      env:
        NODE_ENV: test
    
      steps:
      - name: Checkout
        uses: actions/checkout@v1
    
      - name: Get yarn cache
        id: yarn-cache
        run: echo "::set-output name=dir::$(yarn cache dir)"
    
      - uses: actions/cache@v1
        with:
          path: ${{ steps.yarn-cache.outputs.dir }}
          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-yarn-
    
      - uses: actions/[email protected]
        with:
          node-version: '10.x'
    
      - run: yarn install
    
      - name: Setup PostgreSQL
        uses: harmon758/[email protected]
        with:
          postgresql version: 11
          postgresql db: databasename-test
          postgresql user: dev
          postgresql password: dev
    
      - run: yarn lint
      - run: knex migrate:latest
      - run: yarn test
    

    This is probably a good time to introduce the GitHub Marketplace for Actions. In the script above, whenever you see a step that has a uses: statement, I am using an Action from the GitHub Action Marketplace to do a task for me.

    There are many Actions you can use to do all sorts of tasks in your workflow. In my test job, I am using actions for checking out the code, caching yarn dependencies, setting up Node, and setting up Postgres. All of them were easy to set up and were well documented.

    Any of the other steps in the test job that use the run: statement are running commands on the virtual machine in my project directory. Most of these yarn commands are ones that I have defined in my package.json file, so they are somewhat unique to my project.

    Deploy

    My project is running on Heroku. I have two apps on Heroku: one for QA and the other for production. If I am triggering this workflow from the Develop branch, I want to deploy to the QA app. If I am on the Master branch, I want to deploy to production. I do this by setting an environment variable called ENVIRONMENT. Then I use this environment variable to resolve the name of the appropriate Heroku app.

    The other environment variable I configure is the HEROKU_API_KEY. The key is stored as a GitHub Secret with the same name. Secrets are not automatically pulled into your workflow, so this step of assigning it to a local env: statement is necessary.

    To specify which branch/commit to push to Heroku, I am using an automatic environment variable provided to me by GitHub Actions. The GITHUB_SHA is the unique ID for the current commit that triggered the workflow.

    
    deploy:
      name: Deploy
      runs-on: ubuntu-latest
      needs: test
      if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev')
      env:
        HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
    
      steps:
        - name: Checkout
          uses: actions/checkout@v1
    
        - name: Set QA Environment
          run: echo "::set-env name=ENVIRONMENT::qa"
    
        - name: Set Production Environment
          if: github.ref == 'refs/heads/master'
          run: echo "::set-env name=ENVIRONMENT::prod"
    
        - name: Push to Heroku
          run: git push --force https://heroku:[email protected]/my-app-name-$ENVIRONMENT.git $GITHUB_SHA:master
    
        - name: Run Database Migrations
          run: heroku run knex migrate:latest --app my-app-name-$ENVIRONMENT
    

    The Full Workflow

    Here is the full workflow that contains my two jobs.

    
    name: CI
    on:
      pull_request:
      push:
        branches:
          - master
          - dev
    
    jobs:
      test:
        name: Test
        runs-on: ubuntu-latest
        env:
          NODE_ENV: test
    
        steps:
        - name: Checkout
          uses: actions/checkout@v1
    
        - name: Get yarn cache
          id: yarn-cache
          run: echo "::set-output name=dir::$(yarn cache dir)"
    
        - uses: actions/cache@v1
          with:
            path: ${{ steps.yarn-cache.outputs.dir }}
            key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
            restore-keys: |
              ${{ runner.os }}-yarn-
    
        - uses: actions/[email protected]
          with:
            node-version: '10.x'
    
        - run: yarn install
    
        - name: Setup PostgreSQL
          uses: harmon758/[email protected]
          with:
            postgresql version: 11
            postgresql db: databasename-test
            postgresql user: dev
            postgresql password: dev
    
        - run: yarn lint
        - run: knex migrate:latest
        - run: yarn test:server
    
      deploy:
        name: Deploy
        runs-on: ubuntu-latest
        needs: test
        if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev')
        env:
          HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
    
        steps:
          - name: Checkout
            uses: actions/checkout@v1
    
          - name: Set QA Environment
            run: echo "::set-env name=ENVIRONMENT::qa"
    
          - name: Set Production Environment
            if: github.ref == 'refs/heads/master'
            run: echo "::set-env name=ENVIRONMENT::prod"
    
          - name: Configure Heroku
            run: heroku config:set GIT_HASH=${GITHUB_SHA} GIT_BRANCH=${GITHUB_REF} --app my-app-name-$ENVIRONMENT
    
          - name: Push to Heroku
            run: git push --force https://heroku:[email protected]/my-app-name-$ENVIRONMENT.git $GITHUB_SHA:master
    
          - name: Run Database Migrations
            run: heroku run knex migrate:latest --app my-app-name-$ENVIRONMENT
    

    When a pull request is created or when code is finally merged to Develop or Master, GitHub will run my workflow. You can check the status of your workflow by clicking on the “Actions” tab on your GitHub project page.

    workflow running on GitHub

    If you are needing a CI server, and you are already paying for GitHub, give GitHub Actions a try. It did everything I needed it to do, so I will not be going back to CircleCI.