Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

GitHub Actions Reusable Workflows and Matrix Builds

By Kokil Thapa | Last reviewed: September 2026

GitHub Actions Reusable Workflows and Matrix Builds solve the same problem every growing repo hits: your test job YAML grows until three projects need the same steps. You copy a block, tweak PHP versions, and six months later the files drift apart. Reusable workflows let one repository own the pipeline logic. Matrix builds fan that logic across PHP 8.3, 8.4, and 8.5—or across Ubuntu and macOS—without tripling your file size. If you already run GitHub Actions for Laravel testing and deploy, this is the next layer that keeps CI maintainable as teams and packages multiply.

What are GitHub Actions reusable workflows and how do they differ from composite actions?

A reusable workflow is a normal workflow file that other workflows invoke with uses:. It lives in .github/workflows/ and declares on: workflow_call. The caller passes inputs and secrets. The callee runs as a child workflow with its own job graph.

Composite actions bundle steps inside a single job. Reusable workflows can span multiple jobs, call other reusable workflows, and enforce organisation-wide standards. On production Laravel apps I maintain, reusable workflows fit org-wide PHP test templates. Composite actions fit smaller step groups like "install Composer deps with cache".

Reusable Workflow ArchitectureCaller RepoApp repositoryci.yml triggersReusable WForg/.github repophp-test.ymlRunnerubuntu-latestExecutes jobsuses:Inputs and Secrets Flowphp-versioninput stringwith: blockcaller passessecrets inheritoptional flagSame pattern as Terraform modules — define once, call many times
GitHub Actions Reusable Workflows and Matrix Builds: caller repo invokes a shared workflow with inputs and secrets

The mental model matches Terraform modules for reusable infrastructure. Your org repo holds the canonical test pipeline. Each app repo calls it with project-specific inputs. When PHPUnit or Pest steps change, you update one file—not twelve forks of the same job.

FeatureReusable WorkflowComposite ActionDuplicated YAML
Multiple jobsYesNoYes
Cross-repo sharingYesYesManual sync
Matrix fan-outAt caller or calleeInside one job onlyPer repo
Secrets scopeExplicit pass or inheritJob-level onlyPer repo
Best forFull CI templatesStep bundlesOne-off projects

GitHub documents reusable workflows in their official reusing workflows guide. Read that alongside this article when you wire your first callable file.

How do you create a reusable workflow in GitHub Actions?

Start in a dedicated repo—often org/.github or a ci-templates repo. Create .github/workflows/php-test.yml with a workflow_call trigger and typed inputs.

Define the reusable workflow

# .github/workflows/php-test.yml  (in org/ci-templates repo)
name: PHP Test Template

on:
  workflow_call:
    inputs:
      php-version:
        required: true
        type: string
      laravel-version:
        required: false
        type: string
        default: "12"
    secrets:
      COMPOSER_AUTH:
        required: false

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ inputs.php-version }}
          extensions: mbstring, pdo_mysql, redis
          coverage: none

      - name: Install dependencies
        run: composer install --prefer-dist --no-progress

      - name: Run tests
        run: vendor/bin/pest --parallel

Inputs become available as ${{ inputs.php-version }}. Secrets do not flow automatically unless the caller sets secrets: inherit: true or maps them explicitly. That default protects tokens from leaking into workflows that should not see them.

Call it from an application repository

# .github/workflows/ci.yml  (in your Laravel app repo)
name: CI

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

jobs:
  php-tests:
    uses: my-org/ci-templates/.github/workflows/php-test.yml@main
    with:
      php-version: "8.4"
      laravel-version: "12"
    secrets: inherit

Pin the ref to a tag or SHA—not a floating branch—in production. I tag template repos v1.4.0 and bump callers deliberately. A silent break on @main at 2 AM is not a drill you want.

For Laravel 13 you need PHP 8.3 minimum. Laravel 12 runs on PHP 8.2 or higher. Your reusable workflow inputs should document those floors so callers do not pass invalid pairs. See our Laravel API build guide for project layout assumptions this pipeline expects.

How does the matrix strategy work in GitHub Actions?

A matrix tells GitHub to run the same job multiple times with different variable sets. You declare strategy.matrix keys. GitHub creates one job per combination unless you add exclude or include rules.

Matrix Build Fan-Outstrategy.matrixPHP 8.3ubuntuPHP 8.4ubuntuPHP 8.5ubuntuPHP 8.4macOSMatrix Controlsfail-fast: true stops siblings on first failuremax-parallel: 4 caps concurrent runnersexclude / include refine combinations
Matrix strategy in GitHub Actions Reusable Workflows and Matrix Builds fans one job into parallel runners per PHP and OS combo

A typical Laravel matrix might cover PHP 8.3, 8.4, and 8.5 on Ubuntu. Add macOS only when you ship native extensions or CLI tools that behave differently. Each extra cell costs runner minutes. Budget accordingly—especially on private repos.

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      max-parallel: 4
      matrix:
        php: ["8.3", "8.4", "8.5"]
        os: [ubuntu-latest]
        exclude:
          - php: "8.5"
            os: ubuntu-latest   # example: opt out until deps support 8.5
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
      - run: composer install
      - run: vendor/bin/pest

Set fail-fast: false on library packages so one PHP version failure still shows results for the rest. Use true on app repos during active development when you want fast feedback. The GitHub matrix jobs documentation covers include for adding odd combinations without exploding the default grid.

Service containers pair well with matrices. A MySQL 8.4 LTS cell and a MySQL 9.7 cell catch driver edge cases before production. Validate generated workflow YAML with a JSON or YAML formatter tool when debugging matrix syntax errors—GitHub's error messages often point at line numbers in expanded configs.

How do you combine reusable workflows with matrix builds?

You have two valid patterns. Both appear in real orgs. Pick based on who owns the matrix definition.

Pattern A: matrix on the caller, reusable workflow per cell

The caller job defines the matrix and passes one value set per invocation. Each matrix cell becomes a separate reusable workflow run.

jobs:
  test:
    strategy:
      matrix:
        php: ["8.3", "8.4", "8.5"]
    uses: my-org/ci-templates/.github/workflows/php-test.yml@v2
    with:
      php-version: ${{ matrix.php }}
    secrets: inherit

This keeps version lists visible in each app repo. Product teams choose their supported PHP floor. The shared template stays version-agnostic.

Pattern B: matrix inside the reusable workflow

The reusable workflow owns the full grid. Callers invoke one line. Upgrading PHP 8.5 support happens centrally.

# Inside reusable workflow
jobs:
  test:
    strategy:
      matrix:
        php: ["8.3", "8.4", "8.5"]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}

I use Pattern A for client repos with different Laravel versions. Pattern B fits internal packages where the platform team controls compatibility policy entirely. The approach mirrors Ansible roles and Galaxy reusable automation: parameters at the edge, logic in the middle.

Matrix Placement DecisionPattern A: Caller MatrixApp repo lists PHP versionsPer-project flexibilityMore YAML in app reposBest for client appsLaravel 12 vs 13 mixDifferent deploy targetsPattern B: Callee MatrixTemplate owns PHP gridOne-line caller CICentral policy updatesBest for shared packagesComposer librariesOrg-wide standards
Choosing where to define the matrix in GitHub Actions Reusable Workflows and Matrix Builds depends on who owns version policy

Nesting reusable workflows is supported up to depth limits GitHub documents. A top-level CI file can call a reusable test workflow and a reusable deploy workflow sequentially. Deploy steps should not sit inside the test matrix unless you enjoy paying for redundant SSH keys.

What production setup works for Laravel and PHP projects?

On booking platforms like Adventure Third Pole Trek and sister legal-tech sites I deploy with GitLab CI and Deployer 7. Many teams migrating to GitHub want the same test grid without rewriting deploy logic. Reusable workflows cover the test half cleanly. Pair them with OIDC deploy workflows without long-lived keys for the release half.

A production-grade caller file for Laravel 12 might look like this:

  1. Trigger on push, pull request, and weekly cron for dependency drift.
  2. Call a reusable lint job—Pint or PHP CS Fixer—once on PHP 8.4.
  3. Matrix test across PHP 8.3, 8.4, and 8.5 with MySQL 8.4 service container.
  4. Build frontend assets with Node.js 26 LTS in a separate job—not inside every matrix cell.
  5. Call a reusable deploy workflow only from main after tests pass.
jobs:
  lint:
    uses: my-org/ci-templates/.github/workflows/php-lint.yml@v2

  test:
    needs: lint
    strategy:
      fail-fast: false
      matrix:
        php: ["8.3", "8.4", "8.5"]
    uses: my-org/ci-templates/.github/workflows/php-test.yml@v2
    with:
      php-version: ${{ matrix.php }}
      mysql-version: "8.4"
    secrets: inherit

  assets:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "26"
      - run: npm ci && npm run build

  deploy:
    needs: [test, assets]
    if: github.ref == 'refs/heads/main'
    uses: my-org/ci-templates/.github/workflows/deploy-php.yml@v2
    secrets: inherit

Keep asset builds out of the PHP matrix. Node installs triple your bill for zero extra signal on a typical Blade/Vite 8.x app. Cache Composer and npm directories keyed on lockfile hashes. actions/cache@v4 with composer.lock and package-lock.json paths saves minutes on every push.

For teams comparing platforms, read our GitHub Actions vs GitLab CI comparison for 2026. GitLab CI extends and include files solve a similar DRY problem. The syntax differs. The architecture does not.

Laravel CI Job SequenceLint JobMatrix TestPHP 8.3–8.5Vite BuildNode 26 LTSDeployGotchas to AvoidRunning npm build inside every matrix cellFloating @main ref on production callersMissing permissions for OIDC deploy256 combo matrix from unfiltered cross productUse exclude / max-parallel to control cost
Production Laravel pipeline using GitHub Actions Reusable Workflows and Matrix Builds with separate asset and deploy jobs

Permissions deserve explicit blocks when reusable deploy workflows use OIDC. The caller must set id-token: write and contents: read at workflow or job level. Reusable workflows do not inherit caller permissions by default in all contexts—check the current GitHub Actions token security guide when upgrading.

What are common mistakes with reusable workflows and matrix builds?

Most failures I debug on client migrations are structural—not syntax typos.

  • Unbounded cross products. Three PHP versions times four OS targets times three database versions equals 36 cells. Filter aggressively. Test OS variance only where it matters.
  • Secrets not forwarded. Reusable workflows need explicit secrets: mapping or inherit: true. Missing COMPOSER_AUTH for private Packagist shows up as obscure 403 errors mid-composer install.
  • Outputs not wired. Reusable workflows expose job outputs via workflow_call.outputs. Callers read them with needs.job-id.outputs.name. Skipping this forces hacky artifact passing for simple version strings.
  • Concurrency ignored. Add concurrency: group: … cancel-in-progress: true on pull request workflows. Matrix runs multiply quickly. Without cancellation, five pushes mean five full grids.
  • Reusable workflow in the same repo. Reference it as ./.github/workflows/name.yml. Cross-repo uses owner/repo/.github/workflows/name.yml@ref. Mixing the two formats produces path errors that look like missing files.

Runner cost adds up on private repos. GitHub-hosted minutes for a 36-cell matrix on every commit can exceed Rs 15,000/month (~USD 110) on busy teams. Self-hosted runners on Ubuntu 24 LTS—managed through proper Linux system administration—can pay back quickly at scale.

When pipelines break after a template bump, treat it like any production incident. Roll the caller ref back to the previous tag. Fix forward in the template repo. Document the change in a changelog clients read before bumping v2 to v3. The same discipline applies to build pipeline automation best practices on any CI platform.

For quality gates beyond unit tests, wire reusable workflows into testing and optimization services that add browser tests or Lighthouse budgets as optional matrix dimensions—one include row, not a default column.

Key Takeaways

  • Define reusable workflows with on: workflow_call, typed inputs, and pinned refs—never floating branches in production callers.
  • Place the matrix on the caller when apps need different PHP floors; place it in the template when the platform team owns compatibility policy.
  • Keep Node/Vite builds and deploy jobs outside the PHP test matrix to control runner cost and complexity.
  • Forward secrets explicitly, set OIDC permissions on deploy callers, and cap parallel cells with max-parallel.
  • Pair test reusable workflows with tagged release workflows and OIDC deploy patterns for a complete pipeline without YAML duplication.
  • Audit matrix size before merge—a cross product above nine cells deserves an explicit team approval and cost note.

People Also Ask

Can a reusable workflow call another reusable workflow?

Yes. Nested reusable workflows are supported within GitHub's documented depth limit. A common pattern is a top-level CI caller that invokes separate reusable test and deploy workflows. Keep deploy out of the test matrix unless each cell truly needs an independent deployment target.

What is the maximum matrix size in GitHub Actions?

GitHub enforces a job limit per workflow run—currently 256 jobs including matrix expansions. Large cross products hit this ceiling silently during workflow parsing. Use exclude, split into multiple workflows, or shard test suites across jobs when approaching the limit.

How do reusable workflows share variables between jobs?

Inside a reusable workflow, use job outputs and needs dependencies—the same as ordinary workflows. To pass data back to the caller, define outputs on workflow_call and map them from job outputs. Environment variables set at workflow level are visible to all jobs in that reusable run.

Are reusable workflows available on GitHub Free plans?

Reusable workflows work on public repositories at all plan tiers. Private repositories need GitHub Team or Enterprise for calling reusable workflows across repository boundaries. Same-repo reusable calls work on Free private repos. Check your org plan before centralising templates in a private ci-templates repo.

Ship DRY CI without losing control

GitHub Actions Reusable Workflows and Matrix Builds turn CI from copy-pasted YAML into a maintainable platform layer. Start with one reusable PHP test workflow, add a caller-side matrix for the versions your Laravel app actually supports, and pin every cross-repo reference to a semver tag. Expand to deploy templates once tests are stable. If you want help designing a pipeline for a Laravel, WordPress, or custom PHP product—or migrating from GitLab CI without downtime—contact us for a CI architecture review or browse related guides on Terraform CI/CD with GitHub Actions and custom software development.

Frequently Asked Questions

Reusable workflows are callable YAML files triggered with on: workflow_call that other workflows invoke via uses:. Matrix builds use strategy.matrix to run the same job across variable sets like PHP 8.3, 8.4, and 8.5. Together they let one shared pipeline fan out without duplicating steps across repositories.

A reusable workflow is a full workflow file that can span multiple jobs, accept typed inputs and secrets, call other reusable workflows, and enforce org-wide CI standards. A composite action bundles steps inside a single job only. On production Laravel apps I maintain, reusable workflows fit org-wide PHP test templates. Composite actions fit smaller bundles like installing Composer dependencies with cache. Reusable workflows also support cross-repo sharing without manual YAML sync; composite actions require per-repo duplication unless wrapped carefully.

Create a dedicated repo such as org/ci-templates and add a file like .github/workflows/php-test.yml with on: workflow_call, typed inputs for php-version and laravel-version, and optional secrets like COMPOSER_AUTH. Define jobs with steps using ${{ inputs.php-version }}. Callers reference it as my-org/ci-templates/.github/workflows/php-test.yml@v2 with with: blocks for inputs. Pin the ref to a tag or SHA in production, not a floating branch. Document PHP floors so Laravel 13 callers pass 8.3 or higher and Laravel 12 callers pass 8.2 or higher.

Declare strategy.matrix keys such as php and os, and GitHub spawns one job per combination unless you add exclude or include rules. A typical Laravel grid covers PHP 8.3, 8.4, and 8.5 on ubuntu-latest. Set fail-fast: false on library packages so one PHP failure still reports the rest. Use fail-fast: true on active app development for faster feedback. Cap parallel runners with max-parallel to control queue time and billing. Service containers pair well with matrices, for example MySQL 8.4 LTS and MySQL 9.7 cells to catch driver edge cases before production.

Pattern A puts the matrix on the caller and passes one php-version per cell into the reusable workflow, keeping version lists visible per app repo. Pattern B embeds the matrix inside the reusable workflow so callers invoke one line and the platform team upgrades PHP support centrally. I use Pattern A for client repos on different Laravel versions. Pattern B fits internal packages where compatibility policy is owned centrally. Both are valid. Nesting is supported within GitHub's documented depth limit, so a top-level CI file can call separate reusable test and deploy workflows sequentially.

Put the matrix on the caller when product teams need different PHP floors or Laravel version pairs per repository. Put it inside the reusable workflow when a platform team owns compatibility policy and wants one-line invocations from every app. The article's production example uses Pattern A with php: ["8.3", "8.4", "8.5"] on the caller while the shared template stays version-agnostic. Pattern B is better when upgrading PHP 8.5 support should happen once in ci-templates, not across twelve application repos. Either way, keep deploy and Node asset jobs outside the test matrix.

Each matrix cell bills as a separate runner job. A 36-cell grid on every commit can exceed Rs 15,000/month (~USD 110) on busy private teams.

Trigger on push, pull request, and a weekly cron for dependency drift. Call a reusable lint job once on PHP 8.4 with Pint or PHP CS Fixer. Matrix test across PHP 8.3, 8.4, and 8.5 with a MySQL 8.4 service container. Build frontend assets with Node.js 26 LTS in a separate job, not inside every matrix cell. Call a reusable deploy workflow only from main after tests pass. Cache Composer and npm directories with actions/cache@v4 keyed on composer.lock and package-lock.json hashes. Set explicit OIDC permissions on deploy callers: id-token: write and contents: read.

Unbounded cross products explode cost and hit GitHub's 256-job limit. Three PHP versions times four OS targets times three database versions equals 36 cells nobody needs. Secrets do not flow automatically; missing secrets: inherit or explicit mapping causes obscure Composer 403 errors for private Packagist. Skipping workflow_call.outputs forces hacky artifact passing. Ignoring concurrency: with cancel-in-progress: true lets five pushes spawn five full grids. Mixing reference formats breaks paths: same-repo uses ./.github/workflows/name.yml while cross-repo uses owner/repo/.github/workflows/name.yml@ref. Bumping template tags without a rollback plan breaks callers at 2 AM.

No. Secrets do not flow unless the caller sets secrets: inherit: true or maps each secret explicitly under a secrets: block. That default protects tokens from leaking into workflows that should not see them. Private Packagist access via COMPOSER_AUTH is a common failure when teams assume inheritance. Always audit which secrets each reusable template actually needs before enabling inherit on org-wide callers. Deploy workflows using OIDC still need explicit permission blocks on the caller because reusable workflows do not inherit caller permissions in all contexts.

In the caller's .github/workflows/ci.yml, reference the template with uses: my-org/ci-templates/.github/workflows/php-test.yml@v2, passing inputs under with: and secrets under secrets: inherit or explicit mapping. Pin @v2 to a tag or commit SHA, never @main in production. Cross-repo calls require the template repo to grant access per GitHub's reusing workflows guide. Same-repo calls use ./.github/workflows/name.yml without the owner/repo prefix. Private repositories need GitHub Team or Enterprise for cross-repo reusable workflow calls; same-repo reusable calls work on Free private repos.

Yes, within GitHub's documented nesting depth limit.

GitHub enforces a 256-job limit per workflow run, including all matrix expansions. Large cross products hit this ceiling during workflow parsing.

Reusable workflows work on public repositories at all plan tiers. For private repositories, calling reusable workflows across repository boundaries requires GitHub Team or Enterprise. Same-repository reusable workflow calls work on Free private repos. Check your org plan before centralising templates in a private ci-templates repo that multiple application repositories must invoke. Public template repos are a common workaround for open-source Laravel packages that need shared PHPUnit or Pest pipelines without paid tier requirements.

Inside a reusable workflow, wire job outputs through needs dependencies exactly like ordinary workflows. To return data to the caller, define outputs on workflow_call and map them from job outputs. Callers read them with needs.job-id.outputs.name. Skipping this forces unnecessary artifact uploads for simple values like generated version strings. Environment variables set at workflow level inside the reusable run are visible to all jobs in that invocation. This mirrors standard GitHub Actions job chaining but requires explicit workflow_call.outputs declaration that many first migrations forget.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: