Skip to content

Deploy

Deploy #44

Workflow file for this run

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