Initial commit

This commit is contained in:
Zura Sekhniashvili
2025-10-30 23:51:25 +04:00
commit 8fcb9aead2
12 changed files with 1351 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/vendor/
composer.lock
.phpunit.result.cache
.phpunit.cache
.DS_Store
Thumbs.db

21
LICENSE Normal file
View 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
View 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
View 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
}
}

View 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!');
}
}

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

View 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!');
}
}

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

View 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']),
];
}
}

View 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}";
}
}

View 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',
],
];

View 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