Build Your First GitHub Actions Workflow for Laravel

In the last post I explained what CI/CD is and why you want it. Now we build. By the end of this post you will have a working GitHub Actions workflow that runs automatically every time anyone pushes to your repository.
I'll explain every line. If you've never written YAML before, that's fine — the syntax is simple once you see the pattern.
Step 1: Create the Workflow File
In the root of your Laravel project, create this directory structure:
your-project/
└── .github/
└── workflows/
└── api-ci.ymlThe .github folder must be at the root of your repository (same level as composer.json). GitHub looks for workflow files specifically in .github/workflows/.
Step 2: The Trigger Block
Open api-ci.yml and start with the trigger:
name: API CI
on:
push:
branches: [main, develop]
paths: ['api/**']
pull_request:
branches: [main, develop]
paths: ['api/**']What this does:
name:is just the label shown in the GitHub Actions UIon:defines when the workflow firespush:+branches:— runs when you push to main or developpull_request:— runs when you open or update a PR targeting those branchespaths:— only runs when files insideapi/change (skip it if you have a single Laravel app, not a monorepo)
Step 3: The First Job
defaults:
run:
working-directory: api # remove this if your Laravel app is at the root
jobs:
tests:
name: Tests (PHP 8.4)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP 8.4
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: mbstring, pdo, pdo_mysql, bcmath, gd, zip, intl
coverage: none
tools: composer:v2actions/checkout@v4 downloads your repository code onto the runner. Without it, the VM has an empty disk.
shivammathur/setup-php@v2 is the community-standard action for installing PHP. You specify the version and extensions. The tools: composer:v2 line ensures Composer 2 is installed.
Step 4: Dependency Caching
Without caching, every CI run downloads all your Composer packages from scratch. On a project with 80 dependencies this takes 2–3 minutes. With caching it takes 15 seconds.
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: api/vendor
key: php-8.4-composer-${{ hashFiles('api/composer.lock') }}
restore-keys: php-8.4-composer-The key includes a hash of composer.lock. When your lock file changes (you added a package), the cache key changes and CI downloads fresh dependencies. When nothing changed, it restores the cached vendor/ folder.
Step 5: Install and Test
- name: Install dependencies
run: composer install --no-interaction --prefer-dist --no-progress
- name: Copy environment file
run: cp .env.example .env
- name: Generate app key
run: php artisan key:generate
- name: Run tests
run: ./vendor/bin/pest--no-interaction prevents Composer from asking questions. --prefer-dist downloads zip archives instead of cloning git repos (faster). --no-progress keeps the logs clean.
The cp .env.example .env step is important: Laravel won't boot without an .env file. Your .env.example must have sensible defaults for all values the app reads at startup.
Step 6: Push It
git add .github/workflows/api-ci.yml
git commit -m "ci: add initial GitHub Actions workflow"
git push origin mainGo to your repository on GitHub, click Actions. You'll see the workflow appear and start running. Click into it and watch the steps execute in real time.
If it goes green — congratulations. You have CI. Every future push will run this automatically.
Common First-Run Failures
"Process completed with exit code 1" on key:generate
Your .env.example is missing APP_KEY=. Add APP_KEY= (empty value) and key:generate will fill it.
"Class not found" errors in tests
Run composer dump-autoload locally and commit the result. CI runs composer install which includes autoload generation, but if your composer.json has stale paths this surfaces here.
Tests passing locally but failing in CI
Your tests depend on something that only exists on your machine — a database, a cache entry, or an environment variable. We fix this properly in Post 3 (MySQL) and Post 5 (secrets).
In the next post we add a MySQL service container so your tests run against a real database instead of SQLite.
Frequently Asked Questions
Where does a GitHub Actions workflow file go?
Why does php artisan key:generate fail in CI?
Senior Full Stack Developer · Building SaaS products & teaching Laravel/React · 10+ years experience · Founder of Orion360 · Based in Dubai, UAE.
Was this post helpful?
Reviews & Ratings
Sign in to leave a review.