Admin: move deploy flow into settings and use secrets file

This commit is contained in:
2026-02-21 15:50:39 +03:00
parent 27691ba396
commit bc48e6959d
11 changed files with 305 additions and 136 deletions
+97
View File
@@ -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));
}
+59 -2
View File
@@ -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
View File
@@ -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;