Skip to content
IMPRUTHVI

How 6 Laravel Starter Kits Get Built Automatically with a Single Bash Script

Every push to main builds and deploys 6 production-ready Laravel starter kits to their own GitHub repos automatically. Here is how the whole pipeline works.

Pruthvisinh Rajput·

Introduction

Setting up a new Laravel project always starts the same way — install dependencies, configure PHPStan, set up Pint, wire in Rector, add Pest, write the architecture tests. Every. Single. Time.

pushpak1300 got tired of it. He built a system that generates 6 production-ready Laravel starter kits automatically, pushes each one to its own GitHub repo, and rebuilds everything every night at 2am. I contribute to the repo — adding new features on top of what he built.

One push to main. Six repos updated. Zero manual steps.

Here is how it works.


The Setup

The whole thing lives in a single repository: shipfastlabs/build. It is not a Laravel app — it is a factory that generates them.

shipfastlabs/build
├── build.sh              # The entire pipeline
├── starter-kits.json     # Defines the 6 kits
├── stubs/                # Files copied into every kit
└── .github/workflows/    # GitHub Actions

The 6 kits are Vue, React, and Livewire — each with and without auth:

{
  "starterKits": [
    { "repo": "modern-vue-starter-kit", "options": "--vue --pest --git" },
    { "repo": "modern-vue-starter-kit-auth", "options": "--vue --pest --git --breeze" },
    { "repo": "modern-react-starter-kit", "options": "--react --pest --git" },
    { "repo": "modern-react-starter-kit-auth", "options": "--react --pest --git --breeze" },
    { "repo": "modern-livewire-starter-kit", "options": "--livewire --pest --git" },
    { "repo": "modern-livewire-starter-kit-auth", "options": "--livewire --pest --git --breeze" }
  ]
}

What build.sh Does Per Kit

For each kit, the script runs through 9 steps:

1. Create the Laravel app

laravel new modern-react-starter-kit --react --pest --git --force --no-interaction

2. Install production packages

composer require nunomaduro/essentials

3. Install dev packages

composer require --dev \
  larastan/larastan \
  barryvdh/laravel-ide-helper \
  rector/rector \
  driftingly/rector-laravel \
  nunomaduro/pao \
  pestphp/pest-plugin-type-coverage

nunomaduro/pao is worth calling out — it auto-detects AI agents (Claude Code, Cursor, Devin) and outputs compact JSON instead of human-readable tables. 95% token reduction on every test run.

4. Run artisan commands

php artisan vendor:publish --tag=essentials-config
php artisan essentials:pint --force
php artisan essentials:rector --force
php artisan ide-helper:generate
php artisan ide-helper:models -RW

5. Copy stubs

Everything in stubs/ gets copied verbatim into the generated app — rector.php, pint.json, phpstan.neon, CI workflow, test files. This is how opinionated defaults get baked into every kit.

6. Update .gitignore

AI tool directories get ignored automatically:

.claude/
.cursor/
.idea/
.vscode/
.github/skills/

7. Run Rector + Pint

After stubs are in place, the script runs both tools so the final code is already clean.

8. Inject composer scripts

This is my favourite part. The script patches composer.json directly using jq:

jq '.scripts.lint = ["./vendor/bin/rector", "./vendor/bin/pint --parallel"] |
    .scripts["test:lint"] = ["./vendor/bin/pint --parallel --test", "./vendor/bin/rector --dry-run"] |
    .scripts["test:types"] = "./vendor/bin/phpstan analyse --memory-limit=-1" |
    .scripts["test:type-coverage"] = "pest --type-coverage --min=100" |
    .scripts["test:unit"] = "pest" |
    .scripts.test = ["@test:type-coverage", "@test:unit", "@test:lint", "@test:types"]' \
    composer.json

Every generated kit ships with these commands out of the box:

composer lint               # auto-fix: rector + pint
composer test:lint          # CI-safe: pint --test + rector --dry-run
composer test:types         # PHPStan static analysis
composer test:type-coverage # type coverage (min 100%)
composer test:unit          # Pest tests
composer test               # runs all of the above

9. Deploy to GitHub

gh repo create shipfastlabs/modern-react-starter-kit --public
git remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/shipfastlabs/modern-react-starter-kit.git"
git push -f origin main

Every build force-pushes a single clean "Initial commit" — no history, no noise.


The CI Pipeline

GitHub Actions runs the whole thing on three triggers:

on:
  push:
    branches: [main]
    paths:
      - 'starter-kits.json'
      - 'stubs/**'
      - 'build.sh'
  schedule:
    - cron: '0 2 * * *'   # every night at 2am UTC
  workflow_dispatch:        # manual trigger

The matrix job builds all 6 kits in parallel:

strategy:
  fail-fast: false
  matrix:
    kit: ${{ fromJson(needs.prepare.outputs.matrix) }}

fail-fast: false means one kit failing does not cancel the others.


Using a Kit

laravel new my-project --using=shipfastlabs/modern-react-starter-kit
composer run dev

That is it. PHPStan, Pint, Rector, Pest, type coverage, IDE helper — all pre-configured.


What I Learned

jq is underrated. Patching composer.json with jq instead of maintaining a stub file means the scripts stay in sync with whatever Laravel generates. No merge conflicts, no stale templates.

Stubs are powerful. Dropping files into stubs/ and having them copied verbatim into every kit is the cleanest way to share config. One place to update, six repos get the change.

HTTPS over SSH for CI. GitHub Actions runners have no SSH key. The right pattern for CI git push is:

https://x-access-token:${GITHUB_TOKEN}@github.com/owner/repo.git

Force push is fine for generated repos. Each kit's entire history is one commit. There is nothing to preserve. Force push is not dangerous here — it is the correct tool.


The build repo is private and maintained by pushpak1300 — I contributed to the pipeline as part of the team. The generated kits are public at github.com/shipfastlabs.

Looking for a Laravel developer?

Let's build something great together.