# Staging CI/CD – Complete Setup Guide

> **Staging URL:** https://staging.tinypay.uk/  
> **Stack:** Laravel 12 + Vite + Tailwind  
> **CI/CD:** GitHub Actions  
> **Hosting:** cPanel account on WHM

---

## Table of Contents

1. [Architecture Overview](#architecture-overview)
2. [Branch Strategy](#branch-strategy)
3. [Workflow Files](#workflow-files)
4. [Step 1: Prepare the cPanel Server](#step-1-prepare-the-cpanel-server)
5. [Step 2: Generate SSH Key for Deployment](#step-2-generate-ssh-key-for-deployment)
6. [Step 3: Configure GitHub Secrets](#step-3-configure-github-secrets)
7. [Step 4: Set Up .env on Staging Server](#step-4-set-up-env-on-staging-server)
8. [Step 5: First Manual Deploy (Bootstrapping)](#step-5-first-manual-deploy-bootstrapping)
9. [Step 6: Verify Auto-Deploy](#step-6-verify-auto-deploy)
10. [cPanel Document Root Setup](#cpanel-document-root-setup)
11. [Troubleshooting](#troubleshooting)
12. [Security Considerations](#security-considerations)
13. [Future Improvements](#future-improvements)

---

## Architecture Overview

```
┌──────────────┐       PR/push        ┌──────────────────┐
│  Developer   │ ──────────────────►  │   GitHub Actions  │
│  (feature/*) │                      │   CI workflow     │
└──────────────┘                      └──────────────────┘
                                              │
                                   merge to main
                                              │
                                              ▼
                                      ┌──────────────────┐
                                      │  Deploy workflow  │
                                      │  (test → build → │
                                      │   tar → scp →    │
                                      │   ssh deploy)    │
                                      └────────┬─────────┘
                                               │
                                    SCP + SSH   │
                                               ▼
                                      ┌──────────────────┐
                                      │  cPanel Server   │
                                      │  staging.tinypay │
                                      │  .uk             │
                                      └──────────────────┘
```

**Deploy flow:**
1. Tests pass on CI
2. Vite assets are built (compiled CSS/JS → `public/build/`)
3. Release tar created (excludes `.git`, `vendor`, `node_modules`, `.env`)
4. Tar uploaded via SCP to temp directory
5. SSH: extract → `composer install --no-dev` → migrate → cache optimize

**Key safety features:**
- `.env` is NEVER in the tar archive
- `.env` is verified to exist before deploy proceeds
- `.env` is backed up before extraction (extra safety)
- Route cache failure is non-fatal (closure routes detected gracefully)
- `vendor/` not in tar → fresh `--no-dev` install on server ensures clean prod deps
- Concurrency control: only one deploy at a time, newer cancels older

---

## Branch Strategy

| Branch | CI Tests | Auto-Deploy |
|--------|----------|-------------|
| `feature/*` | ✅ | ❌ |
| PR → any | ✅ | ❌ |
| `main` | ✅ | ✅ staging |
| `production` (future) | ✅ | Manual trigger |

---

## Workflow Files

### `.github/workflows/ci.yml`
- Triggers on all PRs and pushes (except `staging` branch)
- PHP 8.2 + Node 20
- Runs PHPUnit with SQLite
- Builds Vite assets (catches build errors early)

### `.github/workflows/deploy-staging.yml`
- Triggers on push to `main` (i.e., PR merge) + manual dispatch
- Two-job pipeline: `test` → `deploy`
- Uses GitHub environment `staging` (optional for protection rules)
- Actions used:
  - `appleboy/scp-action@v1.0.0` (latest stable, Apr 2025)
  - `appleboy/ssh-action@v1.2.5` (latest stable, Jan 2026)

---

## Step 1: Prepare the cPanel Server

### 1.1 Enable SSH Access

1. Log in to **WHM** → Security Center → Shell Fork Bomb Protection (ensure it's not blocking SSH)
2. In **WHM** → Account Functions → Manage Shell Access → set the cPanel user to **Normal Shell**
3. Verify you can SSH manually:
   ```bash
   ssh cpanel_username@your-server-ip -p 22
   ```

### 1.2 Verify PHP CLI Version

cPanel often has multiple PHP versions. Find the correct one:

```bash
# Check default PHP
php -v

# If it's not 8.2+, find EasyApache PHP:
ls /usr/local/bin/ea-php*
# Example: /usr/local/bin/ea-php82

# Test it:
/usr/local/bin/ea-php82 -v
```

**Note the full path** — you'll use it as `STAGING_PHP_BIN` secret if the default `php` is not 8.2+.

### 1.3 Verify Composer

```bash
# Check if composer is available
composer --version

# If not found, check common locations:
/usr/local/bin/composer --version
~/bin/composer --version

# If Composer is not installed, install for your user:
cd ~
curl -sS https://getcomposer.org/installer | /usr/local/bin/ea-php82
mv composer.phar ~/bin/composer
chmod +x ~/bin/composer
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
```

### 1.4 Create Application Directory

```bash
# Typical cPanel path structure:
# /home/USERNAME/staging.tinypay.uk/     ← app root (Laravel root)
# /home/USERNAME/public_html/staging/    ← OR subdomain docroot

# Create app directory (if not auto-created by cPanel subdomain):
mkdir -p /home/USERNAME/staging.tinypay.uk

# Create temp directory for uploads:
mkdir -p /home/USERNAME/tmp
```

### 1.5 Set Permissions

```bash
APP_PATH="/home/USERNAME/staging.tinypay.uk"

mkdir -p "$APP_PATH/storage/logs"
mkdir -p "$APP_PATH/storage/framework/cache/data"
mkdir -p "$APP_PATH/storage/framework/sessions"
mkdir -p "$APP_PATH/storage/framework/views"
mkdir -p "$APP_PATH/bootstrap/cache"

chmod -R 775 "$APP_PATH/storage"
chmod -R 775 "$APP_PATH/bootstrap/cache"
```

---

## Step 2: Generate SSH Key for Deployment

Generate a dedicated deploy key (do this on your **local machine** or any trusted machine):

```bash
# Generate Ed25519 key (recommended for cPanel)
ssh-keygen -t ed25519 -C "github-deploy-staging" -f ~/.ssh/deploy_staging_key

# You'll be asked for a passphrase — set one for extra security (stored in GitHub secrets)
```

**Alternative: RSA key (if cPanel/server doesn't support Ed25519):**
```bash
ssh-keygen -t rsa -b 4096 -C "github-deploy-staging" -f ~/.ssh/deploy_staging_key
```

### Install the public key on the server:

**Option A — via cPanel UI:**
1. cPanel → Security → SSH Access → Manage SSH Keys → Import Key
2. Paste the content of `deploy_staging_key.pub`
3. Go back → Authorize the key

**Option B — via command line:**
```bash
# Copy public key to server
ssh-copy-id -i ~/.ssh/deploy_staging_key.pub cpanel_user@server-ip

# Or manually:
cat ~/.ssh/deploy_staging_key.pub >> /home/USERNAME/.ssh/authorized_keys
chmod 600 /home/USERNAME/.ssh/authorized_keys
```

### Test the connection:

```bash
ssh -i ~/.ssh/deploy_staging_key cpanel_user@server-ip "echo 'SSH works!'"
```

---

## Step 3: Configure GitHub Secrets

Go to: **GitHub repo → Settings → Secrets and variables → Actions → New repository secret**

| Secret Name | Value | Example |
|-------------|-------|---------|
| `STAGING_SSH_HOST` | Server hostname or IP | `185.xxx.xxx.xxx` or `server.tinypay.uk` |
| `STAGING_SSH_USER` | cPanel username | `cutsolut` |
| `STAGING_SSH_PRIVATE_KEY` | Full private key content | `-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----` |
| `STAGING_SSH_PASSPHRASE` | Key passphrase (if set) | `your-passphrase` |
| `STAGING_SSH_PORT` | SSH port (if non-standard) | `22` (default, can omit) |
| `STAGING_APP_PATH` | Absolute path to Laravel root | `/home/cutsolut/staging.tinypay.uk` |
| `STAGING_TMP_PATH` | Temp upload directory | `/home/cutsolut/tmp` |
| `STAGING_PHP_BIN` | PHP binary path (if not default) | `/usr/local/bin/ea-php82` or `php` |

**How to get the private key content:**
```bash
cat ~/.ssh/deploy_staging_key
# Copy the ENTIRE output including -----BEGIN and -----END lines
```

### Optional: Create GitHub Environment

For extra protection (require approvals before deploy):
1. GitHub repo → Settings → Environments → New environment → `staging`
2. (Optional) Add required reviewers
3. (Optional) Add branch protection: only `main` can deploy

---

## Step 4: Set Up .env on Staging Server

SSH into the server and create the `.env` file:

```bash
cd /home/USERNAME/staging.tinypay.uk
nano .env
```

**Minimal staging .env:**

```env
APP_NAME="Orderbuddy Portal (Staging)"
APP_ENV=staging
APP_KEY=                          # Will be generated in Step 5
APP_DEBUG=true                    # true for staging, false for production
APP_URL=https://staging.tinypay.uk

LOG_CHANNEL=stack
LOG_LEVEL=debug

# Database — use staging database credentials
DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=your_staging_db
DB_USERNAME=your_staging_db_user
DB_PASSWORD=your_staging_db_password

# Session & Cache
CACHE_STORE=file
SESSION_DRIVER=file
QUEUE_CONNECTION=sync

# Vite (for asset URLs)
VITE_APP_NAME="${APP_NAME}"
```

**Important:** Create the staging database via cPanel → MySQL Databases before proceeding.

---

## Step 5: First Manual Deploy (Bootstrapping)

The first deployment must be done manually to initialize the application:

```bash
# On the staging server:
cd /home/USERNAME/staging.tinypay.uk

# Clone the repo (one-time only):
git clone https://github.com/cutsolutions/orderbuddy-portal.git .
# OR if the directory already has files from a previous tar extraction, skip clone

# Install dependencies:
composer install --no-dev --optimize-autoloader

# Generate app key (writes to .env):
php artisan key:generate

# Create storage symlink:
php artisan storage:link

# Run migrations:
php artisan migrate --force

# Build caches:
php artisan config:cache
php artisan view:cache

# Set permissions:
chmod -R 775 storage bootstrap/cache
```

**After this, verify the site loads at https://staging.tinypay.uk/**

> **Note:** After the first manual deploy, all subsequent deploys are handled by GitHub Actions. The git repo on server is NOT used after bootstrapping — the workflow deploys via tar extraction.

---

## Step 6: Verify Auto-Deploy

1. Create a test branch:
   ```bash
   git checkout -b test/cicd-verify
   ```

2. Make a small visible change (e.g., add a comment in `welcome.blade.php`)

3. Push and create PR:
   ```bash
   git push origin test/cicd-verify
   ```

4. **Check CI passes:** GitHub → Actions → CI workflow should be green

5. **Merge the PR** into `main`

6. **Watch deploy:** GitHub → Actions → "Deploy Staging" workflow should trigger

7. **Verify on staging:** Visit https://staging.tinypay.uk/ and confirm change is live

8. Clean up:
   ```bash
   git branch -d test/cicd-verify
   git push origin --delete test/cicd-verify
   ```

---

## cPanel Document Root Setup

Laravel requires the web server's document root to point to the `public/` directory.

### Option A: Subdomain with custom document root (Recommended)

1. **cPanel → Domains (or Subdomains)**
2. Create/edit `staging.tinypay.uk`
3. Set **Document Root** to: `/home/USERNAME/staging.tinypay.uk/public`

This is the cleanest approach — no `.htaccess` hacks needed.

### Option B: .htaccess redirect (if document root can't be changed)

If the document root points to the Laravel root (not `public/`), add this `.htaccess`:

```apache
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteRule ^(.*)$ public/$1 [L]
</IfModule>
```

⚠️ **Option A is strongly preferred** — Option B exposes config files to the web.

### Verify document root is correct:

```bash
# SSH into server:
curl -s -o /dev/null -w "%{http_code}" https://staging.tinypay.uk/
# Should return 200 (or 302 if redirecting to login)
```

---

## Troubleshooting

### Deploy fails: "Missing .env"

The workflow refuses to deploy if `.env` doesn't exist on the server. Fix:
```bash
ssh user@server "cat /home/USERNAME/staging.tinypay.uk/.env"
# If empty/missing, create it per Step 4
```

### SSH connection refused

1. Verify SSH is enabled for the cPanel user (WHM → Shell Access)
2. Check port: `ssh -p 22 user@host` (some cPanel servers use non-standard ports)
3. Verify key is authorized: check `/home/USERNAME/.ssh/authorized_keys`
4. Check key format: Ed25519 or RSA 4096 (DSA/ECDSA may be disabled)

### "Permission denied" on storage/

```bash
chmod -R 775 /home/USERNAME/staging.tinypay.uk/storage
chmod -R 775 /home/USERNAME/staging.tinypay.uk/bootstrap/cache
# Ensure the cPanel user owns the files:
chown -R USERNAME:USERNAME /home/USERNAME/staging.tinypay.uk
```

### Composer not found

The workflow runs `composer install` directly. If composer isn't in PATH:
```bash
# Find it:
which composer
ls /usr/local/bin/composer
ls ~/bin/composer

# If absent, install per Step 1.3
# The workflow assumes composer is in PATH; if it's at a custom location,
# edit the deploy script or add an alias in ~/.bashrc
```

### PHP version mismatch

If migrations or artisan commands fail with syntax errors:
```bash
php -v  # Check version

# If < 8.2, set STAGING_PHP_BIN secret to the correct path:
# e.g., /usr/local/bin/ea-php82
```

### route:cache fails

This is handled gracefully — the workflow falls back to `route:clear`. The only impact is slightly slower first-request routing. To fix permanently:
- Convert closure routes in `routes/web.php` to controller methods
- The `/setup/deploy` route is the known closure route

### Asset 404s (CSS/JS not loading)

1. Check `public/build/manifest.json` exists on server
2. Check `APP_URL` in `.env` matches the actual domain
3. Ensure the Vite manifest is included in the tar (it is — `public/build/` is included)

### Migration fails mid-deploy

If a migration fails, the deploy stops (`set -e`). The app remains in its previous state since:
- Old code files may be overwritten but vendor isn't installed yet
- Fix: SSH in, fix the migration, run manually: `php artisan migrate --force`

---

## Security Considerations

1. **SSH key security:** Use Ed25519 keys with a passphrase. Store passphrase in GitHub secrets.
2. **Secrets exposure:** Never log secrets in workflow steps. The workflow only uses them as action inputs.
3. **`.env` protection:** Never committed to git, never in the tar. Only exists on server.
4. **`APP_DEBUG`:** Set to `false` before going live with real data (can be `true` for initial testing).
5. **Database backups:** Consider adding `mysqldump` before migration in the deploy script for safety.
6. **HTTPS:** Ensure SSL certificate is active on staging.tinypay.uk (cPanel → SSL/TLS or AutoSSL).
7. **Rate limiting:** The workflow has concurrency control — only one deploy runs at a time.

---

## Future Improvements

### Zero-downtime deployments (symlink strategy)

```
/home/USERNAME/staging.tinypay.uk/
├── releases/
│   ├── 20250115_120000/     ← previous release
│   └── 20250116_143000/     ← current release
├── shared/
│   ├── .env
│   └── storage/
└── current -> releases/20250116_143000/   ← symlink
```

Document root would point to `current/public/`. Rollback = switch symlink.

### Production deployment workflow

- Separate workflow with `workflow_dispatch` (manual trigger only)
- Targets IONOS server (`icard.orderbuddy.tech`)
- Requires separate secrets (`PROD_SSH_HOST`, etc.)
- Same deploy logic, different target

### Slack/Discord notifications

Add a notification step at the end of deploy:
```yaml
- name: Notify deploy
  uses: 8398a7/action-slack@v3
  with:
    status: ${{ job.status }}
```

### Database backup before migration

```bash
mysqldump -u user -p'pass' dbname > "$TMP_PATH/backup_$(date +%Y%m%d_%H%M%S).sql"
```

---

## Quick Reference

| Action | Command |
|--------|---------|
| Trigger deploy manually | GitHub → Actions → Deploy Staging → Run workflow |
| Check deploy status | GitHub → Actions → latest "Deploy Staging" run |
| SSH to staging | `ssh cpanel_user@server-ip` |
| View Laravel logs | `tail -f /home/USER/staging.tinypay.uk/storage/logs/laravel.log` |
| Clear all caches | `php artisan optimize:clear` |
| Run migrations manually | `php artisan migrate --force` |
| Check app health | `curl -sI https://staging.tinypay.uk/` |
