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:
- Continuous Integration (CI): Download the application, prepare PHP and Laravel, and run automated tests.
-
Continuous Deployment to staging (CD): After a push to
mainpasses 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
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
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
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]
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
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
-
jobsbegins the collection of jobs. -
testidentifies this job internally. -
nameis its visible label. -
runs-onselects 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
| 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
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
| 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
| 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
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
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
| 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
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 }}
| 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
| 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'
| 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
| 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)"
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 --porcelainproduces machine-readable output. -
--untracked-files=noexcludes untracked files. -
$(...)captures the output. -
test -zsucceeds 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"
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
| 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
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
| 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
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
- A developer opens or updates a pull request targeting
main. - GitHub starts the test runner.
- Checkout downloads the code.
- PHP and Composer are configured.
- Dependencies are installed.
- Laravel's temporary test environment is prepared.
- Tests execute.
- GitHub shows success or failure.
- Deployment is skipped because this is a pull-request event.
Flow B: Push or Merge to main
- GitHub receives a push to
main. - Tests run again for that push.
- Failure prevents deployment.
- Success allows the deployment job to proceed, subject to environment rules and concurrency.
- The runner prepares SSH credentials.
- SSH connects to staging.
- The server checks for tracked changes.
- Git fetches updates and selects the specified SHA.
- Composer installs server dependencies.
- Laravel rebuilds its configuration and view caches.
- The runner requests the health endpoint.
- 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.lockand a safe.env.example. - Confirm that the test command works locally.
- Check the selected PHP version and extensions against Composer requirements.
- Keep credentials, real
.envfiles, and generated caches out of Git.
GitHub
- Enable Actions for the repository.
- Configure the
stagingenvironment and any desired protection rules. - Add
SSH_HOST,SSH_USER,SSH_PRIVATE_KEY, andSSH_KNOWN_HOSTSas 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
.envand 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.
- GitHub Actions workflow syntax
- Understanding GitHub Actions
- GitHub Actions jobs
- GitHub checkout action
- PHP setup action
- Laravel testing documentation
- Laravel deployment documentation
- Composer command-line documentation
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
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
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
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
- GitHub Node.js build and test guide
- Official setup-node action
- Official upload-artifact action
- Laravel Vite documentation
- Vite build documentation
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)