Article summary
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/[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.
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.