Initial commit
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/vendor/
|
||||||
|
composer.lock
|
||||||
|
.phpunit.result.cache
|
||||||
|
.phpunit.cache
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Zura
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
195
README.md
Normal file
195
README.md
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
# Laravel Hostinger Deploy
|
||||||
|
|
||||||
|
A Laravel package for automated deployment to Hostinger shared hosting with GitHub Actions support.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🚀 **One-command deployment** to Hostinger shared hosting
|
||||||
|
- 🔄 **GitHub Actions integration** for automated deployments
|
||||||
|
- 🔑 **Automatic SSH key management** for secure connections
|
||||||
|
- 📦 **Laravel-specific optimizations** (composer, migrations, storage links)
|
||||||
|
- ⚙️ **Configurable deployment options** via config file
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install the package via Composer:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require zura/laravel-hostinger-deploy --dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### 1. Environment Variables
|
||||||
|
|
||||||
|
Add the following variables to your `.env` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Hostinger Deployment Configuration
|
||||||
|
HOSTINGER_SSH_HOST=your-server-ip
|
||||||
|
HOSTINGER_SSH_USERNAME=your-username
|
||||||
|
HOSTINGER_SSH_PORT=22
|
||||||
|
HOSTINGER_SITE_DIR=your-website-folder
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Publish Configuration (Optional)
|
||||||
|
|
||||||
|
Publish the configuration file to customize deployment options:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan vendor:publish --tag=hostinger-deploy-config
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create `config/hostinger-deploy.php` with customizable options.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### 1. Deploy to Hostinger
|
||||||
|
|
||||||
|
Deploy your Laravel application to Hostinger shared hosting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan hostinger:deploy-shared
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--fresh`: Delete existing files and clone fresh repository
|
||||||
|
- `--site-dir=`: Override site directory from config
|
||||||
|
|
||||||
|
### 2. Publish GitHub Actions Workflow
|
||||||
|
|
||||||
|
Create a GitHub Actions workflow file for automated deployments:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan hostinger:publish-workflow
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--branch=`: Override default branch (default: auto-detect)
|
||||||
|
- `--php-version=`: Override PHP version (default: 8.3)
|
||||||
|
|
||||||
|
### 3. Setup Automated Deployment
|
||||||
|
|
||||||
|
Configure SSH keys and display GitHub secrets for automated deployment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan hostinger:auto-deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
This command will:
|
||||||
|
- Generate SSH keys on your Hostinger server
|
||||||
|
- Display all required GitHub secrets and variables
|
||||||
|
- Provide step-by-step instructions for GitHub setup
|
||||||
|
|
||||||
|
## GitHub Actions Setup
|
||||||
|
|
||||||
|
After running `php artisan hostinger:auto-deploy`, you'll need to add the following to your GitHub repository:
|
||||||
|
|
||||||
|
### Secrets (Repository Settings → Secrets and variables → Actions → Secrets)
|
||||||
|
|
||||||
|
- `SSH_HOST`: Your Hostinger server IP address
|
||||||
|
- `SSH_USERNAME`: Your Hostinger SSH username
|
||||||
|
- `SSH_PORT`: Your Hostinger SSH port (usually 22)
|
||||||
|
- `SSH_KEY`: Your private SSH key (displayed by the command)
|
||||||
|
|
||||||
|
### Variables (Repository Settings → Secrets and variables → Actions → Variables)
|
||||||
|
|
||||||
|
- `WEBSITE_FOLDER`: Your Hostinger website folder name
|
||||||
|
|
||||||
|
### Deploy Keys (Repository Settings → Deploy keys)
|
||||||
|
|
||||||
|
Add the public SSH key displayed by the `auto-deploy` command as a deploy key.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
The generated GitHub Actions workflow will:
|
||||||
|
|
||||||
|
1. **Checkout code** from your repository
|
||||||
|
2. **Setup PHP** environment
|
||||||
|
3. **Install dependencies** via Composer
|
||||||
|
4. **Generate application key** and create storage link
|
||||||
|
5. **Run database migrations**
|
||||||
|
6. **Deploy to Hostinger** via SSH
|
||||||
|
7. **Update code** on the server and run optimizations
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
### SSH Settings
|
||||||
|
|
||||||
|
```php
|
||||||
|
'ssh' => [
|
||||||
|
'host' => env('HOSTINGER_SSH_HOST'),
|
||||||
|
'username' => env('HOSTINGER_SSH_USERNAME'),
|
||||||
|
'port' => env('HOSTINGER_SSH_PORT', 22),
|
||||||
|
'timeout' => 30,
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deployment Settings
|
||||||
|
|
||||||
|
```php
|
||||||
|
'deployment' => [
|
||||||
|
'site_dir' => env('HOSTINGER_SITE_DIR'),
|
||||||
|
'composer_flags' => '--no-dev --optimize-autoloader',
|
||||||
|
'run_migrations' => true,
|
||||||
|
'run_storage_link' => true,
|
||||||
|
'run_config_cache' => false,
|
||||||
|
'run_route_cache' => false,
|
||||||
|
'run_view_cache' => false,
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitHub Actions Settings
|
||||||
|
|
||||||
|
```php
|
||||||
|
'github' => [
|
||||||
|
'workflow_file' => '.github/workflows/hostinger-deploy.yml',
|
||||||
|
'php_version' => '8.3',
|
||||||
|
'default_branch' => 'main',
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- PHP ^8.2
|
||||||
|
- Laravel ^11.0|^12.0
|
||||||
|
- SSH access to Hostinger server
|
||||||
|
- Git repository with GitHub integration
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### SSH Connection Issues
|
||||||
|
|
||||||
|
1. Verify your SSH credentials in `.env`
|
||||||
|
2. Test SSH connection manually: `ssh -p PORT USERNAME@HOST`
|
||||||
|
3. Ensure SSH key authentication is working
|
||||||
|
|
||||||
|
### GitHub Actions Issues
|
||||||
|
|
||||||
|
1. Verify all secrets and variables are correctly set
|
||||||
|
2. Check that the deploy key is added to your repository
|
||||||
|
3. Ensure the workflow file is committed and pushed
|
||||||
|
|
||||||
|
### Deployment Issues
|
||||||
|
|
||||||
|
1. Check that your Hostinger server has Composer installed
|
||||||
|
2. Verify the site directory exists and is writable
|
||||||
|
3. Ensure your Laravel application is properly configured
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- SSH keys are generated on the server and should be kept secure
|
||||||
|
- Private keys are displayed for GitHub setup - copy them carefully
|
||||||
|
- The package uses SSH key authentication for secure deployments
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This package is open-sourced software licensed under the [MIT license](LICENSE).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If you encounter any issues or have questions, please open an issue on GitHub.
|
||||||
52
composer.json
Normal file
52
composer.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "zura/laravel-hostinger-deploy",
|
||||||
|
"description": "Laravel package for automated Hostinger deployment with GitHub Actions support",
|
||||||
|
"type": "library",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": [
|
||||||
|
"laravel",
|
||||||
|
"hostinger",
|
||||||
|
"deployment",
|
||||||
|
"github-actions",
|
||||||
|
"ssh"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Zura",
|
||||||
|
"email": "zura@example.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": "^8.2",
|
||||||
|
"illuminate/support": "^11.0|^12.0",
|
||||||
|
"illuminate/console": "^11.0|^12.0",
|
||||||
|
"illuminate/process": "^11.0|^12.0",
|
||||||
|
"symfony/console": "^7.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^10.0",
|
||||||
|
"orchestra/testbench": "^8.0|^9.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Zura\\HostingerDeploy\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Zura\\HostingerDeploy\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Zura\\HostingerDeploy\\HostingerDeployServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true,
|
||||||
|
"config": {
|
||||||
|
"sort-packages": true
|
||||||
|
}
|
||||||
|
}
|
||||||
235
src/Commands/AutoDeployCommand.php
Normal file
235
src/Commands/AutoDeployCommand.php
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Zura\HostingerDeploy\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Zura\HostingerDeploy\Services\SshConnectionService;
|
||||||
|
use Zura\HostingerDeploy\Services\GitHubActionsService;
|
||||||
|
|
||||||
|
class AutoDeployCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*/
|
||||||
|
protected $signature = 'hostinger:auto-deploy';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*/
|
||||||
|
protected $description = 'Setup SSH keys and display GitHub secrets for automated deployment';
|
||||||
|
|
||||||
|
protected SshConnectionService $ssh;
|
||||||
|
protected GitHubActionsService $github;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->github = new GitHubActionsService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$this->info('🔧 Setting up automated deployment...');
|
||||||
|
|
||||||
|
// Validate configuration
|
||||||
|
if (!$this->validateConfiguration()) {
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get repository information
|
||||||
|
$repoInfo = $this->getRepositoryInfo();
|
||||||
|
if (!$repoInfo) {
|
||||||
|
$this->error('❌ Could not detect repository information. Please run this command from a Git repository.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("📦 Repository: {$repoInfo['owner']}/{$repoInfo['name']}");
|
||||||
|
|
||||||
|
// Setup SSH connection
|
||||||
|
$this->setupSshConnection();
|
||||||
|
|
||||||
|
// Test SSH connection
|
||||||
|
if (!$this->ssh->testConnection()) {
|
||||||
|
$this->error('❌ SSH connection failed. Please check your SSH configuration.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('✅ SSH connection successful');
|
||||||
|
|
||||||
|
// Setup SSH keys
|
||||||
|
if (!$this->setupSshKeys()) {
|
||||||
|
$this->error('❌ Failed to setup SSH keys');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display GitHub secrets
|
||||||
|
$this->displayGitHubSecrets($repoInfo);
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate required configuration.
|
||||||
|
*/
|
||||||
|
protected function validateConfiguration(): bool
|
||||||
|
{
|
||||||
|
$required = [
|
||||||
|
'HOSTINGER_SSH_HOST' => config('hostinger-deploy.ssh.host'),
|
||||||
|
'HOSTINGER_SSH_USERNAME' => config('hostinger-deploy.ssh.username'),
|
||||||
|
'HOSTINGER_SITE_DIR' => config('hostinger-deploy.deployment.site_dir'),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($required as $key => $value) {
|
||||||
|
if (empty($value)) {
|
||||||
|
$this->error("❌ Missing required environment variable: {$key}");
|
||||||
|
$this->info("Please add {$key} to your .env file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get repository information.
|
||||||
|
*/
|
||||||
|
protected function getRepositoryInfo(): ?array
|
||||||
|
{
|
||||||
|
if (!$this->github->isGitRepository()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->github->getRepositoryInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup SSH connection service.
|
||||||
|
*/
|
||||||
|
protected function setupSshConnection(): void
|
||||||
|
{
|
||||||
|
$this->ssh = new SshConnectionService(
|
||||||
|
config('hostinger-deploy.ssh.host'),
|
||||||
|
config('hostinger-deploy.ssh.username'),
|
||||||
|
config('hostinger-deploy.ssh.port', 22),
|
||||||
|
config('hostinger-deploy.ssh.timeout', 30)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup SSH keys on the server.
|
||||||
|
*/
|
||||||
|
protected function setupSshKeys(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Generate SSH keys if they don't exist
|
||||||
|
if (!$this->ssh->sshKeyExists()) {
|
||||||
|
$this->info('🔑 Generating SSH keys on server...');
|
||||||
|
if (!$this->ssh->generateSshKey()) {
|
||||||
|
$this->error('❌ Failed to generate SSH keys');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->info('🔑 SSH keys already exist on server');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the public key
|
||||||
|
$publicKey = $this->ssh->getPublicKey();
|
||||||
|
if (!$publicKey) {
|
||||||
|
$this->error('❌ Could not retrieve public key from server');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add public key to authorized_keys
|
||||||
|
$this->info('🔐 Adding public key to authorized_keys...');
|
||||||
|
if (!$this->ssh->addToAuthorizedKeys($publicKey)) {
|
||||||
|
$this->error('❌ Failed to add public key to authorized_keys');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('✅ SSH keys setup completed');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error("SSH keys setup error: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display GitHub secrets and variables for manual setup.
|
||||||
|
*/
|
||||||
|
protected function displayGitHubSecrets(array $repoInfo): void
|
||||||
|
{
|
||||||
|
$this->line('');
|
||||||
|
$this->info('🔒 GitHub Secrets and Variables Setup');
|
||||||
|
$this->line('');
|
||||||
|
|
||||||
|
// Get private key
|
||||||
|
$privateKey = $this->ssh->getPrivateKey();
|
||||||
|
if (!$privateKey) {
|
||||||
|
$this->error('❌ Could not retrieve private key from server');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display secrets
|
||||||
|
$this->warn('📋 Add these secrets to your GitHub repository:');
|
||||||
|
$this->line('Go to: ' . $repoInfo['secrets_url']);
|
||||||
|
$this->line('');
|
||||||
|
|
||||||
|
$secrets = [
|
||||||
|
'SSH_HOST' => config('hostinger-deploy.ssh.host'),
|
||||||
|
'SSH_USERNAME' => config('hostinger-deploy.ssh.username'),
|
||||||
|
'SSH_PORT' => (string) config('hostinger-deploy.ssh.port', 22),
|
||||||
|
'SSH_KEY' => $privateKey,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($secrets as $name => $value) {
|
||||||
|
$this->line("🔑 {$name}:");
|
||||||
|
if ($name === 'SSH_KEY') {
|
||||||
|
$this->line(' [Copy the private key below]');
|
||||||
|
$this->line('');
|
||||||
|
$this->line(' ' . str_repeat('-', 50));
|
||||||
|
$this->line($value);
|
||||||
|
$this->line(' ' . str_repeat('-', 50));
|
||||||
|
} else {
|
||||||
|
$this->line(" {$value}");
|
||||||
|
}
|
||||||
|
$this->line('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display variables
|
||||||
|
$this->warn('📊 Add this variable to your GitHub repository:');
|
||||||
|
$this->line('Go to: ' . $repoInfo['variables_url']);
|
||||||
|
$this->line('');
|
||||||
|
|
||||||
|
$this->line("📊 WEBSITE_FOLDER:");
|
||||||
|
$this->line(" " . config('hostinger-deploy.deployment.site_dir'));
|
||||||
|
$this->line('');
|
||||||
|
|
||||||
|
// Display deploy key information
|
||||||
|
$this->warn('🔑 Deploy Key Information:');
|
||||||
|
$this->line('Go to: ' . $repoInfo['deploy_keys_url']);
|
||||||
|
$this->line('');
|
||||||
|
|
||||||
|
$publicKey = $this->ssh->getPublicKey();
|
||||||
|
$this->line('Add this public key as a Deploy Key:');
|
||||||
|
$this->line('');
|
||||||
|
$this->line(' ' . str_repeat('-', 50));
|
||||||
|
$this->line($publicKey);
|
||||||
|
$this->line(' ' . str_repeat('-', 50));
|
||||||
|
$this->line('');
|
||||||
|
|
||||||
|
// Display next steps
|
||||||
|
$this->info('🎉 Setup completed! Next steps:');
|
||||||
|
$this->line('');
|
||||||
|
$this->line('1. Add all the secrets and variables shown above to GitHub');
|
||||||
|
$this->line('2. Add the deploy key to your repository');
|
||||||
|
$this->line('3. Run: php artisan hostinger:publish-workflow');
|
||||||
|
$this->line('4. Push your changes to trigger the workflow');
|
||||||
|
$this->line('');
|
||||||
|
$this->info('🚀 Your repository will now automatically deploy on push!');
|
||||||
|
}
|
||||||
|
}
|
||||||
237
src/Commands/DeploySharedCommand.php
Normal file
237
src/Commands/DeploySharedCommand.php
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Zura\HostingerDeploy\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Zura\HostingerDeploy\Services\SshConnectionService;
|
||||||
|
use Zura\HostingerDeploy\Services\GitHubActionsService;
|
||||||
|
|
||||||
|
class DeploySharedCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*/
|
||||||
|
protected $signature = 'hostinger:deploy-shared
|
||||||
|
{--fresh : Delete and clone fresh repository}
|
||||||
|
{--site-dir= : Override site directory from config}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*/
|
||||||
|
protected $description = 'Deploy Laravel application to Hostinger shared hosting';
|
||||||
|
|
||||||
|
protected SshConnectionService $ssh;
|
||||||
|
protected GitHubActionsService $github;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->github = new GitHubActionsService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$this->info('🚀 Starting Hostinger deployment...');
|
||||||
|
|
||||||
|
// Validate configuration
|
||||||
|
if (!$this->validateConfiguration()) {
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get repository URL
|
||||||
|
$repoUrl = $this->getRepositoryUrl();
|
||||||
|
if (!$repoUrl) {
|
||||||
|
$this->error('❌ Could not detect Git repository URL. Please run this command from a Git repository.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("📦 Repository: {$repoUrl}");
|
||||||
|
|
||||||
|
// Setup SSH connection
|
||||||
|
$this->setupSshConnection();
|
||||||
|
|
||||||
|
// Test SSH connection
|
||||||
|
if (!$this->ssh->testConnection()) {
|
||||||
|
$this->error('❌ SSH connection failed. Please check your SSH configuration.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('✅ SSH connection successful');
|
||||||
|
|
||||||
|
// Deploy to server
|
||||||
|
if (!$this->deployToServer($repoUrl)) {
|
||||||
|
$this->error('❌ Deployment failed');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('🎉 Deployment completed successfully!');
|
||||||
|
$this->info("🌐 Your Laravel application is now live at: https://{$this->getSiteDir()}");
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate required configuration.
|
||||||
|
*/
|
||||||
|
protected function validateConfiguration(): bool
|
||||||
|
{
|
||||||
|
$required = [
|
||||||
|
'HOSTINGER_SSH_HOST' => config('hostinger-deploy.ssh.host'),
|
||||||
|
'HOSTINGER_SSH_USERNAME' => config('hostinger-deploy.ssh.username'),
|
||||||
|
'HOSTINGER_SITE_DIR' => $this->getSiteDir(),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($required as $key => $value) {
|
||||||
|
if (empty($value)) {
|
||||||
|
$this->error("❌ Missing required environment variable: {$key}");
|
||||||
|
$this->info("Please add {$key} to your .env file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get repository URL from Git.
|
||||||
|
*/
|
||||||
|
protected function getRepositoryUrl(): ?string
|
||||||
|
{
|
||||||
|
$repoUrl = $this->github->getRepositoryUrl();
|
||||||
|
|
||||||
|
if (!$repoUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("✅ Detected Git repository: {$repoUrl}");
|
||||||
|
|
||||||
|
if (!$this->confirm('Use this repository for deployment?')) {
|
||||||
|
$repoUrl = $this->ask('Enter your Git repository URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $repoUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup SSH connection service.
|
||||||
|
*/
|
||||||
|
protected function setupSshConnection(): void
|
||||||
|
{
|
||||||
|
$this->ssh = new SshConnectionService(
|
||||||
|
config('hostinger-deploy.ssh.host'),
|
||||||
|
config('hostinger-deploy.ssh.username'),
|
||||||
|
config('hostinger-deploy.ssh.port', 22),
|
||||||
|
config('hostinger-deploy.ssh.timeout', 30)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deploy application to server.
|
||||||
|
*/
|
||||||
|
protected function deployToServer(string $repoUrl): bool
|
||||||
|
{
|
||||||
|
$siteDir = $this->getSiteDir();
|
||||||
|
$isFresh = $this->option('fresh');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Setup SSH keys if needed
|
||||||
|
$this->setupSshKeys();
|
||||||
|
|
||||||
|
// Prepare deployment commands
|
||||||
|
$commands = $this->buildDeploymentCommands($repoUrl, $siteDir, $isFresh);
|
||||||
|
|
||||||
|
// Execute deployment
|
||||||
|
$this->info('📦 Deploying application...');
|
||||||
|
$this->ssh->executeMultiple($commands);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error("Deployment error: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup SSH keys on server if needed.
|
||||||
|
*/
|
||||||
|
protected function setupSshKeys(): void
|
||||||
|
{
|
||||||
|
if (!$this->ssh->sshKeyExists()) {
|
||||||
|
$this->info('🔑 Generating SSH keys on server...');
|
||||||
|
$this->ssh->generateSshKey();
|
||||||
|
|
||||||
|
$publicKey = $this->ssh->getPublicKey();
|
||||||
|
if ($publicKey) {
|
||||||
|
$this->warn('🔑 Add this SSH key to your GitHub repository:');
|
||||||
|
$this->warn(' Settings → Deploy keys → Add deploy key');
|
||||||
|
$this->line('');
|
||||||
|
$this->line($publicKey);
|
||||||
|
$this->line('');
|
||||||
|
|
||||||
|
if (!$this->confirm('Press ENTER after adding the key to GitHub...')) {
|
||||||
|
throw new \Exception('SSH key setup cancelled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->info('🔑 SSH keys already configured');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build deployment commands.
|
||||||
|
*/
|
||||||
|
protected function buildDeploymentCommands(string $repoUrl, string $siteDir, bool $isFresh): array
|
||||||
|
{
|
||||||
|
$commands = [];
|
||||||
|
|
||||||
|
// Navigate to site directory
|
||||||
|
$commands[] = "mkdir -p domains/{$siteDir}";
|
||||||
|
$commands[] = "cd domains/{$siteDir}";
|
||||||
|
|
||||||
|
// Remove public_html if exists
|
||||||
|
$commands[] = "rm -rf public_html";
|
||||||
|
|
||||||
|
if ($isFresh) {
|
||||||
|
// Fresh deployment - delete everything and clone
|
||||||
|
$commands[] = "rm -rf * .[^.]* 2>/dev/null || true";
|
||||||
|
$commands[] = "git clone {$repoUrl} .";
|
||||||
|
} else {
|
||||||
|
// Check if repository exists
|
||||||
|
$commands[] = "if [ -d .git ]; then git pull; else git clone {$repoUrl} .; fi";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install dependencies
|
||||||
|
$composerFlags = config('hostinger-deploy.deployment.composer_flags', '--no-dev --optimize-autoloader');
|
||||||
|
$commands[] = "composer install {$composerFlags}";
|
||||||
|
|
||||||
|
// Copy .env.example to .env
|
||||||
|
$commands[] = "if [ -f .env.example ]; then cp .env.example .env; fi";
|
||||||
|
|
||||||
|
// Create symbolic link for Laravel public folder
|
||||||
|
$commands[] = "if [ -d public ]; then ln -s public public_html; fi";
|
||||||
|
|
||||||
|
// Laravel setup
|
||||||
|
$commands[] = "php artisan key:generate --quiet";
|
||||||
|
|
||||||
|
if (config('hostinger-deploy.deployment.run_migrations', true)) {
|
||||||
|
$commands[] = "echo 'yes' | php artisan migrate --quiet";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config('hostinger-deploy.deployment.run_storage_link', true)) {
|
||||||
|
$commands[] = "php artisan storage:link --quiet";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get site directory from option or config.
|
||||||
|
*/
|
||||||
|
protected function getSiteDir(): string
|
||||||
|
{
|
||||||
|
return $this->option('site-dir') ?: config('hostinger-deploy.deployment.site_dir');
|
||||||
|
}
|
||||||
|
}
|
||||||
155
src/Commands/PublishWorkflowCommand.php
Normal file
155
src/Commands/PublishWorkflowCommand.php
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Zura\HostingerDeploy\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Zura\HostingerDeploy\Services\GitHubActionsService;
|
||||||
|
|
||||||
|
class PublishWorkflowCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*/
|
||||||
|
protected $signature = 'hostinger:publish-workflow
|
||||||
|
{--branch= : Override default branch}
|
||||||
|
{--php-version= : Override PHP version}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*/
|
||||||
|
protected $description = 'Publish GitHub Actions workflow file for automated deployment';
|
||||||
|
|
||||||
|
protected GitHubActionsService $github;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->github = new GitHubActionsService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*/
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$this->info('📄 Publishing GitHub Actions workflow...');
|
||||||
|
|
||||||
|
// Check if we're in a Git repository
|
||||||
|
if (!$this->github->isGitRepository()) {
|
||||||
|
$this->error('❌ Not in a Git repository. Please run this command from a Git repository.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get repository information
|
||||||
|
$repoInfo = $this->github->getRepositoryInfo();
|
||||||
|
if (!$repoInfo) {
|
||||||
|
$this->error('❌ Could not detect repository information.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("📦 Repository: {$repoInfo['owner']}/{$repoInfo['name']}");
|
||||||
|
$this->info("🌿 Branch: {$repoInfo['branch']}");
|
||||||
|
|
||||||
|
// Get configuration
|
||||||
|
$branch = $this->option('branch') ?: $this->getBranch();
|
||||||
|
$phpVersion = $this->option('php-version') ?: config('hostinger-deploy.github.php_version', '8.3');
|
||||||
|
$workflowFile = config('hostinger-deploy.github.workflow_file', '.github/workflows/hostinger-deploy.yml');
|
||||||
|
|
||||||
|
// Create .github/workflows directory if it doesn't exist
|
||||||
|
$workflowDir = dirname($workflowFile);
|
||||||
|
if (!File::exists($workflowDir)) {
|
||||||
|
File::makeDirectory($workflowDir, 0755, true);
|
||||||
|
$this->info("📁 Created directory: {$workflowDir}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate workflow content
|
||||||
|
$workflowContent = $this->generateWorkflowContent($branch, $phpVersion);
|
||||||
|
|
||||||
|
// Write workflow file
|
||||||
|
if (File::put($workflowFile, $workflowContent)) {
|
||||||
|
$this->info("✅ Workflow file created: {$workflowFile}");
|
||||||
|
} else {
|
||||||
|
$this->error("❌ Failed to create workflow file: {$workflowFile}");
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display next steps
|
||||||
|
$this->displayNextSteps($repoInfo);
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the branch to use for the workflow.
|
||||||
|
*/
|
||||||
|
protected function getBranch(): string
|
||||||
|
{
|
||||||
|
$currentBranch = $this->github->getCurrentBranch();
|
||||||
|
$defaultBranch = config('hostinger-deploy.github.default_branch', 'main');
|
||||||
|
|
||||||
|
if ($currentBranch && $currentBranch !== $defaultBranch) {
|
||||||
|
if ($this->confirm("Use current branch '{$currentBranch}' for the workflow? (default: {$defaultBranch})")) {
|
||||||
|
return $currentBranch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $defaultBranch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate workflow content with placeholders replaced.
|
||||||
|
*/
|
||||||
|
protected function generateWorkflowContent(string $branch, string $phpVersion): string
|
||||||
|
{
|
||||||
|
$stubPath = __DIR__ . '/../../stubs/hostinger-deploy.yml';
|
||||||
|
|
||||||
|
if (!File::exists($stubPath)) {
|
||||||
|
throw new \Exception("Workflow stub not found: {$stubPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = File::get($stubPath);
|
||||||
|
|
||||||
|
// Replace placeholders
|
||||||
|
$content = str_replace('{{BRANCH}}', $branch, $content);
|
||||||
|
$content = str_replace('{{PHP_VERSION}}', $phpVersion, $content);
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display next steps for the user.
|
||||||
|
*/
|
||||||
|
protected function displayNextSteps(array $repoInfo): void
|
||||||
|
{
|
||||||
|
$this->line('');
|
||||||
|
$this->info('🎉 GitHub Actions workflow published successfully!');
|
||||||
|
$this->line('');
|
||||||
|
$this->warn('📋 Next steps:');
|
||||||
|
$this->line('');
|
||||||
|
$this->line('1. Add the following secrets to your GitHub repository:');
|
||||||
|
$this->line(' Go to: ' . $repoInfo['secrets_url']);
|
||||||
|
$this->line('');
|
||||||
|
$this->line(' Required secrets:');
|
||||||
|
$this->line(' - SSH_HOST: Your Hostinger server IP address');
|
||||||
|
$this->line(' - SSH_USERNAME: Your Hostinger SSH username');
|
||||||
|
$this->line(' - SSH_PORT: Your Hostinger SSH port (usually 22)');
|
||||||
|
$this->line(' - SSH_KEY: Your private SSH key');
|
||||||
|
$this->line('');
|
||||||
|
$this->line('2. Add the following variable to your GitHub repository:');
|
||||||
|
$this->line(' Go to: ' . $repoInfo['variables_url']);
|
||||||
|
$this->line('');
|
||||||
|
$this->line(' Required variable:');
|
||||||
|
$this->line(' - WEBSITE_FOLDER: Your Hostinger website folder name');
|
||||||
|
$this->line('');
|
||||||
|
$this->line('3. Run the auto-deploy command to get your SSH keys:');
|
||||||
|
$this->line(' php artisan hostinger:auto-deploy');
|
||||||
|
$this->line('');
|
||||||
|
$this->line('4. Push changes to trigger the workflow:');
|
||||||
|
$this->line(' git add .');
|
||||||
|
$this->line(' git commit -m "Add Hostinger deployment workflow"');
|
||||||
|
$this->line(' git push');
|
||||||
|
$this->line('');
|
||||||
|
$this->info('🚀 Your repository will now automatically deploy on push!');
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/HostingerDeployServiceProvider.php
Normal file
43
src/HostingerDeployServiceProvider.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Zura\HostingerDeploy;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Zura\HostingerDeploy\Commands\DeploySharedCommand;
|
||||||
|
use Zura\HostingerDeploy\Commands\PublishWorkflowCommand;
|
||||||
|
use Zura\HostingerDeploy\Commands\AutoDeployCommand;
|
||||||
|
|
||||||
|
class HostingerDeployServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
$this->mergeConfigFrom(
|
||||||
|
__DIR__.'/config/hostinger-deploy.php', 'hostinger-deploy'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
if ($this->app->runningInConsole()) {
|
||||||
|
$this->commands([
|
||||||
|
DeploySharedCommand::class,
|
||||||
|
PublishWorkflowCommand::class,
|
||||||
|
AutoDeployCommand::class,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->publishes([
|
||||||
|
__DIR__.'/config/hostinger-deploy.php' => config_path('hostinger-deploy.php'),
|
||||||
|
], 'hostinger-deploy-config');
|
||||||
|
|
||||||
|
$this->publishes([
|
||||||
|
__DIR__.'/../stubs/hostinger-deploy.yml' => base_path('.github/workflows/hostinger-deploy.yml'),
|
||||||
|
], 'hostinger-deploy-workflow');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
src/Services/GitHubActionsService.php
Normal file
138
src/Services/GitHubActionsService.php
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Zura\HostingerDeploy\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Process;
|
||||||
|
|
||||||
|
class GitHubActionsService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the current Git repository URL.
|
||||||
|
*/
|
||||||
|
public function getRepositoryUrl(): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = Process::run('git config --get remote.origin.url');
|
||||||
|
|
||||||
|
if (!$result->successful()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($result->output());
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current branch name.
|
||||||
|
*/
|
||||||
|
public function getCurrentBranch(): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = Process::run('git branch --show-current');
|
||||||
|
|
||||||
|
if (!$result->successful()) {
|
||||||
|
return 'main';
|
||||||
|
}
|
||||||
|
|
||||||
|
$branch = trim($result->output());
|
||||||
|
return $branch ?: 'main';
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return 'main';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract repository owner and name from Git URL.
|
||||||
|
*/
|
||||||
|
public function parseRepositoryUrl(string $url): ?array
|
||||||
|
{
|
||||||
|
// Handle SSH URLs: git@github.com:owner/repo.git
|
||||||
|
if (preg_match('/git@github\.com:([^\/]+)\/([^\/]+)\.git/', $url, $matches)) {
|
||||||
|
return [
|
||||||
|
'owner' => $matches[1],
|
||||||
|
'name' => $matches[2],
|
||||||
|
'url' => "https://github.com/{$matches[1]}/{$matches[2]}"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle HTTPS URLs: https://github.com/owner/repo.git
|
||||||
|
if (preg_match('/https:\/\/github\.com\/([^\/]+)\/([^\/]+)\.git/', $url, $matches)) {
|
||||||
|
return [
|
||||||
|
'owner' => $matches[1],
|
||||||
|
'name' => $matches[2],
|
||||||
|
'url' => "https://github.com/{$matches[1]}/{$matches[2]}"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get GitHub repository settings URL for secrets.
|
||||||
|
*/
|
||||||
|
public function getSecretsUrl(string $owner, string $name): string
|
||||||
|
{
|
||||||
|
return "https://github.com/{$owner}/{$name}/settings/secrets/actions";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get GitHub repository settings URL for variables.
|
||||||
|
*/
|
||||||
|
public function getVariablesUrl(string $owner, string $name): string
|
||||||
|
{
|
||||||
|
return "https://github.com/{$owner}/{$name}/settings/variables/actions";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get GitHub repository settings URL for deploy keys.
|
||||||
|
*/
|
||||||
|
public function getDeployKeysUrl(string $owner, string $name): string
|
||||||
|
{
|
||||||
|
return "https://github.com/{$owner}/{$name}/settings/keys";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we're in a Git repository.
|
||||||
|
*/
|
||||||
|
public function isGitRepository(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = Process::run('git rev-parse --git-dir');
|
||||||
|
return $result->successful();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get repository information for display.
|
||||||
|
*/
|
||||||
|
public function getRepositoryInfo(): ?array
|
||||||
|
{
|
||||||
|
if (!$this->isGitRepository()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = $this->getRepositoryUrl();
|
||||||
|
if (!$url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parsed = $this->parseRepositoryUrl($url);
|
||||||
|
if (!$parsed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'url' => $url,
|
||||||
|
'owner' => $parsed['owner'],
|
||||||
|
'name' => $parsed['name'],
|
||||||
|
'branch' => $this->getCurrentBranch(),
|
||||||
|
'secrets_url' => $this->getSecretsUrl($parsed['owner'], $parsed['name']),
|
||||||
|
'variables_url' => $this->getVariablesUrl($parsed['owner'], $parsed['name']),
|
||||||
|
'deploy_keys_url' => $this->getDeployKeysUrl($parsed['owner'], $parsed['name']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
153
src/Services/SshConnectionService.php
Normal file
153
src/Services/SshConnectionService.php
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Zura\HostingerDeploy\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Process;
|
||||||
|
use Illuminate\Process\Exceptions\ProcessFailedException;
|
||||||
|
|
||||||
|
class SshConnectionService
|
||||||
|
{
|
||||||
|
protected string $host;
|
||||||
|
protected string $username;
|
||||||
|
protected int $port;
|
||||||
|
protected int $timeout;
|
||||||
|
|
||||||
|
public function __construct(string $host, string $username, int $port = 22, int $timeout = 30)
|
||||||
|
{
|
||||||
|
$this->host = $host;
|
||||||
|
$this->username = $username;
|
||||||
|
$this->port = $port;
|
||||||
|
$this->timeout = $timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a command on the remote server via SSH.
|
||||||
|
*/
|
||||||
|
public function execute(string $command): string
|
||||||
|
{
|
||||||
|
$sshCommand = $this->buildSshCommand($command);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = Process::timeout($this->timeout)
|
||||||
|
->run($sshCommand);
|
||||||
|
|
||||||
|
if (!$result->successful()) {
|
||||||
|
throw new ProcessFailedException($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result->output();
|
||||||
|
} catch (ProcessFailedException $e) {
|
||||||
|
throw new \Exception("SSH command failed: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute multiple commands on the remote server.
|
||||||
|
*/
|
||||||
|
public function executeMultiple(array $commands): string
|
||||||
|
{
|
||||||
|
$combinedCommand = implode(' && ', $commands);
|
||||||
|
return $this->execute($combinedCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if SSH connection is working.
|
||||||
|
*/
|
||||||
|
public function testConnection(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->execute('echo "SSH connection test successful"');
|
||||||
|
return true;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the public key from the server.
|
||||||
|
*/
|
||||||
|
public function getPublicKey(): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return trim($this->execute('cat ~/.ssh/id_rsa.pub 2>/dev/null || echo ""'));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the private key from the server.
|
||||||
|
*/
|
||||||
|
public function getPrivateKey(): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return trim($this->execute('cat ~/.ssh/id_rsa 2>/dev/null || echo ""'));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate SSH key pair on the server if it doesn't exist.
|
||||||
|
*/
|
||||||
|
public function generateSshKey(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->execute('ssh-keygen -t rsa -b 4096 -C "github-deploy-key" -N "" -f ~/.ssh/id_rsa');
|
||||||
|
return true;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a public key to authorized_keys.
|
||||||
|
*/
|
||||||
|
public function addToAuthorizedKeys(string $publicKey): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->execute("echo '{$publicKey}' >> ~/.ssh/authorized_keys");
|
||||||
|
return true;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if SSH key exists on the server.
|
||||||
|
*/
|
||||||
|
public function sshKeyExists(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$result = $this->execute('test -f ~/.ssh/id_rsa && echo "exists" || echo "not_exists"');
|
||||||
|
return trim($result) === 'exists';
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the SSH command string.
|
||||||
|
*/
|
||||||
|
protected function buildSshCommand(string $command): string
|
||||||
|
{
|
||||||
|
$sshOptions = [
|
||||||
|
'-p ' . $this->port,
|
||||||
|
'-o ConnectTimeout=' . $this->timeout,
|
||||||
|
'-o StrictHostKeyChecking=no',
|
||||||
|
'-o UserKnownHostsFile=/dev/null',
|
||||||
|
];
|
||||||
|
|
||||||
|
$sshCommand = 'ssh ' . implode(' ', $sshOptions) . ' ' . $this->username . '@' . $this->host . ' "' . addslashes($command) . '"';
|
||||||
|
|
||||||
|
return $sshCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get connection details for display.
|
||||||
|
*/
|
||||||
|
public function getConnectionString(): string
|
||||||
|
{
|
||||||
|
return "ssh -p {$this->port} {$this->username}@{$this->host}";
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/config/hostinger-deploy.php
Normal file
65
src/config/hostinger-deploy.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| SSH Connection Settings
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These settings are used for SSH connections to your Hostinger server.
|
||||||
|
| You can override these in your .env file.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'ssh' => [
|
||||||
|
'host' => env('HOSTINGER_SSH_HOST'),
|
||||||
|
'username' => env('HOSTINGER_SSH_USERNAME'),
|
||||||
|
'port' => env('HOSTINGER_SSH_PORT', 22),
|
||||||
|
'timeout' => 30,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Deployment Settings
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configuration for the deployment process.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'deployment' => [
|
||||||
|
'site_dir' => env('HOSTINGER_SITE_DIR'),
|
||||||
|
'composer_flags' => '--no-dev --optimize-autoloader',
|
||||||
|
'run_migrations' => true,
|
||||||
|
'run_storage_link' => true,
|
||||||
|
'run_config_cache' => false,
|
||||||
|
'run_route_cache' => false,
|
||||||
|
'run_view_cache' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| GitHub Actions Settings
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configuration for GitHub Actions workflow generation.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'github' => [
|
||||||
|
'workflow_file' => '.github/workflows/hostinger-deploy.yml',
|
||||||
|
'php_version' => '8.3',
|
||||||
|
'default_branch' => 'main',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Server Paths
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Default paths on the Hostinger server.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'paths' => [
|
||||||
|
'domains' => 'domains',
|
||||||
|
'public_html' => 'public_html',
|
||||||
|
'public' => 'public',
|
||||||
|
],
|
||||||
|
];
|
||||||
51
stubs/hostinger-deploy.yml
Normal file
51
stubs/hostinger-deploy.yml
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: Hostinger Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ {{BRANCH}} ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '{{PHP_VERSION}}'
|
||||||
|
|
||||||
|
- name: Create .env
|
||||||
|
run: cp .env.example .env
|
||||||
|
|
||||||
|
- name: Install composer Dependencies
|
||||||
|
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
||||||
|
|
||||||
|
- name: Set Application Encryption Key
|
||||||
|
run: php artisan key:generate --ansi
|
||||||
|
|
||||||
|
- name: Create Storage Link
|
||||||
|
run: php artisan storage:link
|
||||||
|
|
||||||
|
- name: Run Migrations
|
||||||
|
run: php artisan migrate --force
|
||||||
|
|
||||||
|
- name: Deploy to Hostinger Server
|
||||||
|
if: ${{ success() }}
|
||||||
|
uses: appleboy/ssh-action@master
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USERNAME }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
script: |
|
||||||
|
cd domains/${{ vars.WEBSITE_FOLDER }}
|
||||||
|
git checkout {{BRANCH}}
|
||||||
|
git pull
|
||||||
|
composer install --no-dev --optimize-autoloader
|
||||||
|
php artisan migrate --force
|
||||||
|
php artisan config:cache
|
||||||
|
php artisan route:cache
|
||||||
|
php artisan view:cache
|
||||||
Reference in New Issue
Block a user