Published on

Automating the Laravel 8 schema dump using GitHub Actions

Authors

The release of Laravel 8 introduced us to many great new features, and I think it is one of the best releases we have ever had! 🎉 😍

However, to me, the schema dump command is far by the most useful for me in some personal projects and for the large projects I work on every day at work. I couldn't wait to delete the migrations from some project where we had hundreds of migration files. In this post I will take you through the challenges faced when I decided to add schema:dump into our release process and how we automated it using GitHub Actions.

This post assumes that you have some working knowledge of GitHub Actions, their yaml workflow files, and that you know what the schema command does. If not please check the Laravel documentation first.

When to trigger the schema dump workflow?

This will probably be different from project to project and how you manage your releases. At work, we use GitHub releases for both non-prod (test and staging) and for production. Our nonprod release tags usually contain the words alpha or beta, for example, 2.4.11-alpha.4 or 2.4.11-beta.12 where the final number is a release candidate identifier. When we create these nonprod releases in github we tick the This is a pre-release checkbox and within a GitHub action you are able to check whether a published release is a pre-release or not.

Shows the GitHub release page pre-release checkbox

I knew from the start that we wanted to run schema:dump whenever we published a release but not when the published release was marked as a pre-release. This made setting up the first part of the schema-dump.yml easy:

name: "Schema Dump"

on:
  release:
    types: [published]

jobs:
  dump:
    if: github.event.release.prerelease != true
    # ...

Running the schema dump command

Once I had copied all the boilerplate MySQL, PHP, Composer, Cache and Laravel setup from our existing Run tests action it was time to add the step to actually run the schema:dump command. As you can see in the below snippet it runs the migrate command first to go through all the current migrations.

Then we run the schema:dump command, the --prune argument tells the command to delete any migration files as the generated mysql-schema.sql file will contain an absolute version of your database structure, so the migration files are no longer needed.

- name: Run migration and schema dump
  run: |
    php artisan migrate --force
    php artisan schema:dump --prune
  env:
    DB_HOST: 127.0.0.1
    DB_PORT: ${{ job.services.mysql.ports[3306] }}

Automatically creating a pull request from the changes

Fortunately, the GitHub Actions has an action for almost anything you need and there is an action created specifically to make creating a PR from local changes really simple. The Create Pull Request action allows you to customise anything you can think of from the PR title, body, commit message, the new branch and much more.

- name: Create Pull Request
  uses: peter-evans/create-pull-request@v3
  with:
    base: master
    branch: schema-dump
    branch-suffix: short-commit-hash
    commit-message: "Run schema dump to clean up migrations"
    title: "Run schema dump to clean up migrations"
    body: "This PR was generated by the schema-dump GitHub Action"

If the schema:dump has made any changes then the above step will create a branch with a name like schema-dump-ae234ab where the suffix is from the commit hash of the changes. The other properties are self-explanatory please see the action for the documentation.

Generated PRs won't run other workflows 💥

I assume, in an attempt to prevent someone from accidentally creating an infinite loop, the above step successfully creates the pull request, but it does not trigger my other workflows that will run tests using the newly generated mysql-schema.sql file.

Assuming those previously using the 'create pull request action' will have encountered this I searched the Issues on that action and came across an answer which linked to some documentation the maintainer has provided on the various options available to make the PR trigger further workflow runs.

Creating a Schema Dumper GitHub App and using it in the workflow

I quickly decided that the best option was to create a GitHub App and install on my organization so that it can be used in multiple projects. Again, the create pull request maintainer has graciously provided documentation on how to use this, and the changes required to make it work within the workflow which means I don't need to go through those steps in this post.

- name: Generate new token using app
  uses: tibdex/github-app-token@v1
  id: generate-token
  with:
    app_id: ${{ secrets.SCHEMA_DUMPER_APP_ID }}
    private_key: ${{ secrets.SCHEMA_DUMPER_APP_PRIVATE_KEY }}

- name: Create Pull Request
  uses: peter-evans/create-pull-request@v3
  with:
    base: master
    branch: schema-dump
    branch-suffix: short-commit-hash
    commit-message: "Run schema dump to clean up migrations"
    title: "Run schema dump to clean up migrations"
    body: "This PR was generated by the schema-dump GitHub Action"
    token: ${{ steps.generate-token.outputs.token }}

Once this was in place the PRs were being created and the Run tests workflows were running automatically on those PRs. The schema-dumper bot was now the author of the PR which rounds it off nicely.

Shows the generated GitHub pull request

Conclusion

I hope this post has inspired you to automate your schema dumps or perhaps try automating some other parts of your release process. Let me know on Twitter if you find any issues with what I have described or if you want to talk about it. Please find my full schema-dump.yml workflow file below.

name: "Schema Dump"

on:
  release:
    types: [published]

jobs:
  test:

    if: github.event.release.prerelease != true

    runs-on: ubuntu-latest

    name: Schema Dump (${{ github.event.release.tag_name }})

    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ALLOW_EMPTY_PASSWORD: yes
          MYSQL_DATABASE: services_test
        ports:
          - 3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Cache composer dependencies
        uses: actions/cache@v1
        with:
          path: ~/.composer/cache/files
          key: dependencies-php-7.4-composer-${{ hashFiles('composer.lock') }}
          restore-keys: |
            dependencies-php-7.4-composer-

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: 7.4
          extensions: curl, dom, fileinfo, filter, iconv, json, libxml, mbstring, openssl, pcntl, pcre, pdo, simplexml, sqlite, pdo_sqlite, tokenizer, xml, xmlwriter, zip

      - name: Install PHP dependencies (composer)
        run: |
          composer config "http-basic.nova.laravel.com" "${{ secrets.NOVA_USERNAME }}" "${{ secrets.NOVA_PASSWORD }}"
          composer install --no-interaction --no-suggest

      - name: Prepare Laravel Application
        run: cp .env.testing .env

      - name: Run migration and schema dump
        run: |
          php artisan migrate --force
          php artisan schema:dump --prune
        env:
          DB_HOST: 127.0.0.1
          DB_PORT: ${{ job.services.mysql.ports[3306] }}

      - name: Generate new token using app
        uses: tibdex/github-app-token@v1
        id: generate-token
        with:
          app_id: ${{ secrets.SCHEMA_DUMPER_APP_ID }}
          private_key: ${{ secrets.SCHEMA_DUMPER_APP_PRIVATE_KEY }}

      - name: Create Pull Request
        uses: peter-evans/create-pull-request@v3
        with:
          base: master
          branch: schema-dump
          branch-suffix: short-commit-hash
          commit-message: "Run schema dump to clean up migrations"
          title: "Run schema dump to clean up migrations"
          body: "This PR was generated by the schema-dump GitHub Action"
          token: ${{ steps.generate-token.outputs.token }}

Want to talk about this post? Discuss this on Twitter →