DEV Community

Cover image for Build Your First GitHub Actions Workflow for Laravel
Dinesh Wijethunga
Dinesh Wijethunga

Posted on • Originally published at dineshstack.com

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.yml

The .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 UI
  • on: defines when the workflow fires
  • push: + branches: — runs when you push to main or develop
  • pull_request: — runs when you open or update a PR targeting those branches
  • paths: — only runs when files inside api/ 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:v2

actions/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 main

Go 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.


Originally published at dineshstack.com — read the full version with code samples and updates there.

Top comments (0)