GitHub Actions and Laravel Nova

The automation of development workflows like building or testing the application is known as Continuous Integration (CI). GitHub Actions make it easy to automate the workflows of your Laravel application.

Getting started with GitHub Actions

GitHub rolled out GitHub Actions for everyone in November 2019. GitHub Actions are free to use (up to 2000 minutes per month) for public and private repositories and execute a workflow every time a defined event is triggered on GitHub. GitHub provides quite a lot of webhook events that can trigger a workflow.
So let's explore how to automate the steps of our development workflow when working with Laravel and Laravel Nova.

Workflow file

To get started, you need to create the basic YAML configuration file. This file is located in the repository in the .github/workflows folder.

TL;DR: View the whole workflow file on GitHub.

ci.yml
name: Tests
on: [push]
jobs:
  tests:
    name: Run tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1

The keyword on defines when the action will be executed. In the example above, we listen for the [push] event. It is also possible to chain multiple events [push, pull_request] or restrict the workflow to a specific branch.

The jobs section can hold multiple steps. The first step loads an action actions/checkout@v1 provided by GitHub, imported with the keyword uses.

Workflows run on Linux, macOS, Windows, and even Docker container images. If you're interested in a setup with a custom Docker container image, read the post Using Github Actions to setup CI/CD with Laravel by Luis Dalmolin.

Services

Before adding additional steps to our workflow we want to include a service.

In most cases, Laravel Nova needs a database. For this reason, we're adding the MySQL service to the workflow. My application is still running on MySQL 5.7 so I'm pulling in the official MySQL Docker image for MySQL 5.7 (the image also supports other tags like MySQL 8) that will spin up a MySQL server in the container. Make sure to define the env variables and the port as it will not work without these parameters!

ci.yml
services:
  mysql:
    image: mysql:5.7
    env:
      MYSQL_DATABASE: database_ci
      MYSQL_USER: user
      MYSQL_PASSWORD: secret
      MYSQL_ROOT_PASSWORD: secretroot
    ports:
      - 33306:3306
    options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

To not confuse the environments I usually create a separate .env file for the CI environment. Based on the .env.example file create a new file with the name .env.github which is holding the MySQL credentials from above.
You can also find the contents of the file on GitHub.

.env.github
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=33306
DB_DATABASE=database_ci
DB_USERNAME=user
DB_PASSWORD=secret

Verifying the MySQL connection

We're ready to add the first step. To make sure we've set up everything correctly we start with a step that tests if the MySQL connection is working. This step is optional and not related to the remaining workflow but I find it very helpful to make sure the MySQL connection is actually working.

ci.yml
- name: Verify MySQL connection
  run: |
    mysql --version
    sudo apt-get install -y mysql-client
    mysql --host 127.0.0.1 --port ${{ job.services.mysql.ports['3306'] }} -uuser -psecret -e "SHOW DATABASES"

If you're having trouble setting up the MySQL connection jump to the troubleshooting section below.

Installing dependencies

Now we're good to go to set up the world of our application. In this step, we're going to download and install all of the required dependencies via Composer. To authenticate Nova in the GitHub CI environment add your NOVA_USERNAME and NOVA_PASSWORD to the GitHub Secrets in the repository. The secrets are environment variables that are encrypted and not shared in forked repositories.
You can set your secrets here: GitHub Repository › Settings › Secrets

Screenshot of GitHub Repository Settings
GitHub Repository › Settings › Secrets
ci.yml
- name: Install dependencies
  run: |
    php --version
    composer config "http-basic.nova.laravel.com" "${{ secrets.NOVA_USERNAME }}" "${{ secrets.NOVA_PASSWORD }}"
    composer install -n --prefer-dist

Booting Laravel

Next, we're going to boot the main Laravel application including Laravel Nova and all other dependencies. Therefore, we copy our CI credentials from the .env.github file to the .env file. After generating an encryption key we can run other artisan commands.

ci.yml
- name: Boot Laravel application
  run: |
    cp .env.github .env
    php artisan key:generate
    php artisan --version

Migrating the database

Run the available migrations to initiate the main structure of the database. Append the --seed flag in case you'd like to seed the database with default entries e.g. roles or settings. The seed should not be necessary for your unit or feature tests, but may be helpful for other tests.

ci.yml
- name: Migrate database
  run: |
    mysql --version
    php artisan migrate:fresh --seed

Running the build process

To make use of HTTP (feature) tests in our test suite we need to have a fully working application. This requires us to have access to all compiled assets in the CI environment. To compile the assets I'm using yarn but it works with npm as well.

ci.yml
- name: Run yarn
  run: |
    yarn --version
    yarn && yarn dev

Running the test suite

Finally, we are ready to run the PHPUnit test suite:

ci.yml
- name: Run tests
  run: |
    ./vendor/bin/phpunit --version
    ./vendor/bin/phpunit

Be aware of the two different environments:

  1. the CI environment: ci
    defined in the .env.github file, copied to the .env file
  2. the testing environment: testing
    defined in the phpunit.xml file

Laravel 6 uses an in-memory SQLite database for testing (config on GitHub).
If you'd like to run your tests against a MySQL database, set the DB_CONNECTION to mysql and add the name of the database to DB_DATABASE. Make sure that the database actually exists and is empty.

If you're having trouble setting up the MySQL connection jump to the troubleshooting section below.

Adding security checks

To check if the application uses dependencies with known security vulnerabilities we load and run the open source Symfony Security Checker. I like this step in particular as it adds additional value to the automation workflow.

ci.yml
- name: Run security checks
  run: |
    test -d security-checker || git clone https://github.com/sensiolabs/security-checker.git
    cd security-checker
    composer install
    php security-checker security:check ../composer.lock

Logs & Caching

We can cache the dependencies from Composer and Yarn. Therefore we use the actions/cache@v1 GitHub Action and a hash of the .lock-file as an identifier.

Ruben Van Assche covered these steps quite well in his post Getting started with GitHub Actions and Laravel so I'm not going to illuminate these steps anymore. The steps are also included in the workflow file on GitHub.

In case of an application error, you can download the logfiles from GitHub.

Screenshot of GitHub Actions Logs
Download the logfiles from failed actions

Running the complete workflow

FYI: Download the workflow file from GitHub.

After we commit and push the workflow file to our repository on GitHub the action runs (see on: [push] above). Booting up the CI server including all dependencies and executing all the steps takes about two to three minutes (up to 2000 minutes per month are free which makes the CI service basically free to use).

Watch the workflow in action in the video below:

Running the GitHub Actions workflow

Troubleshooting

Failed to initialize, mysql service is unhealthy.

There is a problem with the MySQL port. If not defined, the port number is a random number. You can get the port number from the job service. Use the env key to assign the dynamic MySQL port number to a step manually.

ci.yml
- name: Run tests
  run: ./vendor/bin/phpunit -v
  env:
    DB_PORT: ${{ job.services.mysql.ports['3306'] }}

SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed

The MySQL credentials are incorrect. Make sure to define the MYSQL_USER, MYSQL_PASSWORD and MYSQL_ROOT_PASSWORD in your .env file.
Read this answer on Stack Overflow for an in-depth explanation.


QueryException: SQLSTATE[HY000] [1045] Access denied for user 'user'@'localhost' (using password: YES)

The database does not exist or the database credentials are invalid. Check the .env file and make sure to clear the config cache after a change:
php artisan config:clear


RuntimeException: No application encryption key has been specified.

This error will pop up if the APP_KEY is not set. Make sure to generate an APP_KEY using the php artisan key:generate command before executing any commands using artisan.


Where to go from here?

The future of automation is now. Explore the awesome actions repository and use the available actions to create a release, send a notification or run a deployment. Or even better: Create your own actions!

Update: Freek Van der Herten shared and explained the GitHub workflow file for Ignition in his post Using GitHub actions to run the tests of Laravel projects and packages which contains a test matrix (php, laravel, dependency-version and os) as well as Slack notifications. Make sure to read the post as well!