Create a new script to deploy and setup automated deployment
This commit is contained in:
110
src/Commands/DeployAndSetupAutomatedCommand.php
Normal file
110
src/Commands/DeployAndSetupAutomatedCommand.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Zura\HostingerDeploy\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class DeployAndSetupAutomatedCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*/
|
||||
protected $signature = 'hostinger:deploy-and-setup-automated
|
||||
{--fresh : Delete and clone fresh repository}
|
||||
{--site-dir= : Override site directory from config}
|
||||
{--token= : GitHub API token}
|
||||
{--branch= : Override default branch}
|
||||
{--php-version= : Override PHP version}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*/
|
||||
protected $description = 'Deploy Laravel application to Hostinger and setup automated deployment via GitHub API';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('🚀 Starting complete deployment and automated setup...');
|
||||
$this->line('');
|
||||
|
||||
// Step 1: Deploy to server
|
||||
$this->info('═══════════════════════════════════════════════════════');
|
||||
$this->info('Step 1: Deploying to Hostinger Server');
|
||||
$this->info('═══════════════════════════════════════════════════════');
|
||||
$this->line('');
|
||||
|
||||
$deployOptions = [];
|
||||
if ($this->option('fresh')) {
|
||||
$deployOptions['--fresh'] = true;
|
||||
}
|
||||
if ($this->option('site-dir')) {
|
||||
$deployOptions['--site-dir'] = $this->option('site-dir');
|
||||
}
|
||||
|
||||
// Call the deploy command - output will be shown in real-time
|
||||
// Pass through verbosity level to ensure all output is shown
|
||||
$deployOptions['-v'] = true;
|
||||
$deployExitCode = $this->call('hostinger:deploy-shared', $deployOptions);
|
||||
|
||||
if ($deployExitCode !== self::SUCCESS) {
|
||||
$this->line('');
|
||||
$this->error('❌ Deployment to server failed. Cannot proceed with automated setup.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->line('');
|
||||
$this->info('✅ Deployment to server completed successfully!');
|
||||
$this->line('');
|
||||
|
||||
// Step 2: Setup automated deployment
|
||||
$this->info('═══════════════════════════════════════════════════════');
|
||||
$this->info('Step 2: Setting up Automated Deployment');
|
||||
$this->info('═══════════════════════════════════════════════════════');
|
||||
$this->line('');
|
||||
|
||||
$setupOptions = [];
|
||||
if ($this->option('token')) {
|
||||
$setupOptions['--token'] = $this->option('token');
|
||||
}
|
||||
if ($this->option('branch')) {
|
||||
$setupOptions['--branch'] = $this->option('branch');
|
||||
}
|
||||
if ($this->option('php-version')) {
|
||||
$setupOptions['--php-version'] = $this->option('php-version');
|
||||
}
|
||||
|
||||
// Call the setup command - output will be shown in real-time
|
||||
// Pass through verbosity level to ensure all output is shown
|
||||
$setupOptions['-v'] = true;
|
||||
$setupExitCode = $this->call('hostinger:setup-automated-deploy', $setupOptions);
|
||||
|
||||
if ($setupExitCode !== self::SUCCESS) {
|
||||
$this->line('');
|
||||
$this->error('❌ Automated deployment setup failed.');
|
||||
$this->warn('⚠️ Your application is deployed but automated deployment is not configured.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$siteDir = $this->option('site-dir') ?: config('hostinger-deploy.deployment.site_dir');
|
||||
|
||||
$this->line('');
|
||||
$this->info('═══════════════════════════════════════════════════════');
|
||||
$this->info('🎉 Complete Setup Finished Successfully!');
|
||||
$this->info('═══════════════════════════════════════════════════════');
|
||||
$this->line('');
|
||||
$this->info('✅ Your Laravel application is deployed and configured for automated deployment!');
|
||||
$this->line('');
|
||||
$this->info("🌐 Your Laravel application: https://{$siteDir}");
|
||||
$this->line('');
|
||||
$this->info('🚀 Next steps:');
|
||||
$this->line(' 1. Push your code to trigger the GitHub Actions workflow');
|
||||
$this->line(' 2. Monitor deployments in the Actions tab on GitHub');
|
||||
$this->line(' 3. Your application will automatically deploy on push');
|
||||
$this->line('');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,9 +194,7 @@ class DeploySharedCommand extends Command
|
||||
$this->line($publicKey);
|
||||
$this->line('');
|
||||
|
||||
if (!$this->confirm('Press ENTER after adding the key to GitHub...')) {
|
||||
throw new \Exception('SSH key setup cancelled');
|
||||
}
|
||||
$this->ask('Press ENTER after adding the key to GitHub to continue...', '');
|
||||
}
|
||||
} else {
|
||||
$this->info('🔑 SSH keys already configured');
|
||||
|
||||
309
src/Commands/SetupAutomatedDeployCommand.php
Normal file
309
src/Commands/SetupAutomatedDeployCommand.php
Normal file
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
|
||||
namespace Zura\HostingerDeploy\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Zura\HostingerDeploy\Services\SshConnectionService;
|
||||
use Zura\HostingerDeploy\Services\GitHubActionsService;
|
||||
use Zura\HostingerDeploy\Services\GitHubAPIService;
|
||||
|
||||
class SetupAutomatedDeployCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*/
|
||||
protected $signature = 'hostinger:setup-automated-deploy
|
||||
{--token= : GitHub API token}
|
||||
{--branch= : Override default branch}
|
||||
{--php-version= : Override PHP version}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*/
|
||||
protected $description = 'Setup automated deployment using GitHub API (creates workflow and secrets)';
|
||||
|
||||
protected SshConnectionService $ssh;
|
||||
protected GitHubActionsService $github;
|
||||
protected ?GitHubAPIService $githubAPI = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->github = new GitHubActionsService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('🚀 Setting up automated deployment via GitHub API...');
|
||||
|
||||
// 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']}");
|
||||
|
||||
// Initialize GitHub API
|
||||
if (!$this->initializeGitHubAPI()) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
// 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 on server
|
||||
if (!$this->setupSshKeys()) {
|
||||
$this->error('❌ Failed to setup SSH keys');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
// Get SSH information
|
||||
$sshHost = config('hostinger-deploy.ssh.host');
|
||||
$sshUsername = config('hostinger-deploy.ssh.username');
|
||||
$sshPort = config('hostinger-deploy.ssh.port', 22);
|
||||
$privateKey = $this->ssh->getPrivateKey();
|
||||
|
||||
if (!$privateKey) {
|
||||
$this->error('❌ Could not retrieve private key from server');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
// Create workflow file
|
||||
if (!$this->createWorkflowFile($repoInfo)) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
// Get site directory
|
||||
$siteDir = config('hostinger-deploy.deployment.site_dir');
|
||||
|
||||
// Create secrets (including WEBSITE_FOLDER)
|
||||
if (!$this->createSecrets($repoInfo, $sshHost, $sshUsername, $sshPort, $privateKey, $siteDir)) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->line('');
|
||||
$this->info('🎉 Automated deployment setup completed successfully!');
|
||||
$this->line('');
|
||||
$this->info("🌐 Your Laravel application: https://{$siteDir}");
|
||||
$this->line('');
|
||||
$this->info('🚀 Your repository will now automatically deploy on push to the configured branch!');
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize GitHub API service.
|
||||
*/
|
||||
protected function initializeGitHubAPI(): bool
|
||||
{
|
||||
try {
|
||||
$token = $this->option('token') ?: env('GITHUB_API_TOKEN');
|
||||
|
||||
if (!$token) {
|
||||
$this->error('❌ GitHub API token is required.');
|
||||
$this->line('');
|
||||
$this->warn('💡 Please provide your GitHub API token:');
|
||||
$this->line(' 1. Set GITHUB_API_TOKEN in your .env file, or');
|
||||
$this->line(' 2. Use --token=YOUR_TOKEN option');
|
||||
$this->line('');
|
||||
$this->info(' Create a token at: https://github.com/settings/tokens');
|
||||
$this->info(' Required scopes: repo, workflow');
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->githubAPI = new GitHubAPIService($token);
|
||||
|
||||
// Test API connection
|
||||
if (!$this->githubAPI->testConnection()) {
|
||||
$this->error('❌ Failed to authenticate with GitHub API. Please check your token.');
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->info('✅ GitHub API connection successful');
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
$this->error("❌ GitHub API error: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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');
|
||||
}
|
||||
|
||||
// Add public key to authorized_keys
|
||||
$publicKey = $this->ssh->getPublicKey();
|
||||
if ($publicKey && !$this->ssh->addToAuthorizedKeys($publicKey)) {
|
||||
$this->warn('⚠️ Could not add public key to authorized_keys (may already exist)');
|
||||
}
|
||||
|
||||
$this->info('✅ SSH keys setup completed');
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
$this->error("SSH keys setup error: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create workflow file via GitHub API.
|
||||
*/
|
||||
protected function createWorkflowFile(array $repoInfo): bool
|
||||
{
|
||||
try {
|
||||
$this->info('📄 Creating GitHub Actions workflow file...');
|
||||
|
||||
// Get branch
|
||||
$branch = $this->option('branch') ?: $this->github->getCurrentBranch() ?: config('hostinger-deploy.github.default_branch', 'main');
|
||||
$phpVersion = $this->option('php-version') ?: config('hostinger-deploy.github.php_version', '8.3');
|
||||
|
||||
// Generate workflow content
|
||||
$workflowContent = $this->generateWorkflowContent($branch, $phpVersion);
|
||||
|
||||
// Create or update workflow file via API
|
||||
$this->githubAPI->createOrUpdateWorkflowFile(
|
||||
$repoInfo['owner'],
|
||||
$repoInfo['name'],
|
||||
$branch,
|
||||
$workflowContent
|
||||
);
|
||||
|
||||
$this->info('✅ Workflow file created successfully');
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
$this->error("❌ Failed to create workflow file: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate workflow content.
|
||||
*/
|
||||
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);
|
||||
$content = str_replace('{{BRANCH}}', $branch, $content);
|
||||
$content = str_replace('{{PHP_VERSION}}', $phpVersion, $content);
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create secrets via GitHub API.
|
||||
*/
|
||||
protected function createSecrets(array $repoInfo, string $sshHost, string $sshUsername, int $sshPort, string $sshKey, string $siteDir): bool
|
||||
{
|
||||
try {
|
||||
$this->info('🔒 Creating GitHub secrets...');
|
||||
|
||||
$secrets = [
|
||||
'SSH_HOST' => $sshHost,
|
||||
'SSH_USERNAME' => $sshUsername,
|
||||
'SSH_PORT' => (string) $sshPort,
|
||||
'SSH_KEY' => $sshKey,
|
||||
'WEBSITE_FOLDER' => $siteDir,
|
||||
];
|
||||
|
||||
foreach ($secrets as $name => $value) {
|
||||
$this->githubAPI->createOrUpdateSecret(
|
||||
$repoInfo['owner'],
|
||||
$repoInfo['name'],
|
||||
$name,
|
||||
$value
|
||||
);
|
||||
$this->info(" ✅ {$name} created");
|
||||
}
|
||||
|
||||
$this->info('✅ All secrets created successfully');
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
$this->error("❌ Failed to create secrets: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,8 @@ use Zura\HostingerDeploy\Commands\DeploySharedCommand;
|
||||
use Zura\HostingerDeploy\Commands\PublishWorkflowCommand;
|
||||
use Zura\HostingerDeploy\Commands\AutoDeployCommand;
|
||||
use Zura\HostingerDeploy\Commands\SetupEnvCommand;
|
||||
use Zura\HostingerDeploy\Commands\SetupAutomatedDeployCommand;
|
||||
use Zura\HostingerDeploy\Commands\DeployAndSetupAutomatedCommand;
|
||||
|
||||
class HostingerDeployServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -31,6 +33,8 @@ class HostingerDeployServiceProvider extends ServiceProvider
|
||||
PublishWorkflowCommand::class,
|
||||
AutoDeployCommand::class,
|
||||
SetupEnvCommand::class,
|
||||
SetupAutomatedDeployCommand::class,
|
||||
DeployAndSetupAutomatedCommand::class,
|
||||
]);
|
||||
|
||||
$this->publishes([
|
||||
|
||||
199
src/Services/GitHubAPIService.php
Normal file
199
src/Services/GitHubAPIService.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace Zura\HostingerDeploy\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Exception;
|
||||
|
||||
class GitHubAPIService
|
||||
{
|
||||
protected string $token;
|
||||
protected string $baseUrl = 'https://api.github.com';
|
||||
|
||||
public function __construct(?string $token = null)
|
||||
{
|
||||
$this->token = $token ?: config('hostinger-deploy.github.api_token') ?: env('GITHUB_API_TOKEN');
|
||||
|
||||
if (!$this->token) {
|
||||
throw new Exception('GitHub API token is required. Set GITHUB_API_TOKEN in your .env file.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repository public key for encrypting secrets.
|
||||
*/
|
||||
public function getRepositoryPublicKey(string $owner, string $repo): array
|
||||
{
|
||||
$response = Http::withHeaders([
|
||||
'Accept' => 'application/vnd.github.v3+json',
|
||||
'Authorization' => "Bearer {$this->token}",
|
||||
'X-GitHub-Api-Version' => '2022-11-28',
|
||||
])->get("{$this->baseUrl}/repos/{$owner}/{$repo}/actions/secrets/public-key");
|
||||
|
||||
if (!$response->successful()) {
|
||||
throw new Exception("Failed to get repository public key: " . $response->body());
|
||||
}
|
||||
|
||||
return $response->json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a secret value using LibSodium (GitHub uses NaCl Box encryption).
|
||||
* Based on GitHub API documentation: https://docs.github.com/en/rest/actions/secrets
|
||||
*/
|
||||
public function encryptSecret(string $plaintext, string $publicKey, string $keyId): array
|
||||
{
|
||||
if (!extension_loaded('sodium')) {
|
||||
throw new Exception('LibSodium extension is required for encrypting secrets. Install php-sodium extension.');
|
||||
}
|
||||
|
||||
// Decode the base64 public key
|
||||
$publicKeyBinary = base64_decode($publicKey, true);
|
||||
|
||||
if ($publicKeyBinary === false) {
|
||||
throw new Exception('Failed to decode public key');
|
||||
}
|
||||
|
||||
// GitHub uses NaCl Box sealed encryption (anonymous encryption)
|
||||
// This automatically handles ephemeral key pair generation
|
||||
$encrypted = sodium_crypto_box_seal($plaintext, $publicKeyBinary);
|
||||
|
||||
if ($encrypted === false) {
|
||||
throw new Exception('Failed to encrypt secret');
|
||||
}
|
||||
|
||||
// Encode to base64 for API
|
||||
// Sealed box automatically includes ephemeral public key in the ciphertext
|
||||
$encryptedValue = base64_encode($encrypted);
|
||||
|
||||
return [
|
||||
'encrypted_value' => $encryptedValue,
|
||||
'key_id' => $keyId,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update a repository secret.
|
||||
*/
|
||||
public function createOrUpdateSecret(string $owner, string $repo, string $secretName, string $plaintextValue): bool
|
||||
{
|
||||
// Get public key
|
||||
$publicKeyData = $this->getRepositoryPublicKey($owner, $repo);
|
||||
$publicKey = $publicKeyData['key'];
|
||||
$keyId = $publicKeyData['key_id'];
|
||||
|
||||
// Encrypt the secret
|
||||
$encryptedData = $this->encryptSecret($plaintextValue, $publicKey, $keyId);
|
||||
|
||||
// Create or update the secret
|
||||
$response = Http::withHeaders([
|
||||
'Accept' => 'application/vnd.github.v3+json',
|
||||
'Authorization' => "Bearer {$this->token}",
|
||||
'X-GitHub-Api-Version' => '2022-11-28',
|
||||
])->put(
|
||||
"{$this->baseUrl}/repos/{$owner}/{$repo}/actions/secrets/{$secretName}",
|
||||
$encryptedData
|
||||
);
|
||||
|
||||
if (!$response->successful()) {
|
||||
throw new Exception("Failed to create/update secret {$secretName}: " . $response->body());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update a repository variable.
|
||||
*/
|
||||
public function createOrUpdateVariable(string $owner, string $repo, string $variableName, string $value): bool
|
||||
{
|
||||
// Check if variable exists first
|
||||
$existingResponse = Http::withHeaders([
|
||||
'Accept' => 'application/vnd.github.v3+json',
|
||||
'Authorization' => "Bearer {$this->token}",
|
||||
'X-GitHub-Api-Version' => '2022-11-28',
|
||||
])->get("{$this->baseUrl}/repos/{$owner}/{$repo}/actions/variables/{$variableName}");
|
||||
|
||||
$method = $existingResponse->successful() ? 'PATCH' : 'POST';
|
||||
$url = "{$this->baseUrl}/repos/{$owner}/{$repo}/actions/variables" .
|
||||
($method === 'PATCH' ? "/{$variableName}" : '');
|
||||
|
||||
$response = Http::withHeaders([
|
||||
'Accept' => 'application/vnd.github.v3+json',
|
||||
'Authorization' => "Bearer {$this->token}",
|
||||
'X-GitHub-Api-Version' => '2022-11-28',
|
||||
])->{strtolower($method)}($url, [
|
||||
'name' => $variableName,
|
||||
'value' => $value,
|
||||
]);
|
||||
|
||||
if (!$response->successful()) {
|
||||
throw new Exception("Failed to create/update variable {$variableName}: " . $response->body());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update a workflow file.
|
||||
*/
|
||||
public function createOrUpdateWorkflowFile(string $owner, string $repo, string $branch, string $workflowContent): bool
|
||||
{
|
||||
$filePath = '.github/workflows/hostinger-deploy.yml';
|
||||
|
||||
// Check if file exists
|
||||
$existingResponse = Http::withHeaders([
|
||||
'Accept' => 'application/vnd.github.v3+json',
|
||||
'Authorization' => "Bearer {$this->token}",
|
||||
'X-GitHub-Api-Version' => '2022-11-28',
|
||||
])->get("{$this->baseUrl}/repos/{$owner}/{$repo}/contents/{$filePath}");
|
||||
|
||||
$sha = null;
|
||||
if ($existingResponse->successful()) {
|
||||
$sha = $existingResponse->json()['sha'];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'message' => $sha ? 'Update Hostinger deployment workflow' : 'Add Hostinger deployment workflow',
|
||||
'content' => base64_encode($workflowContent),
|
||||
'branch' => $branch,
|
||||
];
|
||||
|
||||
if ($sha) {
|
||||
$data['sha'] = $sha;
|
||||
}
|
||||
|
||||
$response = Http::withHeaders([
|
||||
'Accept' => 'application/vnd.github.v3+json',
|
||||
'Authorization' => "Bearer {$this->token}",
|
||||
'X-GitHub-Api-Version' => '2022-11-28',
|
||||
])->put(
|
||||
"{$this->baseUrl}/repos/{$owner}/{$repo}/contents/{$filePath}",
|
||||
$data
|
||||
);
|
||||
|
||||
if (!$response->successful()) {
|
||||
throw new Exception("Failed to create/update workflow file: " . $response->body());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test API connection.
|
||||
*/
|
||||
public function testConnection(): bool
|
||||
{
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
'Accept' => 'application/vnd.github.v3+json',
|
||||
'Authorization' => "Bearer {$this->token}",
|
||||
'X-GitHub-Api-Version' => '2022-11-28',
|
||||
])->get("{$this->baseUrl}/user");
|
||||
|
||||
return $response->successful();
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,11 +101,20 @@ class SshConnectionService
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a public key to authorized_keys.
|
||||
* Add a public key to authorized_keys if it doesn't already exist.
|
||||
*/
|
||||
public function addToAuthorizedKeys(string $publicKey): bool
|
||||
{
|
||||
try {
|
||||
// Check if the key already exists in authorized_keys
|
||||
$keyExists = $this->keyExistsInAuthorizedKeys($publicKey);
|
||||
|
||||
if ($keyExists) {
|
||||
// Key already exists, don't add it again
|
||||
return true;
|
||||
}
|
||||
|
||||
// Key doesn't exist, add it
|
||||
$this->execute("echo '{$publicKey}' >> ~/.ssh/authorized_keys");
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
@@ -113,6 +122,33 @@ class SshConnectionService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a public key already exists in authorized_keys.
|
||||
*/
|
||||
public function keyExistsInAuthorizedKeys(string $publicKey): bool
|
||||
{
|
||||
try {
|
||||
// Extract the key part (without comment) for comparison
|
||||
$keyParts = explode(' ', trim($publicKey));
|
||||
if (count($keyParts) < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$keyData = $keyParts[1]; // The actual key data (middle part)
|
||||
|
||||
// Check if this key data exists in authorized_keys
|
||||
// Use grep with escaped key data to avoid special character issues
|
||||
$escapedKeyData = escapeshellarg($keyData);
|
||||
$command = "grep -Fq {$escapedKeyData} ~/.ssh/authorized_keys 2>/dev/null && echo 'exists' || echo 'not_exists'";
|
||||
$result = trim($this->execute($command));
|
||||
|
||||
return $result === 'exists';
|
||||
} catch (\Exception $e) {
|
||||
// If we can't check, assume it doesn't exist
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SSH key exists on the server.
|
||||
*/
|
||||
|
||||
@@ -47,6 +47,7 @@ return [
|
||||
'workflow_file' => '.github/workflows/hostinger-deploy.yml',
|
||||
'php_version' => '8.3',
|
||||
'default_branch' => 'main',
|
||||
'api_token' => env('GITHUB_API_TOKEN'),
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
port: ${{ secrets.SSH_PORT }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
script: |
|
||||
cd domains/${{ vars.WEBSITE_FOLDER }}
|
||||
cd domains/${{ secrets.WEBSITE_FOLDER }}
|
||||
git checkout {{BRANCH}}
|
||||
git pull
|
||||
composer install --no-dev --optimize-autoloader
|
||||
|
||||
Reference in New Issue
Block a user