How GRRR uses GitHub Actions
Over the last 6 months, GRRR migrated from Travis CI to GitHub Actions, and we happily shed light on how we arrange the workflows.
It took me a while to realize what the difference is between GitHub Actions and Travis CI. The latter focuses on doing Continuous Integration (CI). GitHub Actions is a system to respond to events from the GitHub ecosystem. And yes, one of them is CI, by responding to the push
or pull_request
events. But much more events are available. GitHub Actions is a powerful tool with a lot of possibilities.
This article explains how GRRR uses GitHub Actions to test and deploy applications. We’ve published other GitHub Actions related articles about using OpenVPN and using AWS assume roles.
The workflows
For an application with unit tests and a production and staging environment we create four workflows:
ci.yml
: runs unit tests on every pushdeploy-staging.yml
: deploys themain
branch to stagingdeploy-production.yml
: deploys tags to productiondeploy.yml
: a re-usable workflow, to prevent duplicate code in thedeploy-...
workflows.
ci.yml
In the CI workflow, several tools are used to ensure our code is doing what it should, and code quality is secure. Below are the most common tools we use:
phpunit
docs: Run the PHP unit tests.jest
docs: Run the Javascript unit tests.composer validate
docs: to ensurecomposer.json
andcomposer.lock
are valid.php artisan migrate:rollback
: to ensure the rollback scripts don’t throw exceptions. It’s easy to forget it or make mistakes.phpstan
docs: static analysis tool for PHPprettier
docs: we love this opinionated code style because it supports every language we use. Just a one-stop-shop to solve the ongoing code style debate.- And maybe more by the time you’re reading this.
Below is an example workflow file copied from an active application. It contains some tools from the list above. Almost every tool gets a separate job. This makes it easy to get a list of failed checks in the GitHub UI.
The trigger to run the workflow is on:push
. That means a push to GitHub on any branch or tag will trigger a workflow run. To speed up releasing to production we ignore the release tags. More about that below: deploy-production.yml.
# ci.yml
name: CI
on:
push:
branches:
- "**"
tags-ignore:
- "*-release"
jobs:
php-tests:
name: PHP tests
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
- run: composer install --prefer-dist --no-interaction --ansi
- name: Run PHPUnit
run: vendor/bin/phpunit tests/
static-analysis:
name: Static analysis
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
- run: composer install --prefer-dist --no-interaction --ansi
- name: Run PHPStan
run: vendor/bin/phpstan analyse
prettier:
name: Prettier
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: yarn install
- name: Run Prettier
run: npx prettier --check .
deploy-staging.yml
We want the staging environment to run the latest code from main
. To accomplish that we run this workflow after a completed run of the CI workflow on the main
branch. In the example below you see that the workflow starts after a workflow_run
event. It says: after a completed run of the CI workflow on the main
branch, start this workflow.
Be aware of the difference between a completed and a successful run. A completed run is not always a successful one, but a successful one is always completed. In jobs:deploy:if
a check for completeness is added. The workflow context contains an event object with necessary information.
This could be enough for you, but the most appreciated feature by devs at GRRR is workflow_dispatch
. This adds a button to the GitHub UI to run this workflow manually, on a branch the dev chooses.
That makes it possible to temporarily deploy a non-main branch to the staging environment. It’s used to show experimental code to other devs or demo prototypes to clients.
A disadvantage of this approach is the fact that a push to main
overwrites the staging environment. We fix that by sending a message to the other devs working on the project: “I’m testing branch x on staging, please don’t merge until tomorrow.”. Another option is disabling the workflow, don’t forget to enable it after finishing the feature.
Recently, in Netlify-hosted projects, we have used the great feature of branch subdomains, which is the best solution to this problem.
name: Deploy staging
on:
workflow_run:
workflows: ['CI']
branches: [main]
types:
- completed
workflow_dispatch:
jobs:
deploy:
name: Deploy
uses: organization/repo/.github/workflows/deploy.yml@main
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
with:
environment: staging
version: ${{ github.ref_name }}
secrets:
SSH_KEY: ${{ secrets.SSH_KEY }}
KNOWN_HOSTS: ${{ secrets.KNOWN_HOSTS }}
deploy-production.yml
We release by creating a tag: 1.2.3-release
. It’s a semver number with the suffix -release
. Every tag with that suffix will be deployed to production. A reusable workflow contains the steps to deploy the application.
Unlike deploy-staging.yml
this workflow doesn’t depend on the CI workflow. Because the commit, which the tag refers to, is already checked by the CI workflow. A second time is not necessary and will slow the release process down.
name: Deploy production
on:
push:
tags:
- "*-release"
jobs:
deploy:
name: Deploy
uses: organization/repo/.github/workflows/deploy.yml@main
with:
environment: production
version: ${{ github.ref_name }}
secrets:
SSH_KEY: ${{ secrets.SSH_KEY }}
KNOWN_HOSTS: ${{ secrets.KNOWN_HOSTS }}
deploy.yml
A re-usable workflow is a workflow with on.workflow_call
in it. It supports the properties inputs
and secrets
. The two inputs and two secrets used in the example below are used to make the workflow dynamic.
The secrets look a bit verbose because it’s just repeating the names. It’s necessary because a re-usable workflow does not have access to secrets. You have to provide them explicitly. It prevents secret leaking and makes clear which secrets are being used.
name: "Deploy app"
on:
workflow_call:
inputs:
environment:
description: "GitHub and app environment"
required: true
type: string
version:
description: "Tag or branch to deploy"
required: true
type: string
secrets:
SSH_KEY:
description: "SSH public key"
required: true
KNOWN_HOSTS:
description: "SSH known hosts"
required: true
jobs:
deploy-app:
name: Deploy app
environment: ${{ inputs.environment }}
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
with:
ref: ${{ inputs.version }}
- name: Install SSH key
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.SSH_KEY }}
known_hosts: ${{ secrets.KNOWN_HOSTS }}
- uses: shivammathur/setup-php@v2
- name: Install PHP dependencies
run: composer install --prefer-dist --no-interaction --ansi
- name: Deployer
run: vendor/bin/dep deploy "${{ inputs.environment }}" --tag="${{ inputs.version }}" --log="deployer.log"
- name: Upload logs
uses: actions/upload-artifact@v3
if: always()
with:
name: logs
path: deployer.log
Conclusion
And that’s it. I’ve shown you the most common test and deploy workflows in our application repositories. Devs are happy with it, especially the possibility to deploy branches to the staging environment.