GitHub Actions CI — Fast Feedback, No Duplicates

GitHub Actions CI — Fast Feedback, No Duplicates

Reading Time: 3 minutes

Series: Every Laravel Project Should Have These Building Blocks 
Part: 24 of 35 Level: Intermediate Prerequisites: None


What You’ll Learn

  • The two-pipeline structure: ci.yml and quality.yml
  • Why separating fast feedback from deep analysis matters
  • The optimized ci.yml — tests + style, under 3 minutes
  • The optimized quality.yml — Larastan + security audit
  • Composer and PHP caching strategies
  • Common mistakes in Laravel CI setups

Two Pipelines, Two Purposes

Most teams start with one CI file that does everything: lint, test, static analysis, security audit. The problem: when tests fail, you wait 4 minutes before seeing a failure that a 10-second linter run would have caught first. When Larastan fails on a trivial type error, the same failure blocks the PR whether it’s a typo or a logic flaw.

Two pipelines solve this:

ci.yml — Fast feedback (< 3 minutes). Runs on every push to every branch. Tests + code style. If this fails, you don’t merge.

quality.yml — Deep analysis (< 5 minutes). Runs on push to main and on PRs. Larastan + security audit. Fails don’t block immediately — they’re signals for improvement.


ci.yml — Tests and Style

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: ["*"]
  pull_request:

jobs:
  tests:
    name: Tests
    runs-on: ubuntu-latest

    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: testing
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: "8.4"
          extensions: pdo, pdo_mysql, bcmath, json
          tools: composer:v2
          coverage: none

      - name: Cache Composer dependencies
        uses: actions/cache@v4
        with:
          path: vendor
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-composer-

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist --optimize-autoloader

      - name: Prepare environment
        run: |
          cp .env.example .env
          php artisan key:generate --ansi

      - name: Run migrations
        run: php artisan migrate --force
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_PORT: 3306
          DB_DATABASE: testing
          DB_USERNAME: root
          DB_PASSWORD: password

      - name: Run tests
        run: php artisan test --compact --parallel
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_PORT: 3306
          DB_DATABASE: testing
          DB_USERNAME: root
          DB_PASSWORD: password

  style:
    name: Code Style
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: "8.4"
          tools: composer:v2
          coverage: none

      - name: Cache Composer dependencies
        uses: actions/cache@v4
        with:
          path: vendor
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-composer-

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist --optimize-autoloader

      - name: Check code style
        run: vendor/bin/pint --test

quality.yml — Static Analysis and Security

# .github/workflows/quality.yml
name: Quality

on:
  push:
    branches: [main]
  pull_request:

jobs:
  analyse:
    name: Static Analysis
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: "8.4"
          tools: composer:v2
          coverage: none

      - name: Cache Composer dependencies
        uses: actions/cache@v4
        with:
          path: vendor
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-composer-

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist --optimize-autoloader

      - name: Run Larastan
        run: vendor/bin/phpstan analyse --memory-limit=512M

  security:
    name: Security Audit
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: "8.4"
          tools: composer:v2
          coverage: none

      - name: Install production dependencies only
        run: composer install --no-interaction --no-dev --prefer-dist

      - name: Run security audit
        run: composer audit

The Most Common CI Mistakes

1. Larastan in Both Files

Running Larastan in ci.yml AND quality.yml doubles your CI minutes for zero benefit. Put it in quality.yml only.

2. Self-Copying .env

# ❌ This copies a file to itself — a no-op
- run: cp .env.testing .env.testing

# ✅ Copy example and generate a key
- run: |
    cp .env.example .env
    php artisan key:generate --ansi

3. Missing runner.os in Cache Key

# ❌ Cache collides across different OS runners
key: composer-${{ hashFiles('**/composer.lock') }}

# ✅ OS-specific cache key
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}

4. Hardcoded DB Credentials in Matrix

Use GitHub’s services block for the database container, not hardcoded passwords in env files. The service container’s credentials are never committed to the repository.

5. coverage: xdebug When You Don’t Need Coverage

Xdebug slows test execution by 2–5×. Unless you’re generating coverage reports, use coverage: none.


PHP Version Matrix (When Needed)

If you need to test on multiple PHP versions:

jobs:
  tests:
    strategy:
      matrix:
        php: ["8.3", "8.4"]

    name: Tests (PHP ${{ matrix.php }})
    runs-on: ubuntu-latest

    steps:
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}

For most app codebases (not packages), testing on one PHP version is enough.


Key Takeaways

  • Two pipelines: ci.yml for fast feedback (tests + style), quality.yml for deep analysis (Larastan + security).
  • Run ci.yml on every push. Run quality.yml on PRs and merges to main.
  • Include runner.os in every Composer cache key to prevent cross-OS collisions.
  • coverage: none unless you’re actively measuring coverage — it makes tests 2–5x faster.
  • Use composer install --no-dev in the security audit job — it’s the production dependency set you actually care about.
  • --parallel on php artisan test can halve test suite time on multi-core runners.

Tips and Gotchas

⚠️ Warning: Don’t run all quality checks in a single job. A single 8-minute job that combines tests + Larastan + security audit means every push waits 8 minutes for feedback. Split fast checks (tests, Pint) into one job and slow checks (Larastan, audit) into another. Fail fast on the critical path.

💡 Tip: Use concurrency in your workflow to cancel in-progress runs when a new push arrives on the same branch. This saves CI minutes and prevents stale feedback on abandoned commits.

🔥 Expert Note: Cache Composer dependencies with actions/cache keyed on composer.lock. A cache hit cuts install time from ~60s to ~5s on every run. Similarly cache the NPM node_modules keyed on package-lock.json.

Further Reading


← Notifications and Mail | Next: Static Analysis with Larastan →

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.