Currently Available: Need a skilled Software Developer for your next project?
Categories
Laravel

Setting Up Continuous Integration for Laravel Packages

Laravel package maintainers need tests that check how their code behaves inside Laravel, across the PHP and Laravel versions the package supports. Continuous integration (CI) runs those checks automatically when someone pushes a change or opens a pull request. Passing checks give maintainers evidence that the package works with the tested PHP and Laravel versions.

Use Orchestra Testbench to set up the Laravel context for package tests, then define a GitHub Actions matrix with one job for each supported PHP and Laravel combination. Treat failures on those combinations as release blockers. A matrix is a set of configuration values that GitHub combines into separate jobs; Testbench supplies the Laravel application context those jobs need.

Run package tests inside Laravel

A package’s tests need to exercise the framework behavior the package depends on. That behavior may include service-provider bootstrapping or routes, as well as configuration or migrations. Laravel’s package-development documentation recommends Testbench for running package tests as if the package were installed in a Laravel application.

Match each Laravel major version with a compatible Testbench major version. The Testbench compatibility table maps Laravel 12 to Testbench 10 and Laravel 13 to Testbench 11, among other combinations. That mapping is a starting point: check the exact package constraints and Testbench release requirements before adding a combination to CI.

Build a matrix around supported versions

The required matrix should reflect the compatibility the package promises to consumers. If the package supports Laravel 12 and 13, test both. Choose PHP versions allowed by the package’s Composer requirements and by each Laravel version. Avoid adding combinations that Composer cannot resolve under those constraints.

This example tests two Laravel versions on two PHP versions. Composer runs each combination with either the lowest allowed dependency versions or stable releases, for eight jobs total.

name: Package tests

on:
  push:
  pull_request:

jobs:
  tests:
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        stability: [prefer-lowest, prefer-stable]
        include:
          - php: "8.3"
            laravel: "12.*"
            testbench: "10.*"
          - php: "8.3"
            laravel: "13.*"
            testbench: "11.*"
          - php: "8.4"
            laravel: "12.*"
            testbench: "10.*"
          - php: "8.4"
            laravel: "13.*"
            testbench: "11.*"

    name: PHP ${{ matrix.php }} / Laravel ${{ matrix.laravel }} / ${{ matrix.stability }}

    steps:
      - uses: actions/checkout@v7

      - name: Set up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mbstring, pdo, sqlite, pdo_sqlite
          coverage: none

      - name: Validate Composer metadata
        run: composer validate --strict

      - name: Select framework and Testbench versions
        run: >
          composer require
          "laravel/framework:${{ matrix.laravel }}"
          "orchestra/testbench:${{ matrix.testbench }}"
          --no-interaction
          --no-update

      - name: Resolve and install dependencies
        run: >
          composer update
          --${{ matrix.stability }}
          --prefer-dist
          --no-interaction

      - name: Run tests
        run: vendor/bin/phpunit

The workflow selects Laravel and Testbench constraints before Composer resolves dependencies. The SQLite extensions shown are examples for packages whose tests use SQLite; configure the PHP extensions and database services your own tests require. The PHP setup action documents its PHP-version and extension options.

GitHub’s matrix fail-fast setting defaults to true, which cancels other matrix jobs after one fails. Setting it to false lets the remaining jobs finish, giving maintainers a fuller view of which combinations failed. GitHub documents both matrix behavior and job variations. The action references in the example should also be checked against the repository’s action-pinning and security policy.

Choose Composer commands for the test you want

composer install uses the exact dependency versions in composer.lock when that file exists. It is appropriate for a job that checks a pinned development dependency set:

composer install --prefer-dist --no-interaction

composer update resolves dependencies from the constraints in composer.json. Use it when a matrix changes Laravel or Testbench versions, or when you want to test the dependency ranges consumers may install. The example uses composer update for that reason. The prefer-lowest mode asks Composer to favor the lowest versions allowed by the constraints; prefer-stable favors stable releases.

For libraries, a committed lock file can help maintainers reproduce their development environment, but it does not set the versions Composer installs in applications that require the library. Composer describes this distinction in its library guidance. A separate locked-dependency job can use composer install while matrix jobs use composer update. Do not use --ignore-platform-reqs to force an incompatible PHP or extension combination to install; that hides a platform mismatch rather than testing it.

Make supported-combination failures release blockers

Treat a failing test or dependency-resolution error on a PHP and Laravel combination the package claims to support as a release blocker. The same applies to failures in supported package behavior, such as service-provider registration, configuration, routes, commands, or migrations. Testbench is designed to exercise package code in a Laravel context, so failures there directly challenge the package’s compatibility claim.

Also block a release when a required project check fails. For example, if the project requires Composer metadata validation, formatting, static analysis, or a coverage threshold, keep that check in CI and enforce its result. Composer recommends running composer validate before committing metadata changes and before tagging a release.

An experimental job for an unsupported future version is different: its failure does not establish a regression in a promised configuration. Label exploratory jobs clearly and keep them separate from required release checks. If a CI runner outage prevents a supported job from completing, the result is inconclusive; rerun or investigate the job rather than treating it as a pass.

What I'm building

Delegate tasks. Get software.

Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.

Take a look at vroni.com

Email updates

Usually a new article and a few links I found interesting.

No spam. Unsubscribe with one click.

Leave a Reply

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