Deploying TopVideoHub means pushing to 4 LiteSpeed servers that each cover different Asia-Pacific regions. Manual FTP to 4 hosts is error-prone. GitHub Actions turned that into a one-push workflow.
Here is the full pipeline.
Constraints: Shared Hosting Reality
Shared LiteSpeed hosting does not give you SSH access, Docker runners on the server, or webhook endpoints. The deployment method is FTP via lftp. The CI pipeline must:
- Run tests and lint PHP without a server
- Deploy via FTP in parallel to all 4 hosts
- Clear the LiteSpeed page cache after deploy
- Send a notification when all hosts are live
Repository Secrets
In GitHub Settings → Secrets and variables → Actions, create one secret per host:
FTP_DWV_HOST=ftp.dailywatch.video
FTP_DWV_USER=deploy_user
FTP_DWV_PASS=super_secret
FTP_DWV_PATH=/htdocs
FTP_TVH_HOST=ftp.topvideohub.com
FTP_TVH_USER=deploy_user
FTP_TVH_PASS=super_secret
FTP_TVH_PATH=/htdocs
# ... repeat for tvs and vvv
Main Workflow
# .github/workflows/deploy.yml
name: Test and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# ─── Phase 1: Test ───────────────────────────────────────────
test:
name: PHP Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: pdo_sqlite, gd, curl
coverage: none
- name: Validate composer.json
run: composer validate --strict
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
- name: PHP Lint (syntax check)
run: |
find app/ public/ cron/ templates/ \
-name '*.php' \
-exec php -l {} \; | grep -v 'No syntax errors'
- name: Run unit tests
run: vendor/bin/phpunit tests/ --testdox
- name: Validate .htaccess syntax
run: |
if grep -P '\t' public/.htaccess; then
echo "ERROR: .htaccess contains tab characters"
exit 1
fi
echo ".htaccess OK"
- name: Check deploy_hosts.conf line endings
run: |
if grep -Pc '\r' deploy_hosts.conf > /dev/null 2>&1; then
echo "ERROR: deploy_hosts.conf has Windows line endings!"
exit 1
fi
echo "deploy_hosts.conf OK"
# ─── Phase 2: Deploy (only on push to main) ───────────────────
deploy:
name: Deploy — ${{ matrix.site }}
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
strategy:
fail-fast: false # Don't cancel other deploys if one fails
matrix:
site: [dwv, tvh, tvs, vvv]
steps:
- uses: actions/checkout@v4
- name: Install lftp
run: sudo apt-get install -y lftp
- name: Deploy via FTP
env:
FTP_HOST: ${{ secrets[format('FTP_{0}_HOST', matrix.site)] }}
FTP_USER: ${{ secrets[format('FTP_{0}_USER', matrix.site)] }}
FTP_PASS: ${{ secrets[format('FTP_{0}_PASS', matrix.site)] }}
FTP_PATH: ${{ secrets[format('FTP_{0}_PATH', matrix.site)] }}
run: |
lftp -u "$FTP_USER","$FTP_PASS" "$FTP_HOST" << 'LFTP'
set ssl:verify-certificate no
set net:timeout 60
set net:max-retries 3
mirror -R --verbose --ignore-time \
--exclude .git/ \
--exclude .github/ \
--exclude .env \
--exclude 'data/' \
--exclude '*.log' \
--exclude '*.db' \
--exclude 'backlink/' \
./ $FTP_PATH
put api_keys.conf -o $FTP_PATH/api_keys.conf
rm -rf $FTP_PATH/lscache
quit
LFTP
- name: Verify deploy
env:
FTP_HOST: ${{ secrets[format('FTP_{0}_HOST', matrix.site)] }}
FTP_USER: ${{ secrets[format('FTP_{0}_USER', matrix.site)] }}
FTP_PASS: ${{ secrets[format('FTP_{0}_PASS', matrix.site)] }}
FTP_PATH: ${{ secrets[format('FTP_{0}_PATH', matrix.site)] }}
run: |
REMOTE_VER=$(lftp -u "$FTP_USER","$FTP_PASS" "$FTP_HOST" -e \
"set ssl:verify-certificate no; cat $FTP_PATH/version.txt; quit")
LOCAL_VER=$(cat version.txt)
if [ "$REMOTE_VER" = "$LOCAL_VER" ]; then
echo "[${{ matrix.site }}] Verify PASSED: $REMOTE_VER"
else
echo "[${{ matrix.site }}] Verify FAILED: remote=$REMOTE_VER local=$LOCAL_VER"
exit 1
fi
# ─── Phase 3: Post-deploy notification ───────────────────────
notify:
name: Notify
needs: deploy
if: always()
runs-on: ubuntu-latest
steps:
- name: Post status to Slack
uses: slackapi/slack-github-action@v1.26.0
with:
payload: |
{
"text": "TopVideoHub deploy ${{ needs.deploy.result == 'success' && '✓ succeeded' || '✗ failed' }} on `${{ github.ref_name }}` by ${{ github.actor }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
PHP Unit Test Example
<?php
// tests/QuotaManagerTest.php
use PHPUnit\Framework\TestCase;
class QuotaManagerTest extends TestCase
{
private PDO $db;
protected function setUp(): void
{
// In-memory SQLite for tests — no file I/O
$this->db = new PDO('sqlite::memory:');
$this->db->exec(file_get_contents(__DIR__ . '/../schema.sql'));
}
public function testQuotaDecrement(): void
{
$mgr = new QuotaManager($this->db);
$mgr->setQuota('key1', 10000);
$mgr->consume('key1', 100);
$this->assertSame(9900, $mgr->remaining('key1'));
}
public function testKeyRotation(): void
{
$mgr = new QuotaManager($this->db);
$mgr->setQuota('key1', 0); // Exhausted
$mgr->setQuota('key2', 10000);
$activeKey = $mgr->getActiveKey();
$this->assertSame('key2', $activeKey);
}
public function testRegionFetchOrder(): void
{
$regions = ['US', 'GB', 'JP', 'KR', 'TW', 'SG', 'VN', 'TH', 'HK'];
$this->assertCount(9, $regions);
$this->assertContains('JP', $regions);
$this->assertContains('KR', $regions);
}
}
Caching the Composer Install
- name: Cache Composer packages
uses: actions/cache@v4
with:
path: vendor
key: composer-${{ hashFiles('composer.lock') }}
restore-keys: composer-
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
Parallel Deploy Performance
The matrix strategy runs all 4 deployments simultaneously. Wall-clock time for a full deploy to all 4 LiteSpeed hosts:
| Step | Time |
|---|---|
| PHP tests + lint | ~45s |
| FTP mirror (parallel × 4) | ~3min |
| Verify + notify | ~15s |
| Total | ~4min |
Without parallelism, sequential FTP to 4 hosts would take ~12 minutes.
Branch Protection
# .github/branch_protection.yml (via GitHub API or UI)
branch: main
required_status_checks:
strict: true
contexts:
- "PHP Tests"
require_pull_request_reviews:
required_approving_review_count: 1
restrict_pushes: true
Every commit to TopVideoHub now goes through this pipeline. The matrix deploy means all 4 servers are updated in under 4 minutes from a git push.
This is part of the "Building TopVideoHub" series, documenting the architecture behind a video discovery platform covering 9 Asia-Pacific regions.
Top comments (0)