Debug School

rakesh kumar
rakesh kumar

Posted on

Laravel CI/CD with GitHub Actions: A Beginner’s Guide to Every YAML Line

Learn what on, jobs, steps, test, and deploy mean, how automatic Laravel testing works, and how a passing commit reaches a staging server.

Quick Comparison: Build vs Test vs Deploy

Area Build Test Deploy
Main question Can we prepare usable application output? Does the application behave as expected? Can we release this version to an environment?
Purpose Transform/package source into runnable or distributable output Verify expected behavior using checks and assertions Install or activate a release on staging or production
Laravel example npm run build for Vite assets; release packaging can also prepare PHP dependencies php artisan test SSH commands, release switch, runtime refresh
Input Source, configuration, dependency lock files Code, fixtures, test definitions and dependencies Selected source commit or prepared artifact
Output Compiled assets, bundles, image or release package Pass/fail results and optional reports Updated running environment
Typical failure Missing import, bundler error, incompatible build dependency Unexpected response, broken validation, failed assertion Authentication, permissions, installation or health-check failure
What success does not prove Login or business rules work correctly Deployment and untested behavior will work Every user journey is correct
Runs in CI? Commonly yes Yes Usually a later delivery/deployment stage
Requires a live server? Usually no Unit tests do not; integration/UI checks may need services Yes, or another deployment platform

Build prepares the output. Test checks behavior. Deploy makes a version available. A successful build can still contain bugs. Passing backend tests can still leave a broken frontend build.

PHP applications do not normally compile all PHP source to a standalone executable in this workflow. In the three examples below, build specifically means the Laravel Vite frontend asset build. Composer installation prepares dependencies; it does not replace functional tests.

Which Example Should You Choose?

File What it does What it intentionally leaves for other workflows
01-test-only.yml Installs PHP dependencies and runs Laravel tests Frontend compilation and deployment
02-build-only.yml Installs npm dependencies, builds frontend assets, uploads the output Backend tests and deployment
03-build-and-test.yml Prepares Laravel, builds assets, runs backend tests, then uploads assets Deployment and browser-level UI tests

Choose one of these as your starting workflow rather than enabling all three and duplicating work. They are alternative examples. None connects to a server or needs SSH secrets.

1. What Is the Purpose of This Workflow?

This workflow performs two major tasks:

  1. Continuous Integration (CI): Download the application, prepare PHP and Laravel, and run automated tests.
  2. Continuous Deployment to staging (CD): After a push to main passes its tests, connect to the staging server, deploy that commit, and check the website.

Staging is a separate environment for checking changes before production. This workflow does not deploy to production.

Situation Tests run? Staging deployment?
Pull request targeting main Yes No
Push directly to main Yes Only after tests pass
Pull request merged into main Yes, again on the resulting push Only after tests pass
Push to a feature branch without a qualifying pull request No, under these triggers No
Tests fail Yes, but fail No

A workflow is an automation definition. One execution of it is a workflow run.

2. Where Should the YAML File Be Saved?

Create this file inside your Git repository:

.github/workflows/laravel-staging.yml
Enter fullscreen mode Exit fullscreen mode

GitHub Actions reads workflow files from .github/workflows/. Both .yml and .yaml extensions are supported.

3. YAML Basics Before You Start

YAML is a configuration format. GitHub reads its settings, then runs the referenced actions and shell commands.

Syntax Role Example
key: value A setting and its value APP_ENV: testing
Indentation Shows parent-child relationships steps nested inside a job
- An item in a list - name: Run tests
[main] A list with one value Branch filter
`\ ` Preserves a multiline text block
# A comment # Stop if tracked files changed
${{ ... }} GitHub expression ${{ github.sha }}
$VARIABLE Shell environment-variable expansion $SSH_HOST
$(command) Shell command substitution $(git status ...)
\ at the end of a shell line Continues the command Multiline Composer command

Use spaces, not tabs, for indentation. The backslash used in shell commands is different from YAML indentation.

4. The Complete Example Workflow

The following is the original example. Replace its example server path and domain before using it. Its action version tags are retained for explanation; check supported versions and your project's requirements when adopting it.

name: Laravel CI and Staging Deployment

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

permissions:
  contents: read

jobs:
  test:
    name: Laravel tests
    runs-on: ubuntu-latest

    env:
      APP_ENV: testing
      DB_CONNECTION: sqlite
      DB_DATABASE: ":memory:"
      CACHE_STORE: array
      CACHE_DRIVER: array
      SESSION_DRIVER: array
      QUEUE_CONNECTION: sync

    steps:
      - name: Download repository
        uses: actions/checkout@v4

      - name: Set up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: "8.3"
          extensions: mbstring, pdo_sqlite, sqlite3
          tools: composer:v2
          coverage: none

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

      - name: Prepare test environment
        run: |
          cp .env.example .env
          php artisan key:generate

      - name: Run tests
        run: php artisan test

  deploy:
    name: Deploy staging
    needs: test
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging

    concurrency:
      group: staging-deployment
      cancel-in-progress: false

    env:
      SSH_HOST: ${{ secrets.SSH_HOST }}
      SSH_USER: ${{ secrets.SSH_USER }}
      SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
      SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
      DEPLOY_SHA: ${{ github.sha }}

    steps:
      - name: Configure SSH
        run: |
          install -m 700 -d ~/.ssh
          printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          printf '%s\n' "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
          chmod 600 ~/.ssh/known_hosts

      - name: Deploy tested commit
        run: |
          ssh -o BatchMode=yes "$SSH_USER@$SSH_HOST" \
            "bash -s -- '$DEPLOY_SHA'" <<'REMOTE'
          set -euo pipefail

          cd /var/www/example-app

          # Stop if tracked files have manual server changes.
          test -z "$(git status --porcelain --untracked-files=no)"

          git fetch origin main
          git checkout --detach "$1"

          composer install \
            --no-dev \
            --no-interaction \
            --prefer-dist \
            --optimize-autoloader

          php artisan config:cache
          php artisan view:cache
          REMOTE

      - name: Check deployed website
        run: |
          curl --fail --silent --show-error \
            --retry 3 --retry-delay 5 \
            --max-time 20 \
            https://staging.example.com/health

      - name: Remove SSH private key
        if: always()
        run: rm -f ~/.ssh/id_ed25519
Enter fullscreen mode Exit fullscreen mode

5. Understand the Main Keywords

Keyword Role Simple question it answers
name Human-readable label What should this be called?
on Event triggers When should the workflow start?
permissions Automatic GitHub token access What GitHub access is allowed?
jobs Collection of major tasks What work groups exist?
test A user-chosen job ID Which job performs tests?
deploy A user-chosen job ID Which job performs deployment?
runs-on Runner selection Which machine runs the job?
env Environment variables What settings do commands receive?
steps Tasks within a job What actions happen in this job?
uses Invoke a reusable action Which existing tool should run?
with Action input settings How should that action be configured?
run Execute shell commands What commands should run?
needs Job dependency Which job must succeed first?
if Conditional execution Should this job or step run?
environment GitHub deployment environment Which deployment rules and secrets apply?
concurrency Overlap control Can deployments run simultaneously?

test and deploy are not reserved GitHub keywords. You choose these IDs. If you rename test to laravel_tests, update needs: test to needs: laravel_tests.

Steps in this example execute in order. Jobs can otherwise run independently, so needs is essential here.

6. Workflow Name

name: Laravel CI and Staging Deployment
Enter fullscreen mode Exit fullscreen mode

This name appears in GitHub's Actions tab. It describes the workflow without changing its behavior.

7. Triggers: Why Use on?

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]
Enter fullscreen mode Exit fullscreen mode

Pull Request Trigger

pull_request runs for default activities such as opening, reopening, and updating a pull request. The branch filter refers to the target branch, not the feature branch.

Example: a pull request from feature-login into main runs tests.

Push Trigger

push runs when commits are pushed to main. Merging a pull request normally creates a push event on main, so tests run again on the resulting branch state.

Why Test Both?

Pull-request tests provide feedback before merging. Push tests check the actual commit reaching main before staging deployment.

For a pull-request run, checkout normally tests GitHub's pull-request merge reference. For the deployment-eligible push run, github.sha identifies the pushed commit being tested and deployed.

8. Repository Permissions

permissions:
  contents: read
Enter fullscreen mode Exit fullscreen mode

GitHub provides a temporary GITHUB_TOKEN to workflows. This setting grants read access to repository contents, sufficient for checkout without granting code-write access.

It does not grant SSH access to staging or configure the server's access to GitHub. Those are separate credentials.

9. The Test Job and Its Runner

jobs:
  test:
    name: Laravel tests
    runs-on: ubuntu-latest
Enter fullscreen mode Exit fullscreen mode
  • jobs begins the collection of jobs.
  • test identifies this job internally.
  • name is its visible label.
  • runs-on selects a GitHub-hosted Ubuntu runner.

A runner is the machine executing a job. GitHub-hosted jobs use fresh environments; the test job and deploy job do not share their installed PHP, files, or dependencies automatically.

ubuntu-latest is a moving runner image label. An explicit supported Ubuntu version can reduce unexpected operating-system changes, though its installed software still receives updates.

10. Laravel Test Environment Variables

env:
  APP_ENV: testing
  DB_CONNECTION: sqlite
  DB_DATABASE: ":memory:"
  CACHE_STORE: array
  CACHE_DRIVER: array
  SESSION_DRIVER: array
  QUEUE_CONNECTION: sync
Enter fullscreen mode Exit fullscreen mode
Variable Role Reason
APP_ENV: testing Select testing environment Distinguish tests from normal application execution
DB_CONNECTION: sqlite Use SQLite Avoid requiring a MySQL service for this test job
DB_DATABASE: ":memory:" Use an in-memory database Keep test database data temporary
CACHE_STORE: array Select array cache in projects using this configuration variable Avoid an external cache service
CACHE_DRIVER: array Support projects using the older configuration name Compatibility with existing Laravel configuration
SESSION_DRIVER: array Use temporary session storage Avoid persistent session files or database tables
QUEUE_CONNECTION: sync Execute queued work immediately Avoid starting a separate queue worker for these tests

These variables apply to the test job, not the remote staging application.

Details That Are Easy to Miss

An in-memory database does not create tables by itself. Tests must prepare the schema, commonly through Laravel's RefreshDatabase trait and migrations.

Review phpunit.xml, .env.testing, and cached configuration if tests use unexpected settings. Do not commit generated configuration caches or real .env secrets.

SQLite and MySQL have differences in SQL syntax, types, constraints, and behavior. Add tests using the real database engine when database compatibility matters.

A synchronous queue test also does not reproduce all behavior of a real background worker.

11. Download the Repository

steps:
  - name: Download repository
    uses: actions/checkout@v4
Enter fullscreen mode Exit fullscreen mode

steps begins the ordered list. uses invokes a reusable action. Checkout puts repository files into the runner's workspace so later commands can access composer.json, artisan, and tests.

@v4 selects an action version tag; it is not a PHP version.

12. Set Up PHP and Composer

- name: Set up PHP
  uses: shivammathur/setup-php@v2
  with:
    php-version: "8.3"
    extensions: mbstring, pdo_sqlite, sqlite3
    tools: composer:v2
    coverage: none
Enter fullscreen mode Exit fullscreen mode
Setting Explanation
shivammathur/setup-php@v2 PHP setup action, major version 2
with Inputs passed to the action
php-version: "8.3" Requested PHP version for testing
mbstring Multibyte text processing
pdo_sqlite SQLite access through PDO
sqlite3 PHP SQLite3 extension
tools: composer:v2 Install Composer 2
coverage: none Disable code coverage tooling for this run

Choose PHP and extensions compatible with your application's Composer requirements. Match staging closely enough that tests represent the deployed runtime.

This setup does not install PHP on staging.

13. Install Test Dependencies

- name: Install dependencies
  run: composer install --no-interaction --prefer-dist --no-progress
Enter fullscreen mode Exit fullscreen mode
Command or flag Purpose
composer install Install dependencies from the lock file when present
--no-interaction Avoid interactive questions
--prefer-dist Prefer packaged downloads
--no-progress Keep logs readable

Commit composer.lock for a Laravel application so CI and staging use the intended dependency versions. composer update changes dependency resolution and is not a substitute for a reproducible deployment install.

There is no --no-dev here because test tools usually belong to development dependencies.

Composer scripts may run application commands during installation. If those scripts need additional settings, configure them before this step.

14. Prepare the Laravel Test Application

- name: Prepare test environment
  run: |
    cp .env.example .env
    php artisan key:generate
Enter fullscreen mode Exit fullscreen mode

run: | supplies a multiline shell script.

cp .env.example .env creates a test-runner environment file. php artisan key:generate creates a temporary application encryption key in that environment.

This does not change staging's .env or application key. Never regenerate the persistent staging or production key casually: doing so can break access to existing encrypted data.

15. Execute Tests

- name: Run tests
  run: php artisan test
Enter fullscreen mode Exit fullscreen mode

Laravel runs the configured test suite. Common locations are tests/Unit and tests/Feature.

Possible checks include validation, authentication, API responses, and database behavior—but only when tests for them exist.

A passing command is evidence about the written test suite. It does not automatically cover the whole website, all integrations, or security vulnerabilities.

If tests fail, the test job fails and deployment is skipped.

16. Deployment Rules

deploy:
  name: Deploy staging
  needs: test
  if: github.event_name == 'push' && github.ref == 'refs/heads/main'
  runs-on: ubuntu-latest
  environment: staging
Enter fullscreen mode Exit fullscreen mode
Line Meaning
deploy Job ID
name Display label
needs: test Require successful test completion
github.event_name == 'push' Require a push event
&& Require both conditions
github.ref == 'refs/heads/main' Require the main branch reference
runs-on Start a separate Ubuntu runner
environment: staging Apply the named GitHub environment's settings

Does environment: staging Create a Server?

No. It associates the job with a GitHub deployment environment. It can provide environment-scoped secrets and configured protection rules, including reviewer approval where supported and enabled.

The server, domain, database, and application must already exist.

Why Is There No Checkout in the Deployment Job?

This job sends commands over SSH. The server already has the repository, and the remote script fetches the required code. The runner does not use a local application checkout for this deployment method.

17. Avoid Simultaneous Deployments

concurrency:
  group: staging-deployment
  cancel-in-progress: false
Enter fullscreen mode Exit fullscreen mode

The shared group prevents jobs in that group from deploying simultaneously. Setting cancel-in-progress: false avoids interrupting a deployment already running when another qualifying run arrives.

Do not treat this as a guarantee that every intermediate commit will deploy or that commit order is enforced. Pending-run behavior depends on GitHub's concurrency rules. A delayed older run can also be an issue if you need staging always to represent the newest branch state; add an explicit deployment-order policy for that requirement.

All workflows deploying to this same target should coordinate through the same group. Manual server deployments bypass GitHub's group, so a server-side lock can provide additional coordination.

18. Secrets and the Deployment Commit

env:
  SSH_HOST: ${{ secrets.SSH_HOST }}
  SSH_USER: ${{ secrets.SSH_USER }}
  SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
  SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
  DEPLOY_SHA: ${{ github.sha }}
Enter fullscreen mode Exit fullscreen mode
Name Example or expected content
SSH_HOST Your staging hostname or IP
SSH_USER A dedicated deployment account such as deploy
SSH_PRIVATE_KEY Complete multiline private key
SSH_KNOWN_HOSTS Verified SSH host-key entries for that server
DEPLOY_SHA GitHub-supplied commit identifier

The ${{ ... }} expressions are resolved by GitHub. Later shell commands use the resulting environment variables through $SSH_HOST, $DEPLOY_SHA, and similar names.

Use a dedicated deployment account with only the permissions it needs. Keep credentials out of the repository, screenshots, and logs.

Two Separate Authentication Connections

Connection Required access
GitHub runner → staging server Private key on the runner; matching public key authorized on staging
Staging server → Git repository Server-side repository credentials, if the repository is private

Granting the first connection does not automatically grant the second.

19. Configure SSH Files

install -m 700 -d ~/.ssh
printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
printf '%s\n' "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
Enter fullscreen mode Exit fullscreen mode
Line Explanation
install -m 700 -d ~/.ssh Create the directory; only its owner can access it
First printf Write the private key while preserving its multiline content
chmod 600 on the private key Restrict reading and writing to its owner
Second printf Write trusted server host keys
chmod 600 on known hosts Restrict access to that file

> writes or replaces a file. %s\n prints the supplied string followed by a newline.

The example assumes a key usable without an interactive passphrase prompt. A passphrase-protected key needs an appropriate noninteractive agent arrangement.

Verify the server host-key fingerprint through a trusted channel before storing SSH_KNOWN_HOSTS. Collecting a key with ssh-keyscan alone does not establish that it belongs to the intended server. Update this secret deliberately if the server key changes.

The SSH command uses default port 22. A different port requires matching SSH options and known-hosts entries.

20. Connect to Staging and Send a Script

ssh -o BatchMode=yes "$SSH_USER@$SSH_HOST" \
  "bash -s -- '$DEPLOY_SHA'" <<'REMOTE'
Enter fullscreen mode Exit fullscreen mode
Part Role
ssh Connect to the remote server
-o BatchMode=yes Fail rather than waiting for interactive authentication
"$SSH_USER@$SSH_HOST" Select account and host
\ Continue the command on the next line
bash -s Execute a script received through standard input
-- End Bash option parsing
'$DEPLOY_SHA' inside the command string Pass the commit SHA as a script argument
<<'REMOTE' Begin a quoted here-document

The here-document ends at the closing REMOTE line. Its quoted delimiter prevents the runner's shell from expanding the script body. Remote Bash expands expressions such as $1 on the server.

After YAML removes the block's indentation, the closing delimiter must be positioned correctly for the shell. Keep the example's indentation intact.

21. Make Remote Errors Stop Deployment

set -euo pipefail
Enter fullscreen mode Exit fullscreen mode
Option Meaning
-e Exit on unhandled command failures, subject to Bash's conditional-command rules
-u Treat unset variables as errors
pipefail Fail a pipeline if a component command fails

These settings improve failure handling. They do not undo commands already completed.

22. Select and Check the Application Directory

cd /var/www/example-app

# Stop if tracked files have manual server changes.
test -z "$(git status --porcelain --untracked-files=no)"
Enter fullscreen mode Exit fullscreen mode

cd enters the existing application repository. Replace /var/www/example-app with the verified application path.

The second command checks whether Git reports tracked-file changes:

  • git status --porcelain produces machine-readable output.
  • --untracked-files=no excludes untracked files.
  • $(...) captures the output.
  • test -z succeeds if the output is empty.

Tracked changes cause deployment to stop instead of overwriting manual edits.

This ignores untracked files and does not guarantee a conflict-free checkout. Keep .env and persistent storage managed separately from tracked application code. Do not resolve a dirty checkout by blindly deleting server changes; review them first.

23. Deploy the Specific Tested Commit

git fetch origin main
git checkout --detach "$1"
Enter fullscreen mode Exit fullscreen mode

git fetch downloads repository updates. git checkout --detach "$1" selects the commit passed as the first script argument.

Here, $1 contains DEPLOY_SHA.

Detached HEAD means the checkout points directly to a commit rather than a local branch. That is intentional for selecting an exact release commit.

The commit must be available to the server's repository. Shallow clones, rewritten history, or unusual fetch configuration may require additional handling. A checkout failure stops this script.

24. Install Server Dependencies

composer install \
  --no-dev \
  --no-interaction \
  --prefer-dist \
  --optimize-autoloader
Enter fullscreen mode Exit fullscreen mode
Flag Purpose
--no-dev Exclude development packages
--no-interaction Avoid prompts
--prefer-dist Prefer packaged downloads
--optimize-autoloader Optimize Composer's autoload information

The staging server needs compatible PHP, required PHP extensions, Composer, repository access, and package-download access.

The remote .env remains on the server. This workflow does not copy CI's generated .env to staging.

Because dependencies are installed again on the server, this is a source-based deployment rather than deployment of a single prebuilt artifact. Lock files improve consistency, but runtime differences and installation scripts still matter.

25. Cache Laravel Configuration and Views

php artisan config:cache
php artisan view:cache
REMOTE
Enter fullscreen mode Exit fullscreen mode

config:cache creates cached configuration. view:cache precompiles Blade templates. REMOTE closes the SSH script; it is not a Laravel command.

After configuration caching, use config() in application code and keep env() calls inside configuration files. Rebuild configuration caches after changing environment configuration.

Laravel also needs appropriate write access to storage and bootstrap/cache. Do not use broad world-writable permissions as a routine solution.

26. Check Website Health

curl --fail --silent --show-error \
  --retry 3 --retry-delay 5 \
  --max-time 20 \
  https://staging.example.com/health
Enter fullscreen mode Exit fullscreen mode
Option Role
--fail Fail for HTTP error responses
--silent Hide progress output
--show-error Display errors despite silent mode
--retry 3 Retry eligible transient failures up to three times
--retry-delay 5 Wait five seconds between retries
--max-time 20 Limit each attempt to 20 seconds

Replace the URL and ensure the endpoint exists. Some Laravel applications use /up; others define a custom route. Confirm the actual endpoint rather than assuming /health is built in.

What Does This Check Prove?

Only what the endpoint verifies. Consider checking basic application boot, required database connectivity, and critical dependencies without exposing credentials or internal diagnostics publicly.

This command does not inspect the response body, require exactly status 200, follow redirects, or verify the deployed commit. For stronger verification, deliberately check the expected status and release identifier. curl --fail alone can treat a redirect response as successful.

A failing health check marks the job failed but does not roll back the application.

27. Remove the SSH Private Key

- name: Remove SSH private key
  if: always()
  run: rm -f ~/.ssh/id_ed25519
Enter fullscreen mode Exit fullscreen mode

if: always() attempts cleanup even when an earlier step fails. rm -f removes the key file without prompting and tolerates a missing file.

This is cleanup on the runner, not deletion of server credentials. It is not a guarantee of execution after abrupt runner loss.

28. Full Workflow Flow

Flow A: Pull Request

  1. A developer opens or updates a pull request targeting main.
  2. GitHub starts the test runner.
  3. Checkout downloads the code.
  4. PHP and Composer are configured.
  5. Dependencies are installed.
  6. Laravel's temporary test environment is prepared.
  7. Tests execute.
  8. GitHub shows success or failure.
  9. Deployment is skipped because this is a pull-request event.

Flow B: Push or Merge to main

  1. GitHub receives a push to main.
  2. Tests run again for that push.
  3. Failure prevents deployment.
  4. Success allows the deployment job to proceed, subject to environment rules and concurrency.
  5. The runner prepares SSH credentials.
  6. SSH connects to staging.
  7. The server checks for tracked changes.
  8. Git fetches updates and selects the specified SHA.
  9. Composer installs server dependencies.
  10. Laravel rebuilds its configuration and view caches.
  11. The runner requests the health endpoint.
  12. GitHub reports the result and attempts SSH-key cleanup.

29. Setup Checklist Before Your First Run

Repository

  • Commit the workflow under .github/workflows/.
  • Commit composer.lock and a safe .env.example.
  • Confirm that the test command works locally.
  • Check the selected PHP version and extensions against Composer requirements.
  • Keep credentials, real .env files, and generated caches out of Git.

GitHub

  • Enable Actions for the repository.
  • Configure the staging environment and any desired protection rules.
  • Add SSH_HOST, SSH_USER, SSH_PRIVATE_KEY, and SSH_KNOWN_HOSTS as suitable repository or environment secrets.
  • Require the test check through a branch protection rule or ruleset if failed tests must block merging. A workflow alone does not enforce that policy.

Staging Server

  • Prepare a separate staging application and database.
  • Clone the application into the verified directory.
  • Install compatible PHP, extensions, Composer, and Git.
  • Configure repository authentication for the server.
  • Create the server's .env and persistent application key.
  • Configure Apache or Nginx to serve the Laravel application's public directory, not its repository root.
  • Authorize the deployment public key and set correct directory ownership.
  • Verify HTTPS, the health endpoint, and network access from the runner.

A private staging network may require a suitable self-hosted runner or an explicitly configured secure network connection.

30. What Is Missing from This Basic Workflow?

Missing capability Why it matters Possible addition
Frontend build Vite assets may be outdated or missing Install the correct Node version, run npm ci, then the project's build command
Database migrations New code may require schema changes Reviewed migration step such as php artisan migrate --force, with compatibility and backup planning
Worker refresh Long-running workers may keep old code Restart queue workers or use the appropriate process manager command
Other runtime refresh Octane or similar services can retain old code Application-specific reload procedure
Backups Deployment can affect data or configuration Tested database and persistent-file backup process
Automatic rollback Failed health checks leave changes deployed Release directories, a previous-release pointer, and rollback logic
Atomic release switch In-place changes can expose partial updates Prepare a new release, verify it, then switch a symlink
Integration checks SQLite tests may miss real-service failures Database-engine and cross-service tests
Security checks Functional tests do not cover all vulnerabilities Dependency auditing, static analysis, and appropriate scanning
Deployment identity verification A healthy response may come from the wrong release Compare the expected commit/release ID
Deployment notification Failures may go unnoticed Configure appropriate alerts

Add only the features the application needs. Commands for migrations, worker restarts, and runtime reloads should reflect the actual architecture.

Important Limitation: In-Place Deployment

The example modifies the serving application directory directly. If Composer or cache generation fails after checkout, the server can be left partially updated. Visitors may also encounter code and dependencies changing at different times.

For more reliable deployments, prepare separate release directories, share persistent configuration and storage appropriately, verify the new release, and switch the active release atomically. Database rollback still requires a separate plan; switching code back does not reverse schema or data changes.

31. Common Errors and What to Check

Error or symptom Likely checks
Workflow never starts File path, YAML indentation, enabled Actions, branch filters
Deployment skipped Event is a pull request, tests failed, or push was not to main
Deployment waits Environment approval/protection rules or concurrency
Permission denied (publickey) SSH username, private key formatting, public-key authorization, file permissions
Host key verification failed Correct verified host key, hostname, port, or legitimate server-key change
Remote git fetch fails Server repository credentials, remote URL, network access
Dirty checkout stops deployment Inspect tracked server changes before proceeding
composer or php not found Noninteractive SSH PATH and server installations
Composer platform requirement failure PHP version, extensions, and lock-file compatibility
SQLite table missing Test migrations and database reset setup
Unexpected test configuration phpunit.xml, .env.testing, environment variables, cached configuration
Permission error writing Laravel caches Ownership and permissions for storage and bootstrap/cache
Health check fails URL, route, HTTPS, application logs, database and dependency availability
Health check passes but release is wrong Add explicit status/body and release-identifier verification

32. Beginner Questions

Is YAML the Same as Bash?

No. YAML describes the workflow structure. Bash executes the commands supplied under run and in the remote script.

Are test and deploy Mandatory Names?

No. They are job IDs chosen by the author. needs must refer to the actual ID you use.

Does This Workflow Write Tests for Me?

No. It runs the tests already present in your project.

Will Failed Tests Always Stop a Pull Request from Merging?

No. Configure branch protection or repository rules requiring the relevant status check.

Does runs-on: ubuntu-latest Mean My Server Must Use Ubuntu?

No. It selects GitHub's runner. The remote server must support the commands and software used by the deployment script.

Does CI's PHP Setup Update Server PHP?

No. Server software is managed separately.

Why Use a Commit SHA Instead of git pull?

The SHA selects the particular commit tested by the push run. A later git pull could deploy a newer commit than the one that passed that run's tests.

Is This Continuous Delivery or Continuous Deployment?

Automatic deployment to staging is continuous deployment to that environment. A pipeline that prepares a release and waits for approval before deploying is commonly described as continuous delivery. The same project can deploy automatically to staging and require approval for production.

Does a Green Workflow Guarantee a Secure and Bug-Free Website?

No. It means the configured commands and checks succeeded. Confidence depends on the coverage and quality of those checks.

33. CI and CD Responsibilities at a Glance

Area Main role Example here
CI Check whether changes pass automated verification php artisan test
CD to staging Deliver a passing version to the staging environment SSH, commit checkout, Composer, cache commands
Deployment verification Check the running application after deployment HTTP health request
Operational monitoring Watch the application continuously Not included in this workflow
Security monitoring Detect suspicious activity and operational threats Not included in this workflow

CI/CD runs around code changes. Continuous uptime monitoring and server-security monitoring require additional systems or scheduled checks.

34. Official References

Consult the documentation matching your Laravel version and the action versions you adopt.

The complete example is an educational starting point. Adapt the paths, domains, runtime requirements, written tests, and release procedures to the application before using it for a real deployment.

35. Three Complete YAML Examples

Requirements Shared by These Examples

Requirement Applies to Detail
Laravel source and composer.lock Test only; build and test PHP 8.3 is illustrative; choose the version required by the project
package.json with a build script Build only; build and test Standard example: "build": "vite build"
Committed package-lock.json Build only; build and test npm ci requires a compatible npm lock file
Supported Node.js version Build only; build and test Node 22 is illustrative; match package engine requirements and the team's supported runtime
Vite output directory Build only; build and test These examples assume public/build/; change it for custom output settings
SQLite-capable tests Test only; build and test Tests must create the database schema; external services require additional configuration
GitHub.com Actions All Artifact action availability differs on GitHub Enterprise Server

The original workflow retains its action tags. The new examples use setup-node@v7 from the current official examples and upload-artifact@v4. Treat all action tags as explicit dependencies, review their supported runtimes, and consider pinning reviewed full commit SHAs with an update process.

Example 1: Test Only

Save as .github/workflows/01-test-only.yml.

This checks Laravel behavior through the project's existing test suite. It does not run npm or prepare real frontend output. If Blade-rendering tests require a Vite manifest, use the combined workflow, or deliberately disable Vite only in tests that are not intended to validate asset integration.

name: Laravel - Test Only

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

permissions:
  contents: read

jobs:
  test:
    name: Laravel tests
    runs-on: ubuntu-latest

    env:
      APP_ENV: testing
      DB_CONNECTION: sqlite
      DB_DATABASE: ":memory:"
      CACHE_STORE: array
      CACHE_DRIVER: array
      SESSION_DRIVER: array
      QUEUE_CONNECTION: sync

    steps:
      - name: Download repository
        uses: actions/checkout@v4

      - name: Set up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: "8.3"
          extensions: mbstring, pdo_sqlite, sqlite3
          tools: composer:v2
          coverage: none

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

      - name: Prepare test environment
        run: |
          cp .env.example .env
          php artisan key:generate

      - name: Run tests
        run: php artisan test

Enter fullscreen mode Exit fullscreen mode

Test-Only Flow in a Table

Order Step Why it exists
1 Checkout Obtain application code
2 PHP setup Provide compatible PHP, SQLite extensions and Composer
3 Composer install Install application and testing dependencies
4 Copy example environment Supply a temporary Laravel environment file
5 Generate application key Prepare encryption for this test environment
6 Run tests Execute assertions and report pass/fail

Example 2: Build Only

Save as .github/workflows/02-build-only.yml.

This is a frontend build, not a complete deployable Laravel release package. It does not install PHP dependencies. It suits a Vite configuration that can build using only the npm project. If your custom frontend build invokes Artisan or reads vendor/, add the PHP and Composer preparation it requires.

name: Laravel - Build Only

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

permissions:
  contents: read

jobs:
  build:
    name: Build frontend assets
    runs-on: ubuntu-latest
    steps:
      - name: Download repository
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v7
        with:
          node-version: "22"
          cache: npm

      - name: Install frontend dependencies
        run: npm ci

      - name: Build frontend assets
        run: npm run build

      - name: Save frontend build output
        uses: actions/upload-artifact@v4
        with:
          name: frontend-assets-${{ github.sha }}
          path: public/build/
          if-no-files-found: error
          retention-days: 7
Enter fullscreen mode Exit fullscreen mode

Build-Only Lines Explained

YAML line or command Purpose Important detail
build: Chosen job ID The word itself does not execute a build
uses: actions/setup-node@v7 Configure Node and npm Match the Node version to project dependencies
node-version: "22" Select Node major version Separate from the action's own version
cache: npm Cache npm's package-download data Does not replace npm ci or cache the whole application
npm ci Install locked npm dependencies Fails when package metadata and lock file disagree
npm run build Execute the project's build script Usually Vite for this Laravel example; inspect package.json
actions/upload-artifact@v4 Preserve output for download or later jobs Uploading does not deploy it
name: frontend-assets-${{ github.sha }} Identify output by source commit Helps connect files to the workflow commit
path: public/build/ Select uploaded files Must match your build configuration
if-no-files-found: error Fail if no expected output exists Avoids a misleading successful upload step
retention-days: 7 Request seven-day artifact retention Repository/organization limits still apply

Build flow: checkout, configure Node, install locked dependencies, build assets, save assets.

Example 3: Build and Test Together

Save as .github/workflows/03-build-and-test.yml.

This example uses one job with sequential steps. It builds frontend assets before Laravel tests, so tests rendering Vite-enabled pages can find the generated manifest. Tests and frontend output refer to the same checkout.

name: Laravel - Build and Test

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

permissions:
  contents: read

jobs:
  build_and_test:
    name: Build frontend and test Laravel
    runs-on: ubuntu-latest

    env:
      APP_ENV: testing
      DB_CONNECTION: sqlite
      DB_DATABASE: ":memory:"
      CACHE_STORE: array
      CACHE_DRIVER: array
      SESSION_DRIVER: array
      QUEUE_CONNECTION: sync

    steps:
      - name: Download repository
        uses: actions/checkout@v4

      - name: Set up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: "8.3"
          extensions: mbstring, pdo_sqlite, sqlite3
          tools: composer:v2
          coverage: none

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

      - name: Prepare test environment
        run: |
          cp .env.example .env
          php artisan key:generate

      - name: Set up Node.js
        uses: actions/setup-node@v7
        with:
          node-version: "22"
          cache: npm

      - name: Install frontend dependencies
        run: npm ci

      - name: Build frontend assets
        run: npm run build

      - name: Run tests
        run: php artisan test


      - name: Save frontend build output
        uses: actions/upload-artifact@v4
        with:
          name: frontend-assets-${{ github.sha }}
          path: public/build/
          if-no-files-found: error
          retention-days: 7
Enter fullscreen mode Exit fullscreen mode

Combined Flow in a Table

Order Operation If it fails
1 Checkout Stop; no source to process
2 Set up PHP Stop; runtime unavailable
3 Install Composer dependencies Stop; application/tests cannot run reliably
4 Prepare Laravel environment and key Stop; test application setup failed
5 Set up Node Stop; frontend runtime unavailable
6 Run npm ci Stop; frontend dependencies not installed
7 Run npm run build Stop; do not run later tests or upload assets
8 Run php artisan test Stop; do not publish the workflow's asset artifact
9 Upload public/build/ Preserve assets only after all preceding steps succeed

This does not execute JavaScript unit tests or browser tests. If those suites exist, add their real project commands, for example an appropriate noninteractive npm test command. Do not assume a test script exists merely because the project uses Vite.

Why Not Use Two Separate Jobs Here?

Arrangement Benefit Required care
One job: build, then test Easy to understand; files remain available between steps Everything runs sequentially
Independent build and test jobs Can run concurrently Each fresh runner needs its own setup; tests cannot see build output automatically
Build job, then test job with needs: build Enforces ordering Transfer needed assets using upload/download artifacts; needs alone does not share files

36. How to Connect the Combined Checks to Staging Deployment

The three files above are CI examples. The earlier SSH example supplies the staging CD portion.

Change Purpose
Add the original deploy job under jobs in the combined workflow Add staging deployment after CI
Change needs: test to needs: build_and_test Match the combined job's actual ID
Keep the push-to-main condition Prevent pull-request deployments
Configure staging secrets and server prerequisites Enable authenticated deployment
Decide how frontend output reaches staging CI artifacts remain on GitHub unless explicitly transferred
Verify the selected release after deployment Confirm both health and expected version

Copying the original deploy job alone does not deploy public/build/. Its Git checkout and Composer commands do not fetch CI-generated assets. Choose a deliberate approach:

Approach How it works Trade-off
Build on staging Run Node setup/npm install/build as part of server deployment Requires server tooling and rebuilds the output
Transfer CI output Download the successful run's artifact on the deployment runner, transfer assets into the corresponding release Requires explicit artifact download and transfer steps
Prepare a complete release artifact Package source, required PHP dependencies and frontend assets in a compatible build environment More setup, but avoids rebuilding the release on staging

For an artifact approach, use the artifact from the same successful workflow run and commit, preserve required file permissions when packaging, and never package a real .env, private key, or credentials. Frontend VITE_* values can be embedded in public output; never place secrets there. Staging and production may need different public build settings unless the frontend supports runtime configuration.

37. Where Each Test Type Belongs

The following is a practical starting schedule, not a requirement to run every expensive suite on every change.

Test type What it checks Pull-request CI Staging/CD Scheduled checks
Unit Isolated functions and rules Run routinely Usually no need to repeat unchanged checks Optional broader matrix
Feature/API Routes, validation, authentication and responses Run routinely Critical smoke checks after deployment Regression checks if useful
Database integration Real engine queries, constraints and transactions Use an isolated database service where needed Verify release/migration compatibility Larger compatibility suite
Frontend build Asset bundling succeeds Run for projects with assets Deploy or build the required output Dependency compatibility checks
JavaScript unit Frontend logic Add if the project has a suite Usually rely on CI results Optional matrix
Browser/UI User interactions and rendering Fast critical journeys if practical Broader journeys on isolated staging Full browser regression
Cross-service/SSO Services and login work together Targeted integration tests where practical Critical full journeys on staging Broader integration suite
Performance/load Response times, throughput and resource behavior Small deterministic regression checks if useful Controlled tests on representative isolated infrastructure Larger load tests
Dependency/code security Known dependency issues and risky code patterns Appropriate scans Required release gates where defined Repeat scans as new advisories appear
Availability/attack monitoring Live uptime, abuse and intrusion signals Not provided by ordinary CI tests Basic health check at deployment Continuous monitoring and alerts

Do not run heavy load tests against a live production site as an incidental CI step. Database and browser tests should use controlled test data and environments. Functional tests and a green build do not replace live monitoring or incident response.

38. Original Workflow: Consolidated Reference Table

Block or command Runs where? Role Key limitation or prerequisite
name GitHub metadata Display label No effect on application behavior
on.pull_request GitHub event evaluation Start checks for a PR targeting main Does not deploy in this example
on.push GitHub event evaluation Start checks on pushes to main Direct pushes also qualify
permissions.contents: read GitHub token scope Allow repository reading Does not authorize server SSH
jobs.test CI runner Group testing work ID is chosen by the author
runs-on GitHub Select runner Separate jobs have separate environments
env under test Test runner Configure temporary Laravel services Does not configure staging
checkout Test runner Download source Must occur before source-based commands
setup-php and with Test runner Configure PHP, extensions and Composer Server PHP is separate
composer install in test Test runner Install locked dependencies including dev tools Keep lock file committed
cp and key:generate Test runner Prepare temporary environment Never replace server key with this one
php artisan test Test runner Execute written tests Does not create missing test coverage
jobs.deploy Deployment runner Group staging release steps Does not itself provide a server
needs: test GitHub scheduling Wait for successful checks Rename when depending on another job ID
Job-level if GitHub condition evaluation Restrict to push on main Both conditions must match
environment: staging GitHub deployment rules Apply environment settings Configure actual rules separately
concurrency GitHub scheduling Avoid overlapping group deployments Not a complete release-order policy
secrets.* GitHub expression evaluation Supply protected connection settings Must exist in the appropriate scope
github.sha GitHub context Identify source commit Verify running release separately if needed
install, printf, chmod Deployment runner Create restricted SSH files Preserve key format and verified host keys
ssh and here-document Runner connecting to staging Execute remote script Network and authorized key required
set -euo pipefail Staging Bash Improve error handling Does not undo completed commands
cd Staging Select repository Replace example path
Clean-worktree check Staging Git Stop on tracked manual edits Ignores untracked files
git fetch Staging Obtain Git objects Private repository needs server credentials
checkout --detach Staging Select specified commit Commit must be available
Composer with --no-dev Staging Install runtime dependencies Requires compatible server runtime
config:cache, view:cache Staging Laravel Prepare configuration/views Needs correct environment and write access
curl Deployment runner Request application health URL No automatic body/release verification or rollback
if: always() and rm -f Deployment runner Attempt private-key cleanup Abrupt machine loss can prevent execution

39. Additional Build References

The three examples have been checked for YAML structure and expected workflow content. They have not been executed against your repository; actual results depend on its dependencies, scripts, tests, and runtime requirements.

Top comments (0)