remove: all deploy UI/backend code, keep _deploy folder only
- Delete DeployRunCommand, DeployController, deploy API routes - Delete Dashboard/Deploy Vue pages and components - Remove deploy route from web.php - Remove deploy quick action from config - Remove DeployRunCommand from ConsoleKernel
This commit is contained in:
@@ -1,203 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Illuminate\Support\Facades\File;
|
|
||||||
use Illuminate\Support\Process;
|
|
||||||
|
|
||||||
class DeployRunCommand extends Command
|
|
||||||
{
|
|
||||||
protected $signature = 'deploy:run';
|
|
||||||
protected $description = 'Execute deployment process';
|
|
||||||
|
|
||||||
private string $statusFile = '/tmp/deploy-status.json';
|
|
||||||
private string $lockFile = '/tmp/deploy.lock';
|
|
||||||
private int $totalSteps = 13;
|
|
||||||
|
|
||||||
public function handle(): int
|
|
||||||
{
|
|
||||||
if (File::exists($this->lockFile)) {
|
|
||||||
$this->error('Deploy already in progress');
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
File::put($this->lockFile, (string) getpid());
|
|
||||||
$this->initStatus();
|
|
||||||
|
|
||||||
try {
|
|
||||||
$this->executeDeploy();
|
|
||||||
$this->updateStatus([
|
|
||||||
'running' => false,
|
|
||||||
'success' => true,
|
|
||||||
]);
|
|
||||||
return 0;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->handleError($e);
|
|
||||||
return 1;
|
|
||||||
} finally {
|
|
||||||
File::delete($this->lockFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function initStatus(): void
|
|
||||||
{
|
|
||||||
$this->writeStatus([
|
|
||||||
'running' => true,
|
|
||||||
'step' => 0,
|
|
||||||
'total_steps' => $this->totalSteps,
|
|
||||||
'current_step' => '',
|
|
||||||
'started_at' => now()->toIso8601String(),
|
|
||||||
'logs' => [],
|
|
||||||
'error' => null,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function writeStatus(array $data): void
|
|
||||||
{
|
|
||||||
File::put($this->statusFile, json_encode($data));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function updateStatus(array $updates): void
|
|
||||||
{
|
|
||||||
$status = json_decode(File::get($this->statusFile), true);
|
|
||||||
$status = array_merge($status, $updates);
|
|
||||||
$this->writeStatus($status);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function addLog(string $message): void
|
|
||||||
{
|
|
||||||
$status = json_decode(File::get($this->statusFile), true);
|
|
||||||
$status['logs'][] = '[' . now()->format('H:i:s') . '] ' . $message;
|
|
||||||
$this->writeStatus($status);
|
|
||||||
$this->line($message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function executeStep(int $step, string $name, callable $callback): void
|
|
||||||
{
|
|
||||||
$this->updateStatus([
|
|
||||||
'step' => $step,
|
|
||||||
'current_step' => $name,
|
|
||||||
]);
|
|
||||||
$this->addLog("Step {$step}/{$this->totalSteps}: {$name}");
|
|
||||||
|
|
||||||
$callback();
|
|
||||||
|
|
||||||
$this->addLog("✅ {$name} completed");
|
|
||||||
}
|
|
||||||
|
|
||||||
private function executeDeploy(): void
|
|
||||||
{
|
|
||||||
$this->executeStep(1, 'Maintenance mode', function () {
|
|
||||||
$this->execCommand('php artisan down');
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->executeStep(2, 'Git pull', function () {
|
|
||||||
$this->execCommand('git config --global --add safe.directory /var/www');
|
|
||||||
$this->execCommand('git reset --hard');
|
|
||||||
$this->execCommand('git pull origin master');
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->executeStep(3, 'Composer install', function () {
|
|
||||||
$this->execCommand('composer install --no-dev --no-interaction --prefer-dist --no-cache');
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->executeStep(4, 'Database backup', function () {
|
|
||||||
$backupDir = storage_path('app/backups');
|
|
||||||
if (!is_dir($backupDir)) {
|
|
||||||
mkdir($backupDir, 0755, true);
|
|
||||||
}
|
|
||||||
$filename = 'backup_' . now()->format('Y-m-d_H-i-s') . '.sql.gz';
|
|
||||||
$filepath = $backupDir . '/' . $filename;
|
|
||||||
$dbHost = env('DB_HOST', 'db');
|
|
||||||
$dbName = env('DB_DATABASE', 'ntspi_db');
|
|
||||||
$dbUser = env('DB_USERNAME', 'admin');
|
|
||||||
$dbPass = env('DB_PASSWORD', 'secret');
|
|
||||||
$this->execCommand(
|
|
||||||
"mysqldump -h {$dbHost} -u {$dbUser} -p{$dbPass} {$dbName} | gzip > {$filepath}"
|
|
||||||
);
|
|
||||||
$this->addLog("Backup saved: {$filepath}");
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->executeStep(5, 'Migrate', function () {
|
|
||||||
$this->execCommand('php artisan migrate --force');
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->executeStep(6, 'NPM install', function () {
|
|
||||||
$this->execCommand('npm install --legacy-peer-deps');
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->executeStep(7, 'NPM build', function () {
|
|
||||||
$this->execCommand('npm run build');
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->executeStep(8, 'Cache clear', function () {
|
|
||||||
$this->execCommand('php artisan cache:clear');
|
|
||||||
$this->execCommand('php artisan config:clear');
|
|
||||||
$this->execCommand('php artisan route:clear');
|
|
||||||
$this->execCommand('php artisan view:clear');
|
|
||||||
$this->execCommand('php artisan filament:clear-cached-components');
|
|
||||||
$this->execCommand('php artisan filament:optimize-clear');
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->executeStep(9, 'Cache warm', function () {
|
|
||||||
$this->execCommand('php artisan routes:register');
|
|
||||||
$this->execCommand('php artisan route:cache');
|
|
||||||
$this->execCommand('php artisan view:cache');
|
|
||||||
$this->execCommand('php artisan icons:cache');
|
|
||||||
$this->execCommand('php artisan filament:cache-components');
|
|
||||||
$this->execCommand('php artisan filament:optimize');
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->executeStep(10, 'Fix permissions', function () {
|
|
||||||
$this->execCommand('chown -R www-data:www-data storage bootstrap/cache');
|
|
||||||
$this->execCommand('chmod -R 775 storage bootstrap/cache');
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->executeStep(11, 'Restart processes', function () {
|
|
||||||
$this->execCommand('supervisorctl restart inertia-ssr');
|
|
||||||
$this->execCommand('supervisorctl restart cron');
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->executeStep(12, 'Restart queue workers', function () {
|
|
||||||
$this->execCommand('php artisan queue:restart');
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->executeStep(13, 'Maintenance off', function () {
|
|
||||||
$this->execCommand('php artisan up');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private function execCommand(string $command): void
|
|
||||||
{
|
|
||||||
$process = Process::run($command);
|
|
||||||
|
|
||||||
if ($process->exitCode() !== 0) {
|
|
||||||
throw new \RuntimeException(
|
|
||||||
"Command failed: {$command}\n" . $process->errorOutput()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function handleError(\Exception $e): void
|
|
||||||
{
|
|
||||||
$this->addLog("❌ Error: " . $e->getMessage());
|
|
||||||
|
|
||||||
$this->updateStatus([
|
|
||||||
'running' => false,
|
|
||||||
'success' => false,
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->addLog('Attempting rollback...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$this->execCommand('git checkout .');
|
|
||||||
$this->execCommand('composer install --no-dev --prefer-dist');
|
|
||||||
$this->execCommand('php artisan up');
|
|
||||||
$this->addLog('Rollback completed');
|
|
||||||
} catch (\Exception $rollbackException) {
|
|
||||||
$this->addLog("❌ Rollback failed: " . $rollbackException->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Containers\Dashboard\UI\API\Controllers;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Support\Facades\File;
|
|
||||||
use Illuminate\Support\Process;
|
|
||||||
|
|
||||||
class DeployController extends Controller
|
|
||||||
{
|
|
||||||
private string $statusFile = '/tmp/deploy-status.json';
|
|
||||||
private string $lockFile = '/tmp/deploy.lock';
|
|
||||||
|
|
||||||
public function store(): JsonResponse
|
|
||||||
{
|
|
||||||
if (File::exists($this->lockFile)) {
|
|
||||||
return response()->json([
|
|
||||||
'error' => 'Deploy already in progress'
|
|
||||||
], 409);
|
|
||||||
}
|
|
||||||
|
|
||||||
Process::run('nohup php artisan deploy:run > /dev/null 2>&1 &');
|
|
||||||
|
|
||||||
return response()->json(['message' => 'Deploy started']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function status(): JsonResponse
|
|
||||||
{
|
|
||||||
if (!File::exists($this->statusFile)) {
|
|
||||||
return response()->json([
|
|
||||||
'running' => false,
|
|
||||||
'step' => 0,
|
|
||||||
'total_steps' => 12,
|
|
||||||
'current_step' => '',
|
|
||||||
'logs' => [],
|
|
||||||
'error' => null,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$status = json_decode(File::get($this->statusFile), true);
|
|
||||||
return response()->json($status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use App\Containers\Dashboard\UI\API\Controllers\DeployController;
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
|
|
||||||
Route::middleware([
|
|
||||||
\App\Ship\Middleware\EncryptCookies::class,
|
|
||||||
\Illuminate\Session\Middleware\StartSession::class,
|
|
||||||
\App\Ship\Middleware\VerifyCsrfToken::class,
|
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
|
||||||
'superadmin',
|
|
||||||
])->group(function () {
|
|
||||||
Route::post('/deploy', [DeployController::class, 'store']);
|
|
||||||
Route::get('/deploy/status', [DeployController::class, 'status']);
|
|
||||||
});
|
|
||||||
@@ -416,11 +416,6 @@ Route::middleware(['access-check', 'dashboard.auth'])->group(function () {
|
|||||||
Route::put('/{credential}', [IntegrationCredentialsController::class, 'update'])->name('update');
|
Route::put('/{credential}', [IntegrationCredentialsController::class, 'update'])->name('update');
|
||||||
Route::delete('/{credential}', [IntegrationCredentialsController::class, 'destroy'])->name('destroy');
|
Route::delete('/{credential}', [IntegrationCredentialsController::class, 'destroy'])->name('destroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Deploy
|
|
||||||
Route::get('/dashboard/deploy', function () {
|
|
||||||
return inertia()->render('Dashboard/Deploy/Index');
|
|
||||||
})->name('dashboard.deploy');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace App\Ship\Kernels;
|
|||||||
use AlxDorosenco\PortoForLaravel\Loaders\CommandsLoader;
|
use AlxDorosenco\PortoForLaravel\Loaders\CommandsLoader;
|
||||||
use AlxDorosenco\PortoForLaravel\Loaders\RoutesLoader;
|
use AlxDorosenco\PortoForLaravel\Loaders\RoutesLoader;
|
||||||
use App\Containers\Dashboard\Commands\FetchEmailNewsCommand;
|
use App\Containers\Dashboard\Commands\FetchEmailNewsCommand;
|
||||||
use App\Console\Commands\DeployRunCommand;
|
|
||||||
use App\Ship\Commands\InitRoles;
|
use App\Ship\Commands\InitRoles;
|
||||||
use Illuminate\Console\Scheduling\Schedule;
|
use Illuminate\Console\Scheduling\Schedule;
|
||||||
use Illuminate\Foundation\Console\Kernel as LaravelConsoleKernel;
|
use Illuminate\Foundation\Console\Kernel as LaravelConsoleKernel;
|
||||||
@@ -35,7 +34,6 @@ class ConsoleKernel extends LaravelConsoleKernel
|
|||||||
protected $commands = [
|
protected $commands = [
|
||||||
InitRoles::class,
|
InitRoles::class,
|
||||||
FetchEmailNewsCommand::class,
|
FetchEmailNewsCommand::class,
|
||||||
DeployRunCommand::class,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -31,10 +31,4 @@ export const quickActions = [
|
|||||||
href: null,
|
href: null,
|
||||||
icon: 'cog',
|
icon: 'cog',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Deploy',
|
|
||||||
route: 'dashboard.deploy',
|
|
||||||
href: null,
|
|
||||||
icon: 'cog',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
|
||||||
import DeployButton from './components/DeployButton.vue'
|
|
||||||
import DeployProgress from './components/DeployProgress.vue'
|
|
||||||
import DeployLogs from './components/DeployLogs.vue'
|
|
||||||
|
|
||||||
const status = ref({
|
|
||||||
running: false,
|
|
||||||
step: 0,
|
|
||||||
total_steps: 12,
|
|
||||||
current_step: '',
|
|
||||||
logs: [],
|
|
||||||
error: null,
|
|
||||||
})
|
|
||||||
|
|
||||||
let pollingInterval = null
|
|
||||||
|
|
||||||
const getCsrfToken = () => {
|
|
||||||
const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/)
|
|
||||||
return match ? decodeURIComponent(match[1]) : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-XSRF-TOKEN': getCsrfToken(),
|
|
||||||
'Accept': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchStatus = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/deploy/status', {
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
const text = await response.text()
|
|
||||||
|
|
||||||
try {
|
|
||||||
status.value = JSON.parse(text)
|
|
||||||
} catch {
|
|
||||||
console.error('Status response is not JSON:', text.substring(0, 200))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!status.value.running && pollingInterval) {
|
|
||||||
clearInterval(pollingInterval)
|
|
||||||
pollingInterval = null
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch status:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const startDeploy = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/deploy', {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
const text = await response.text()
|
|
||||||
console.log('Deploy response:', response.status, text)
|
|
||||||
|
|
||||||
let data
|
|
||||||
try {
|
|
||||||
data = JSON.parse(text)
|
|
||||||
} catch {
|
|
||||||
alert('Server returned HTML instead of JSON. Check console.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
startPolling()
|
|
||||||
} else {
|
|
||||||
alert(data.error || 'Failed to start deploy')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
alert('Failed to start deploy: ' + error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const startPolling = () => {
|
|
||||||
if (pollingInterval) return
|
|
||||||
pollingInterval = setInterval(fetchStatus, 2000)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchStatus()
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (pollingInterval) {
|
|
||||||
clearInterval(pollingInterval)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="max-w-4xl mx-auto p-6">
|
|
||||||
<h1 class="text-2xl font-bold mb-6">Deploy</h1>
|
|
||||||
|
|
||||||
<DeployButton
|
|
||||||
:running="status.running"
|
|
||||||
@deploy="startDeploy"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DeployProgress
|
|
||||||
v-if="status.running || status.step > 0"
|
|
||||||
:step="status.step"
|
|
||||||
:total-steps="status.total_steps"
|
|
||||||
:current-step="status.current_step"
|
|
||||||
:success="status.success"
|
|
||||||
:error="status.error"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DeployLogs :logs="status.logs" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
defineProps({
|
|
||||||
running: Boolean,
|
|
||||||
})
|
|
||||||
|
|
||||||
defineEmits(['deploy'])
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<button
|
|
||||||
@click="$emit('deploy')"
|
|
||||||
:disabled="running"
|
|
||||||
class="px-6 py-3 bg-blue-600 text-white rounded-lg font-medium
|
|
||||||
hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed
|
|
||||||
transition-colors"
|
|
||||||
>
|
|
||||||
<span v-if="running">Deploying...</span>
|
|
||||||
<span v-else>🚀 Deploy to Production</span>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, watch, nextTick } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
logs: Array,
|
|
||||||
})
|
|
||||||
|
|
||||||
const logsContainer = ref(null)
|
|
||||||
|
|
||||||
watch(() => props.logs, async () => {
|
|
||||||
await nextTick()
|
|
||||||
if (logsContainer.value) {
|
|
||||||
logsContainer.value.scrollTop = logsContainer.value.scrollHeight
|
|
||||||
}
|
|
||||||
}, { deep: true })
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="mt-6">
|
|
||||||
<h3 class="text-lg font-medium mb-3">Logs</h3>
|
|
||||||
<div
|
|
||||||
ref="logsContainer"
|
|
||||||
class="bg-gray-900 text-green-400 p-4 rounded-lg h-64 overflow-y-auto font-mono text-sm"
|
|
||||||
>
|
|
||||||
<div v-if="logs.length === 0" class="text-gray-500">
|
|
||||||
No logs yet...
|
|
||||||
</div>
|
|
||||||
<div v-for="(log, index) in logs" :key="index">
|
|
||||||
{{ log }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
step: Number,
|
|
||||||
totalSteps: Number,
|
|
||||||
currentStep: String,
|
|
||||||
success: Boolean,
|
|
||||||
error: String,
|
|
||||||
})
|
|
||||||
|
|
||||||
const progress = computed(() => (props.step / props.totalSteps) * 100)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="mt-6 p-4 bg-white rounded-lg shadow">
|
|
||||||
<div class="mb-2 flex justify-between text-sm">
|
|
||||||
<span>{{ currentStep }}</span>
|
|
||||||
<span>{{ step }}/{{ totalSteps }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w-full bg-gray-200 rounded-full h-3">
|
|
||||||
<div
|
|
||||||
class="h-3 rounded-full transition-all duration-500"
|
|
||||||
:class="{
|
|
||||||
'bg-blue-600': !success && !error,
|
|
||||||
'bg-green-600': success,
|
|
||||||
'bg-red-600': error
|
|
||||||
}"
|
|
||||||
:style="{ width: progress + '%' }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="error" class="mt-3 p-3 bg-red-100 text-red-700 rounded">
|
|
||||||
❌ {{ error }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="success" class="mt-3 p-3 bg-green-100 text-green-700 rounded">
|
|
||||||
✅ Deploy completed successfully
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
Reference in New Issue
Block a user