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.

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.

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/setup-node@v1.1.0 with: node-version: '10.x' - run: yarn install - name: Setup PostgreSQL uses: harmon758/postgresql-action@v1.0.0 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:$HEROKU_API_KEY@git.heroku.com/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/setup-node@v1.1.0 with: node-version: '10.x' - run: yarn install - name: Setup PostgreSQL uses: harmon758/postgresql-action@v1.0.0 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:$HEROKU_API_KEY@git.heroku.com/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.

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.