Admin: move deploy flow into settings and use secrets file
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
function adminCheckForUpdates(string $projectRoot, string $branch): array
|
||||
{
|
||||
if (!is_dir($projectRoot . '/.git')) {
|
||||
throw new RuntimeException('Репозиторий не найден: .git отсутствует');
|
||||
}
|
||||
|
||||
$fetch = adminRunShellCommand('git fetch origin ' . escapeshellarg($branch) . ' --prune', $projectRoot);
|
||||
if ($fetch['code'] !== 0) {
|
||||
throw new RuntimeException('Не удалось обновить данные из origin: ' . adminTailOutput($fetch['output']));
|
||||
}
|
||||
|
||||
$local = adminRunShellCommand('git rev-parse --short=12 HEAD', $projectRoot);
|
||||
$remote = adminRunShellCommand('git rev-parse --short=12 origin/' . escapeshellarg($branch), $projectRoot);
|
||||
$behindRaw = adminRunShellCommand('git rev-list --count HEAD..origin/' . escapeshellarg($branch), $projectRoot);
|
||||
$aheadRaw = adminRunShellCommand('git rev-list --count origin/' . escapeshellarg($branch) . '..HEAD', $projectRoot);
|
||||
|
||||
if ($local['code'] !== 0 || $remote['code'] !== 0 || $behindRaw['code'] !== 0 || $aheadRaw['code'] !== 0) {
|
||||
throw new RuntimeException('Не удалось определить состояние ветки');
|
||||
}
|
||||
|
||||
$behind = (int)trim($behindRaw['output']);
|
||||
$ahead = (int)trim($aheadRaw['output']);
|
||||
|
||||
$state = 'up_to_date';
|
||||
if ($behind > 0 && $ahead === 0) {
|
||||
$state = 'update_available';
|
||||
} elseif ($ahead > 0 && $behind === 0) {
|
||||
$state = 'local_ahead';
|
||||
} elseif ($ahead > 0 && $behind > 0) {
|
||||
$state = 'diverged';
|
||||
}
|
||||
|
||||
return [
|
||||
'state' => $state,
|
||||
'branch' => $branch,
|
||||
'local_ref' => trim($local['output']),
|
||||
'remote_ref' => trim($remote['output']),
|
||||
'behind' => $behind,
|
||||
'ahead' => $ahead,
|
||||
'can_deploy' => $state === 'update_available',
|
||||
];
|
||||
}
|
||||
|
||||
function adminRunDeployScript(string $projectRoot, string $branch, string $scriptPath, string $phpBin): array
|
||||
{
|
||||
if (!is_file($scriptPath)) {
|
||||
throw new RuntimeException('Скрипт деплоя не найден: ' . $scriptPath);
|
||||
}
|
||||
|
||||
$run = adminRunShellCommand('bash ' . escapeshellarg($scriptPath), $projectRoot, [
|
||||
'BRANCH' => $branch,
|
||||
'PHP_BIN' => $phpBin,
|
||||
]);
|
||||
|
||||
return [
|
||||
'ok' => $run['code'] === 0,
|
||||
'code' => $run['code'],
|
||||
'output' => adminTailOutput($run['output']),
|
||||
];
|
||||
}
|
||||
|
||||
function adminRunShellCommand(string $command, string $cwd, array $env = []): array
|
||||
{
|
||||
$envPrefix = '';
|
||||
foreach ($env as $key => $value) {
|
||||
if (!preg_match('/^[A-Z_][A-Z0-9_]*$/', (string)$key)) {
|
||||
continue;
|
||||
}
|
||||
$envPrefix .= $key . '=' . escapeshellarg((string)$value) . ' ';
|
||||
}
|
||||
|
||||
$fullCommand = 'cd ' . escapeshellarg($cwd) . ' && ' . $envPrefix . $command . ' 2>&1';
|
||||
$output = [];
|
||||
$code = 0;
|
||||
exec($fullCommand, $output, $code);
|
||||
|
||||
return ['code' => $code, 'output' => implode("\n", $output)];
|
||||
}
|
||||
|
||||
function adminTailOutput(string $output, int $maxLines = 80): string
|
||||
{
|
||||
$output = trim($output);
|
||||
if ($output === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$lines = preg_split('/\r\n|\r|\n/', $output);
|
||||
if (!is_array($lines)) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
return implode("\n", array_slice($lines, -$maxLines));
|
||||
}
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
function adminHandlePostAction(string $action, bool $isAjax, string $projectRoot): array
|
||||
function adminHandlePostAction(string $action, bool $isAjax, string $projectRoot, array $deployOptions = []): array
|
||||
{
|
||||
$message = '';
|
||||
$errors = [];
|
||||
$deployStatus = null;
|
||||
$deployOutput = '';
|
||||
|
||||
switch ($action) {
|
||||
case 'create_section': {
|
||||
@@ -141,6 +143,56 @@ function adminHandlePostAction(string $action, bool $isAjax, string $projectRoot
|
||||
break;
|
||||
}
|
||||
|
||||
case 'check_updates': {
|
||||
$branch = (string)($deployOptions['branch'] ?? 'main');
|
||||
$deployStatus = adminCheckForUpdates($projectRoot, $branch);
|
||||
$state = (string)($deployStatus['state'] ?? '');
|
||||
|
||||
if ($state === 'update_available') {
|
||||
$message = 'Найдена новая версия. Можно обновиться.';
|
||||
} elseif ($state === 'up_to_date') {
|
||||
$message = 'Обновлений нет: установлена актуальная версия.';
|
||||
} elseif ($state === 'local_ahead') {
|
||||
$message = 'Локальная ветка опережает origin. Автообновление отключено.';
|
||||
} else {
|
||||
$message = 'Ветка расходится с origin. Нужна ручная синхронизация.';
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'deploy_updates': {
|
||||
$branch = (string)($deployOptions['branch'] ?? 'main');
|
||||
$scriptPath = (string)($deployOptions['script'] ?? ($projectRoot . '/scripts/deploy.sh'));
|
||||
$phpBin = (string)($deployOptions['php_bin'] ?? 'php');
|
||||
|
||||
$deployStatus = adminCheckForUpdates($projectRoot, $branch);
|
||||
if (!(bool)($deployStatus['can_deploy'] ?? false)) {
|
||||
$state = (string)($deployStatus['state'] ?? '');
|
||||
if ($state === 'up_to_date') {
|
||||
$message = 'Обновление не требуется: уже актуальная версия.';
|
||||
break;
|
||||
}
|
||||
if ($state === 'local_ahead') {
|
||||
throw new RuntimeException('Локальная ветка опережает origin. Автообновление отключено.');
|
||||
}
|
||||
if ($state === 'diverged') {
|
||||
throw new RuntimeException('Ветка расходится с origin. Выполни ручную синхронизацию.');
|
||||
}
|
||||
throw new RuntimeException('Нельзя применить обновление в текущем состоянии ветки.');
|
||||
}
|
||||
|
||||
$deployResult = adminRunDeployScript($projectRoot, $branch, $scriptPath, $phpBin);
|
||||
$deployOutput = (string)($deployResult['output'] ?? '');
|
||||
if (!(bool)($deployResult['ok'] ?? false)) {
|
||||
throw new RuntimeException('Деплой завершился с ошибкой: ' . ($deployOutput !== '' ? $deployOutput : ('код ' . (int)($deployResult['code'] ?? 1))));
|
||||
}
|
||||
|
||||
$deployStatus = adminCheckForUpdates($projectRoot, $branch);
|
||||
$message = 'Обновление выполнено.';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'upload_before_bulk': {
|
||||
$sectionId = (int)($_POST['section_id'] ?? 0);
|
||||
if ($sectionId < 1 || !sectionById($sectionId)) {
|
||||
@@ -392,5 +444,10 @@ function adminHandlePostAction(string $action, bool $isAjax, string $projectRoot
|
||||
}
|
||||
}
|
||||
|
||||
return ['message' => $message, 'errors' => $errors];
|
||||
return [
|
||||
'message' => $message,
|
||||
'errors' => $errors,
|
||||
'deploy_status' => $deployStatus,
|
||||
'deploy_output' => $deployOutput,
|
||||
];
|
||||
}
|
||||
|
||||
+20
@@ -22,6 +22,26 @@ function appConfig(): array
|
||||
return $cfg;
|
||||
}
|
||||
|
||||
function appSecrets(): array
|
||||
{
|
||||
static $secrets = null;
|
||||
if ($secrets !== null) {
|
||||
return $secrets;
|
||||
}
|
||||
|
||||
$path = __DIR__ . '/../secrets.php';
|
||||
if (!is_file($path)) {
|
||||
throw new RuntimeException('secrets.php not found. Copy secrets.php.example');
|
||||
}
|
||||
|
||||
$secrets = require $path;
|
||||
if (!is_array($secrets)) {
|
||||
throw new RuntimeException('Invalid secrets.php format');
|
||||
}
|
||||
|
||||
return $secrets;
|
||||
}
|
||||
|
||||
function db(): PDO
|
||||
{
|
||||
static $pdo = null;
|
||||
|
||||
Reference in New Issue
Block a user