Skip to content

The workflows

How a package is tested and released is defined once, in the reusable workflows of innoboxrr/.github, and every repository calls them. That file used to be copied into each repository, and copying isn't sharing: when something had to change, either all fifteen got touched or fifteen variants were left behind.

Repositorytests.yml callsrelease.yml calls
Composer packagephp-tests.ymlphp-release.yml
npm packagenode-tests.ymlnode-release.yml

All are referenced at @main: uses: innoboxrr/.github/.github/workflows/php-tests.yml@main.

php-tests.yml

yaml
# .github/workflows/tests.yml
name: Tests

on:
    push:
        branches: [main, master]
    pull_request:

jobs:
    tests:
        uses: innoboxrr/.github/.github/workflows/php-tests.yml@main

name: Tests matters: release.yml waits for a workflow with that name.

Inputs

InputTypeDefaultWhat it does
php-versionsstring (JSON array)'["8.3", "8.4"]'PHP versions in the matrix
auditbooleantrueRun larapack:audit against the package
extensionsstring''Extra PHP extensions, comma-separated
tests-requiredbooleantrueFail when there's no runnable suite. With false, a package without tests passes by installing and compiling
COMPOSER_AUTH (secret)optionalComposer auth JSON, for private dependencies declared as vcs repositories
yaml
jobs:
    tests:
        uses: innoboxrr/.github/.github/workflows/php-tests.yml@main
        with:
            php-versions: '["8.3", "8.4", "8.5"]'
            extensions: 'pdo_sqlite, sqlite3'
        secrets:
            COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}

What it does, in order

One run per PHP version (fail-fast: false):

  1. Installs with composer update --prefer-dist --no-interaction --no-progress. update, not install: packages are libraries and don't commit a lockfile, so the suite runs against what whoever installs them will actually resolve.
  2. Syntax: php -l on every file in src/. It's the minimum gate left for packages without a suite.
  3. Resolved versions of illuminate/support and phpunit/phpunit, for the log.
  4. Audit (when audit is true):
    • with LaraPack 7 or later in vendor/, runs php vendor/innoboxrr/larapack-generator/builder larapack:audit .;
    • with an older LaraPack that lacks larapack:audit, warns and skips;
    • inside LaraPack itself, runs php builder larapack:audit .;
    • without LaraPack, warns and skips.
  5. Tests: vendor/bin/phpunit if there's PHPUnit, its config and at least one *Test.php. Otherwise it fails, or only warns with tests-required: false.

WARNING

The audit only runs when LaraPack is installed in vendor/, via require or require-dev. And only in repositories whose tests.yml calls php-tests.yml: innoboxrr/traits and innoboxrr/search-surge have their own matrix and currently don't go through the audit in CI.

php-release.yml

yaml
# .github/workflows/release.yml
name: Release

on:
    workflow_run:
        workflows: ['Tests']
        types: [completed]
        branches: [main, master]
    workflow_dispatch:

jobs:
    release:
        permissions:
            contents: write

        uses: innoboxrr/.github/.github/workflows/php-release.yml@main
InputDefaultWhat it does
php-version'8.4'PHP the job runs on

What it does:

  1. Only runs when dispatched by hand (workflow_dispatch) or when Tests succeeded.
  2. Checks out the commit Tests ran on (workflow_run.head_sha) with full history.
  3. Reads the version from VERSION, trimming spaces and newlines. Only when there's no VERSION does it look at the version key of composer.json; if there's neither, it fails.
  4. If the tag already exists, it does nothing.
  5. Creates the tag without a v prefix (7.10.3) and pushes it. Packagist publishes when it receives it.

DANGER

permissions: contents: write goes in each repository's release.yml. The reusable workflow needs it to push the tag, and a repository whose default token permission is read-only can't grant it. That doesn't fail the job: it fails the whole workflow at startup (startup_failure), with no log and no explanation.

INFO

The header comment of php-release.yml says "the version is declared by composer.json". It isn't: VERSION wins, and the version key in composer.json is only a last resort that larapack:audit flags as an error (composer-version). Use VERSION.

node-tests.yml

yaml
# .github/workflows/tests.yml
name: Tests

on:
    push:
        branches: [master]
    pull_request:

jobs:
    tests:
        uses: innoboxrr/.github/.github/workflows/node-tests.yml@main
InputDefaultWhat it does
node-versions'["20", "22", "24"]'Node versions in the matrix
working-directory'.'The package directory, if not the root

For each Node version: checkout, npm install --no-audit --no-fund (no lockfile, for the same reason as Composer), and npm test.

node-release.yml

yaml
# .github/workflows/release.yml
name: Release

on:
    workflow_run:
        workflows: ['Tests']
        types: [completed]
        branches: [master]
    workflow_dispatch:

jobs:
    release:
        uses: innoboxrr/.github/.github/workflows/node-release.yml@main
        secrets: inherit
InputDefaultWhat it does
working-directory'.'The package directory

What it does, with permissions: contents: write and id-token: write:

  1. Only runs after a green Tests or when dispatched by hand.
  2. Node 22 with the npm registry, then npm install -g npm@latest: the npm bundled with Node 22 predates OIDC support.
  3. Reads the version from package.json. If the tag v<version> already exists, it does nothing.
  4. npm install, npm test, and npm publish --provenance --access public without NODE_AUTH_TOKEN.
  5. Creates and pushes the tag v<version>.

Publishing uses trusted publishing; the requirements are in Releasing a version.

Jobs each repository adds

quality: Pint and Larastan

Packages created by larapack:new, and LaraPack itself, add a job to tests.yml that checks formatting and types. It doesn't depend on the PHP version, so it runs once:

yaml
jobs:
    tests:
        uses: innoboxrr/.github/.github/workflows/php-tests.yml@main

    quality:
        name: Pint y Larastan
        runs-on: ubuntu-latest

        steps:
            - uses: actions/checkout@v5

            - uses: shivammathur/setup-php@v2
              with:
                  php-version: '8.4'
                  coverage: none

            - name: Instalar dependencias
              run: composer update --prefer-dist --no-interaction --no-progress

            - name: Formato
              run: vendor/bin/pint --test

            - name: Análisis estático
              run: vendor/bin/phpstan analyse --no-progress --memory-limit=1G

With its configuration:

json
{
    "preset": "laravel"
}
yaml
includes:
    - vendor/larastan/larastan/extension.neon

parameters:
    level: 5
    paths:
        - src
        - database
    # Larastan learns each model's columns by reading its migrations.
    databaseMigrationsPath:
        - database/migrations
    # Casts live in the casts() method; without this Larastan doesn't know payload is an array.
    parseModelCastsMethod: true

What LaraPack generates comes out formatted and passes that analysis, so a failure here is something written by hand: vendor/bin/pint formats it.

To add it to an existing package:

bash
composer require --dev laravel/pint larastan/larastan
# copy pint.json, phpstan.neon.dist and the quality job from a freshly created package
vendor/bin/pint
git commit -am "Formatear con Pint"

Formatting everything at once, in its own commit, keeps the history of other changes clean.

LaraPack also tests PHP 8.5 (php-versions: '["8.3", "8.4", "8.5"]'): 8.3 resolves symfony/console 7.4 and 8.4 or later resolves 8.x, which have different signatures in Command::configure().

frontend: the base application's interfaces

innoboxrr/laravel-setup copies its interfaces as-is into every new application: if their tests fail, nothing is released.

yaml
    frontend:
        runs-on: ubuntu-latest

        strategy:
            fail-fast: false
            matrix:
                ui: [vue, react]

        name: Interfaz ${{ matrix.ui }}

        defaults:
            run:
                working-directory: tests/Frontend/${{ matrix.ui }}

        steps:
            - uses: actions/checkout@v5

            - uses: actions/setup-node@v5
              with:
                  node-version: 22

            - run: npm install --no-audit --no-fund

            - run: npx vitest run

Why satellites stay on Vite 7 and Vitest 3

The ecosystem's npm packages test with Vite 7 and Vitest 3, and the baseline pins it (js.vite: ^7.1.0, js.vitest: ^3.0.0). Applications and generated modules are already on Vite 8, Laravel 13's.

The reason is npm: Vitest 4 with Vite 8.3 crashes npm install on npm 10, the npm bundled with Node 20 and 22, two of the three versions in the node-tests.yml matrix. With npm 11 the suites pass, but CI installs with each Node's own npm.

A generated module can require Vite 8 because it doesn't install Vitest. For the same reason, laravel-setup's React test harness (tests/Frontend/react) uses vite ^7.1, @vitejs/plugin-react ^5 and vitest ^3.2, even though the application it copies requires vite ^8 and @vitejs/plugin-react ^6.

This gets revisited when npm 10 fixes it or CI moves to npm 11.

Common CI problems

Startup failures, reruns that reuse an old workflow version, empty logs and secrets across organizations are covered in Troubleshooting.