Deploy #44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy | |
| # Triggers: | |
| # - Automatically after CI workflow completes successfully on main | |
| # - Manually via workflow_dispatch for hotfixes or rollbacks | |
| on: | |
| workflow_run: | |
| workflows: ["CI"] | |
| types: [completed] | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| environment: | |
| description: "Target environment" | |
| required: true | |
| default: "production" | |
| type: choice | |
| options: [production, staging] | |
| run_migrations: | |
| description: "Run database migrations" | |
| required: true | |
| default: true | |
| type: boolean | |
| run_scout_import: | |
| description: "Re-index Typesense (slow — only if schema changed)" | |
| required: true | |
| default: false | |
| type: boolean | |
| concurrency: | |
| group: deploy-${{ github.ref }} | |
| cancel-in-progress: false # never cancel an in-flight deploy | |
| jobs: | |
| deploy: | |
| name: Deploy to ${{ github.event.inputs.environment || 'production' }} | |
| runs-on: ubuntu-latest | |
| # Only deploy if CI succeeded (or triggered manually) | |
| if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} | |
| environment: ${{ github.event.inputs.environment || 'production' }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| # ── Build frontend assets on CI runner (npm not available on server) ── | |
| - name: Setup PHP | |
| uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: "8.4" | |
| tools: composer:v2 | |
| coverage: none | |
| - name: Install PHP dependencies | |
| run: composer install --no-dev --no-interaction --no-scripts --prefer-dist | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: "npm" | |
| - name: Install Node dependencies | |
| run: npm ci | |
| - name: Build frontend assets | |
| run: npm run build | |
| # ── SSH deploy ────────────────────────────────────────── | |
| # Required GitHub secrets: | |
| # DEPLOY_HOST — server hostname or IP (e.g. aljebal-albeedos.com) | |
| # DEPLOY_PORT — SSH port (e.g. 21098) | |
| # DEPLOY_USER — SSH username (e.g. aljedkrq) | |
| # DEPLOY_KEY — private SSH key (RSA or Ed25519) | |
| # DEPLOY_PATH — absolute path to the Laravel app root on the server | |
| # (e.g. /home/aljedkrq/nexo-ecommerce) | |
| # DEPLOY_PHP — path to PHP binary on server (e.g. /home/aljedkrq/php84) | |
| # DEPLOY_WEBROOT — absolute path to the domain's web document root | |
| # (e.g. /home/aljedkrq/store.aljebal-albeedos.com) | |
| # Required when the web root differs from DEPLOY_PATH/public | |
| # Check cPanel → Domains → Document Root for the value. | |
| # ──────────────────────────────────────────────────────── | |
| - name: Setup SSH agent | |
| uses: webfactory/ssh-agent@v0.9.0 | |
| with: | |
| ssh-private-key: ${{ secrets.DEPLOY_KEY }} | |
| - name: Add server to known hosts | |
| run: ssh-keyscan -p ${{ secrets.DEPLOY_PORT }} -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts | |
| - name: Pull latest code & install PHP dependencies | |
| env: | |
| DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} | |
| DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }} | |
| DEPLOY_USER: ${{ secrets.DEPLOY_USER }} | |
| DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} | |
| DEPLOY_PHP: ${{ secrets.DEPLOY_PHP }} | |
| run: | | |
| ssh -p $DEPLOY_PORT $DEPLOY_USER@$DEPLOY_HOST << EOF | |
| set -e | |
| PHP="${DEPLOY_PHP:-php}" | |
| cd $DEPLOY_PATH | |
| echo "==> Pulling latest code" | |
| git fetch origin main | |
| if ! git diff --quiet || ! git diff --cached --quiet; then | |
| echo "==> Stashing unexpected local changes on server before pulling" | |
| git stash push -u -m "auto-stash before deploy \$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| fi | |
| git merge --ff-only origin/main | |
| echo "==> Installing PHP dependencies" | |
| if command -v composer >/dev/null 2>&1; then | |
| COMPOSER="composer" | |
| elif [ -f "\$HOME/bin/composer" ]; then | |
| COMPOSER="\$HOME/bin/composer" | |
| elif [ -f "$DEPLOY_PATH/composer.phar" ]; then | |
| COMPOSER="$DEPLOY_PATH/composer.phar" | |
| else | |
| echo "==> composer not found on server, bootstrapping composer.phar" | |
| \$PHP -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" | |
| ACTUAL_SIG="\$(\$PHP -r "echo hash_file('sha384', 'composer-setup.php');")" | |
| EXPECTED_SIG="\$(\$PHP -r "echo file_get_contents('https://composer.github.io/installer.sig');")" | |
| if [ "\$ACTUAL_SIG" != "\$EXPECTED_SIG" ]; then | |
| echo "Composer installer signature mismatch, aborting" >&2 | |
| rm -f composer-setup.php | |
| exit 1 | |
| fi | |
| \$PHP composer-setup.php --quiet --install-dir="$DEPLOY_PATH" --filename=composer.phar | |
| rm -f composer-setup.php | |
| COMPOSER="$DEPLOY_PATH/composer.phar" | |
| fi | |
| \$PHP \$COMPOSER install --no-dev --no-interaction --optimize-autoloader --prefer-dist | |
| EOF | |
| - name: Upload built assets | |
| env: | |
| DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} | |
| DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }} | |
| DEPLOY_USER: ${{ secrets.DEPLOY_USER }} | |
| DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} | |
| run: | | |
| rsync -az --delete -e "ssh -p $DEPLOY_PORT" \ | |
| public/build/ \ | |
| $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/public/build/ | |
| - name: Finalize deploy | |
| env: | |
| DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} | |
| DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }} | |
| DEPLOY_USER: ${{ secrets.DEPLOY_USER }} | |
| DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} | |
| DEPLOY_PHP: ${{ secrets.DEPLOY_PHP }} | |
| DEPLOY_WEBROOT: ${{ secrets.DEPLOY_WEBROOT }} | |
| RUN_MIGRATIONS: ${{ github.event.inputs.run_migrations || 'true' }} | |
| RUN_SCOUT_IMPORT: ${{ github.event.inputs.run_scout_import || 'false' }} | |
| run: | | |
| ssh -p $DEPLOY_PORT $DEPLOY_USER@$DEPLOY_HOST << EOF | |
| set -e | |
| PHP="${DEPLOY_PHP:-php}" | |
| cd $DEPLOY_PATH | |
| # Ensure the web root's build/ points to the app's compiled assets. | |
| # The web root (DEPLOY_WEBROOT) differs from DEPLOY_PATH/public on this server. | |
| if [ -n "$DEPLOY_WEBROOT" ]; then | |
| ln -sfn $DEPLOY_PATH/public/build $DEPLOY_WEBROOT/build | |
| echo "==> Symlinked $DEPLOY_WEBROOT/build -> $DEPLOY_PATH/public/build" | |
| fi | |
| echo "==> Caching configuration" | |
| \$PHP artisan config:cache | |
| \$PHP artisan route:cache | |
| \$PHP artisan view:cache | |
| \$PHP artisan event:cache | |
| if [ "$RUN_MIGRATIONS" = "true" ]; then | |
| echo "==> Running database migrations" | |
| \$PHP artisan migrate --force | |
| fi | |
| echo "==> Restarting queue workers" | |
| \$PHP artisan queue:restart | |
| if [ "$RUN_SCOUT_IMPORT" = "true" ]; then | |
| echo "==> Re-indexing Typesense" | |
| \$PHP artisan scout:flush "App\Domain\Product\Models\Product" | |
| \$PHP artisan scout:import "App\Domain\Product\Models\Product" | |
| \$PHP artisan scout:flush "App\Domain\Category\Models\Category" | |
| \$PHP artisan scout:import "App\Domain\Category\Models\Category" | |
| \$PHP artisan scout:flush "App\Domain\Order\Models\Order" | |
| \$PHP artisan scout:import "App\Domain\Order\Models\Order" | |
| fi | |
| echo "==> Done (\$(ls $DEPLOY_PATH/public/build/assets/ | wc -l) assets)" | |
| EOF | |
| - name: Notify on failure | |
| if: failure() | |
| run: | | |
| echo "::error::Deployment failed. Check the logs above." | |
| # Add Slack/email notification here if needed |