The TrendVidStream codebase is shared across 4 LiteSpeed servers. Each server has its own .env with region assignments, API keys, and site-specific config. A git push to main should safely update all 4 without human intervention.
GitHub Actions handles this with a matrix deploy strategy.
Repository Layout Relevant to CI
v2/
├── app/
├── public/
├── templates/
├── cron/
├── site_envs/
│ ├── dwv.env
│ ├── tvh.env
│ ├── tvs.env ← TrendVidStream production config
│ └── vvv.env
├── deploy_hosts.conf ← FTP credentials (NEVER committed)
├── api_keys.conf ← YouTube keys (committed, excluded from .gitignore)
├── version.txt ← Git SHA, bumped on each deploy
└── .github/
└── workflows/
└── deploy.yml
Full Workflow
# .github/workflows/deploy.yml
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
PHP_VERSION: '8.3'
jobs:
# ─── Phase 1: Static analysis + tests ───────────────────────
quality:
name: Quality Gates
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP ${{ env.PHP_VERSION }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
extensions: pdo_sqlite, gd, curl, intl
tools: phpstan, cs2pr
coverage: none
- name: Cache Composer
uses: actions/cache@v4
with:
path: vendor
key: composer-${{ hashFiles('composer.lock') }}
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
- name: PHP syntax check
run: >
find app/ public/ cron/ -name '*.php'
-exec php -l {} \; 2>&1 | tee /tmp/syntax.log;
grep -v 'No syntax errors' /tmp/syntax.log | grep -v '^$' || true
- name: PHPStan analysis
run: vendor/bin/phpstan analyse app/ --level=6 --no-progress
- name: Unit tests
run: vendor/bin/phpunit tests/ --testdox --colors=always
- name: Validate deploy_hosts.conf line endings
run: |
if file deploy_hosts.conf | grep -q CRLF; then
echo "::error::deploy_hosts.conf has Windows line endings"
exit 1
fi
- name: Validate .env files exist for all sites
run: |
for site in dwv tvh tvs vvv; do
if [ ! -f "site_envs/${site}.env" ]; then
echo "::error::Missing site_envs/${site}.env"
exit 1
fi
done
echo "All .env files present"
- name: Check no secrets in tracked files
run: |
# Ensure .env files are not tracked
if git ls-files | grep -q '\.env$'; then
echo "::error::.env file is tracked by git"
exit 1
fi
# ─── Phase 2: Write version.txt ──────────────────────────────
version:
name: Tag Version
needs: quality
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Write version file
run: |
echo "${{ github.sha }}" > version.txt
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> version.txt
- name: Upload version artifact
uses: actions/upload-artifact@v4
with:
name: version
path: version.txt
# ─── Phase 3: Parallel deploy to all 4 hosts ─────────────────
deploy:
name: Deploy — ${{ matrix.site }}
needs: version
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- site: tvs
site_env: tvs.env
- site: dwv
site_env: dwv.env
- site: tvh
site_env: tvh.env
- site: vvv
site_env: vvv.env
steps:
- uses: actions/checkout@v4
- name: Download version artifact
uses: actions/download-artifact@v4
with:
name: version
- name: Install lftp
run: sudo apt-get install -y lftp
- name: Deploy to ${{ matrix.site }}
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 5
set net:reconnect-interval-base 5
# Mirror app files (excludes data, logs, .env)
mirror -R --ignore-time --verbose \
--exclude .git/ \
--exclude .github/ \
--exclude backlink/ \
--exclude 'site_envs/' \
--exclude 'data/' \
--exclude '*.log' \
--exclude '*.db' \
./ $FTP_PATH
# Deploy this site's env file
put site_envs/${{ matrix.site_env }} -o $FTP_PATH/.env
# Deploy shared API keys
put api_keys.conf -o $FTP_PATH/api_keys.conf
# Deploy version marker
put version.txt -o $FTP_PATH/version.txt
# Clear LiteSpeed page cache
rm -rf $FTP_PATH/lscache
quit
LFTP
- name: Verify deployment
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=$(lftp -u "$FTP_USER","$FTP_PASS" "$FTP_HOST" -e \
"set ssl:verify-certificate no; get $FTP_PATH/version.txt -o /tmp/remote_ver.txt; quit")
LOCAL=$(head -1 version.txt)
REMOTE_SHA=$(head -1 /tmp/remote_ver.txt)
if [ "$LOCAL" = "$REMOTE_SHA" ]; then
echo "[${{ matrix.site }}] OK: $REMOTE_SHA"
else
echo "[${{ matrix.site }}] MISMATCH: local=$LOCAL remote=$REMOTE_SHA"
exit 1
fi
# ─── Phase 4: Summary ─────────────────────────────────────────
summary:
name: Deploy Summary
needs: deploy
if: always()
runs-on: ubuntu-latest
steps:
- name: Output result
run: |
echo "## Deploy Result" >> $GITHUB_STEP_SUMMARY
echo "| Site | Status |" >> $GITHUB_STEP_SUMMARY
echo "|---|---|" >> $GITHUB_STEP_SUMMARY
echo "| tvs | ${{ needs.deploy.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| dwv | ${{ needs.deploy.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| tvh | ${{ needs.deploy.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| vvv | ${{ needs.deploy.result }} |" >> $GITHUB_STEP_SUMMARY
Pull Request Workflow (Test Only)
On pull requests, only the quality gates run — no deployment:
# Reuses the same quality job, skips version + deploy
# because of the `if: github.ref == 'refs/heads/main'` guards
Cron Monitoring Job
# .github/workflows/monitor.yml
name: Monitor Cron Health
on:
schedule:
- cron: '0 6 * * *' # Daily at 06:00 UTC
jobs:
ping:
runs-on: ubuntu-latest
strategy:
matrix:
url:
- https://trendvidstream.com/health
- https://topvideohub.com/health
- https://dailywatch.video/health
steps:
- name: Health check ${{ matrix.url }}
run: |
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
--max-time 10 '${{ matrix.url }}')
if [ "$STATUS" != "200" ]; then
echo "::error::${{ matrix.url }} returned HTTP $STATUS"
exit 1
fi
echo "${{ matrix.url }} OK ($STATUS)"
Total Pipeline Time
| Phase | Time |
|---|---|
| Quality gates | ~50s |
| Version tag | ~5s |
| Parallel FTP × 4 | ~3.5min |
| Verification × 4 | ~20s |
| Total | ~5min |
Every commit to TrendVidStream now deploys to all 4 global servers in under 5 minutes with zero manual steps.
This is part of the "Building TrendVidStream" series, documenting the architecture behind a global video directory covering Nordic, Middle Eastern, and Central European regions.
Top comments (0)