From d2a2fe3c672964ba51bd132de50fcb4f565341a1 Mon Sep 17 00:00:00 2001 From: Sourov Biswas Date: Sat, 19 Sep 2026 16:13:50 +0600 Subject: [PATCH] Deploy to accounts.3ag.app with Deployer Mirrors the SalesReport and ProductSync setup: assets are built on the runner and uploaded, releases are atomic, and queue workers restart gracefully. Deploys run only after the tests workflow passes on main, so this adds that workflow too. PHP 8.4 in CI matches the php-fpm the server runs. Co-Authored-By: Claude Opus 5 --- .github/workflows/deploy-production.yml | 64 ++++++++++ .github/workflows/tests.yml | 40 +++++++ composer.json | 3 +- composer.lock | 56 ++++++++- deploy.php | 148 ++++++++++++++++++++++++ 5 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/deploy-production.yml create mode 100644 .github/workflows/tests.yml create mode 100644 deploy.php diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml new file mode 100644 index 0000000..ce0e12c --- /dev/null +++ b/.github/workflows/deploy-production.yml @@ -0,0 +1,64 @@ +name: Deploy to Production + +# Deploy only after the tests workflow passes on main. +on: + workflow_run: + workflows: [tests] + types: [completed] + branches: [main] + workflow_dispatch: + +concurrency: production_environment + +permissions: + contents: read + +jobs: + deploy: + if: github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push') + runs-on: ubuntu-latest + environment: production + + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.workflow_run.head_sha || github.sha }} + persist-credentials: false + + - name: Setup PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 + with: + php-version: '8.4' + tools: composer:v2 + coverage: none + + # Deployer comes from require-dev. + - name: Install Composer dependencies + run: composer install --prefer-dist --no-progress --no-interaction + + - name: Setup Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22' + cache: 'npm' + + - name: Create environment file + run: | + cp .env.example .env + php artisan key:generate + + # deploy.php runs `npm ci && npm run build` before uploading public/build. + # Pin the revision so the server code matches the assets built here, even + # if main moves while this run is queued. + - name: Deploy to Production + uses: deployphp/action@v1 + with: + private-key: ${{ secrets.SSH_PRIVATE_KEY }} + dep: deploy --revision=${{ github.event.workflow_run.head_sha || github.sha }} + env: + DEPLOY_HOSTNAME: ${{ vars.DEPLOY_HOSTNAME }} + DEPLOY_PATH: ${{ vars.DEPLOY_PATH }} + DEPLOY_SSH_PORT: ${{ secrets.DEPLOY_SSH_PORT }} + DEPLOY_USER: ${{ vars.DEPLOY_USER }} + DEPLOY_BRANCH: main diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..ca4026e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,40 @@ +name: tests + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + ci: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # 8.4 is what php-fpm runs on the server. + - name: Setup PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 + with: + php-version: '8.4' + tools: composer:v2 + coverage: none + + - name: Setup Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22' + + # composer setup writes .env, generates a key, migrates sqlite and builds assets. + - name: Setup Application + run: composer setup + + - name: Run Tests + run: composer test diff --git a/composer.json b/composer.json index 6b77bc1..42fcd8f 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "laravel/tinker": "^3.0" }, "require-dev": { + "deployer/deployer": "^7.5", "fakerphp/faker": "^1.23", "laravel/boost": "^2.2", "laravel/pail": "^1.2.5", @@ -89,4 +90,4 @@ }, "minimum-stability": "stable", "prefer-stable": true -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 9cabcd8..c5855f4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5472847cfdb6fbc0e4b01bf244b9c12b", + "content-hash": "d2608fdd96cdca7a4aca757f3c037ee9", "packages": [ { "name": "brick/math", @@ -6215,6 +6215,60 @@ ], "time": "2025-08-20T19:15:30+00:00" }, + { + "name": "deployer/deployer", + "version": "v7.5.12", + "source": { + "type": "git", + "url": "https://github.com/deployphp/deployer.git", + "reference": "efc71dac9ccc86b3f9946e75d50cb106b775aae2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/deployphp/deployer/zipball/efc71dac9ccc86b3f9946e75d50cb106b775aae2", + "reference": "efc71dac9ccc86b3f9946e75d50cb106b775aae2", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^8.0|^7.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.64", + "pestphp/pest": "^3.3", + "phpstan/phpstan": "^1.4", + "phpunit/php-code-coverage": "^11.0", + "phpunit/phpunit": "^11.4" + }, + "bin": [ + "bin/dep" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anton Medvedev", + "email": "anton@medv.io" + } + ], + "description": "Deployment Tool", + "homepage": "https://deployer.org", + "support": { + "docs": "https://deployer.org/docs", + "issues": "https://github.com/deployphp/deployer/issues", + "source": "https://github.com/deployphp/deployer" + }, + "funding": [ + { + "url": "https://github.com/sponsors/antonmedv", + "type": "github" + } + ], + "time": "2025-02-19T16:45:27+00:00" + }, { "name": "doctrine/deprecations", "version": "1.1.6", diff --git a/deploy.php b/deploy.php new file mode 100644 index 0000000..1f1f756 --- /dev/null +++ b/deploy.php @@ -0,0 +1,148 @@ +set('remote_user', getenv('DEPLOY_USER') ?: 'sourov') + ->set('deploy_path', $deployPath) + ->set('http_user', 'www-data') + ->set('port', $sshPort); + +/* +|-------------------------------------------------------------------------- +| Local Asset Build +|-------------------------------------------------------------------------- +*/ + +// The runner builds public/build; the server never installs npm. +task('build:assets', function () { + writeln('📦 Building assets locally...'); + runLocally('npm ci'); + runLocally('npm run build'); +})->desc('Build assets locally'); + +/* +|-------------------------------------------------------------------------- +| Upload Built Assets +|-------------------------------------------------------------------------- +*/ + +task('upload:assets', function () { + writeln('🚀 Uploading built assets...'); + $user = get('remote_user'); + $hostname = currentHost()->getHostname(); + $port = get('port'); + $releasePath = get('release_path'); + $archive = 'build-assets.tar.gz'; + + runLocally("tar -czf {$archive} -C public build"); + runLocally("scp -P {$port} {$archive} {$user}@{$hostname}:{$releasePath}/"); + run("tar -xzf {$releasePath}/{$archive} -C {$releasePath}/public/"); + runLocally("rm {$archive}"); + run("rm {$releasePath}/{$archive}"); +})->desc('Upload built assets'); + +/* +|-------------------------------------------------------------------------- +| Skip npm on Server +|-------------------------------------------------------------------------- +*/ + +task('deploy:npm', function () { + writeln('⏭️ Skipping npm install on server'); +}); + +/* +|-------------------------------------------------------------------------- +| Restart Queue Workers +|-------------------------------------------------------------------------- +*/ + +// No queued work ships yet, but the workers are running; queue:restart +// lets one finish its current job, then Supervisor starts it again on the +// new release. The signal travels through the cache, so the deploy user needs +// no rights over the www-data worker processes. +task('queue:restart', function () { + writeln('🔄 Gracefully restarting queue workers...'); + run('cd {{release_path}} && php artisan queue:restart'); +})->desc('Gracefully restart queue workers'); + +/* +|-------------------------------------------------------------------------- +| Hooks +|-------------------------------------------------------------------------- +*/ + +before('deploy', 'build:assets'); + +after('deploy:vendors', 'upload:assets'); + +after('deploy:symlink', 'queue:restart'); + +after('deploy:failed', 'deploy:unlock');