Skip to content
compiler.dev

Laravel GitHub Actions Workflow Template

Last updated

This is a working .github/workflows/ci.yml for a Laravel app tested against MySQL. It installs PHP with setup-php, caches Composer downloads, creates the .env, migrates the database and runs Pint and the test suite.

The workflow

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

concurrency:
  group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: laravel_test
        ports:
          - 3306:3306
        options: >-
          --health-cmd "mysqladmin ping -h 127.0.0.1 -proot"
          --health-interval 5s
          --health-timeout 5s
          --health-retries 20
    env:
      APP_ENV: testing
      DB_CONNECTION: mysql
      DB_HOST: 127.0.0.1
      DB_PORT: 3306
      DB_DATABASE: laravel_test
      DB_USERNAME: root
      DB_PASSWORD: root
    steps:
      - uses: actions/checkout@v4

      - uses: shivammathur/setup-php@v2
        with:
          php-version: "8.3"
          extensions: mbstring, pdo_mysql, bcmath
          coverage: none

      - name: Get Composer cache directory
        id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"

      - uses: actions/cache@v4
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: composer-${{ runner.os }}-${{ hashFiles('composer.lock') }}
          restore-keys: |
            composer-${{ runner.os }}-

      - run: composer install --no-interaction --prefer-dist --no-progress
      - run: cp .env.example .env
      - run: php artisan key:generate
      - run: vendor/bin/pint --test
      - run: php artisan migrate --force
      - run: php artisan test --parallel

What the non-obvious parts do

  • shivammathur/setup-php@v2 installs the PHP version and extensions you list. coverage: none turns off Xdebug and PCOV, which makes tests faster; set coverage: pcov only when you collect coverage.
  • The Composer cache steps save Composer's download cache, not vendor/. The directory is asked from Composer itself, so the path is right on any runner. The key is the hash of composer.lock.
  • MySQL health check uses 127.0.0.1 and the root password. MySQL takes several seconds to initialise, so the long retry count avoids early connection errors.
  • DB_HOST: 127.0.0.1 is deliberate. With localhost, PHP's MySQL driver tries a Unix socket that does not exist and fails.
  • cp .env.example .env and key:generate give the app a valid environment and APP_KEY without any real secrets.
  • pint --test reports style problems without rewriting files, so CI fails instead of silently fixing.
  • artisan test --parallel needs the brianium/paratest package, and creates a database per process. Drop the flag if you do not use it.

Variants

PHP version matrix

Add matrix.php: ["8.2", "8.3", "8.4"] and use ${{ matrix.php }}. Keep versions quoted. See matrix builds.

Frontend assets

Add actions/setup-node@v4 with cache: npm, then npm ci and npm run build before the tests if feature tests render Vite assets.

Postgres or SQLite

Swap the service for postgres:16 with pg_isready and set DB_CONNECTION: pgsql. In-memory SQLite needs no service and is faster, but hides engine differences.

Deploy job

Add a second job with needs: test on main only, and use OIDC for cloud access.

Run it on faster machines

Change one line:

    runs-on: [self-hosted, Linux, compiler-ci]

The job runs on a fresh 8 vCPU machine from compiler.dev, with automatic fallback to your original runner. Parallel tests make good use of the extra cores. See speed up Laravel on GitHub Actions and pricing.

FAQ

Should I cache the vendor directory?

Cache Composer's download cache instead. composer install from a warm cache is quick and always consistent with composer.lock.

Why does MySQL refuse connections at the start?

It is still starting. Keep the health check and use 127.0.0.1.

How do I run Dusk browser tests?

Dusk needs Chrome and a running app server. Run it in a separate job so slow browser tests do not delay unit tests, and upload screenshots as an artifact.

Can I use Sail in CI?

You can, but it adds Docker start-up time. Plain setup-php plus service containers is usually faster.