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');