DEV Community

ahmet gedik
ahmet gedik

Posted on

CI/CD Pipeline for a Multi-Site Video Platform

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)"
Enter fullscreen mode Exit fullscreen mode

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)