Admin: allow configurable deploy remote for update checks

This commit is contained in:
Alexander Andreev 2026-02-22 05:04:55 +03:00
parent 9be6bf50dd
commit 8aee3a64ea
6 changed files with 108 additions and 20 deletions

View File

@ -83,6 +83,12 @@ cp config.php.example config.php
2. Заполни доступы к БД в `config.php`. 2. Заполни доступы к БД в `config.php`.
Для деплоя из админки в `config.php` можно задать:
- `deploy.remote_name` (обычно `origin`),
- `deploy.remote_url` (по умолчанию `git@github.com:wrkandreev/reframe.git`),
- `deploy.branch` (`main` или `dev`).
3. Создай `secrets.php`: 3. Создай `secrets.php`:
```bash ```bash
@ -155,15 +161,16 @@ php scripts/generate_thumbs.php
Деплой запускается из админки (вкладка `Настройки`): Деплой запускается из админки (вкладка `Настройки`):
- кнопка `Проверить обновления` делает `git fetch` и сравнивает `HEAD` с `origin/<branch>`, - кнопка `Проверить обновления` делает `git fetch` и сравнивает `HEAD` с `<remote>/<branch>`,
- если локальная ветка отстает и не расходится (`behind > 0`, `ahead = 0`) — показывается кнопка `Обновить проект`. - если локальная ветка отстает и не расходится (`behind > 0`, `ahead = 0`) — показывается кнопка `Обновить проект`.
Скрипт `scripts/deploy.sh`: Скрипт `scripts/deploy.sh`:
1. делает `git fetch --all --prune`, 1. настраивает remote из `REMOTE_NAME`/`REMOTE_URL` (если передан `REMOTE_URL`),
2. переключает код на `origin/<branch>` через `git reset --hard`, 2. делает `git fetch <remote> <branch> --prune`,
3. запускает миграции `php scripts/migrate.php`, 3. переключает код на `<remote>/<branch>` через `git reset --hard`,
4. сохраняет runtime-папки (`photos`, `thumbs`, `data`). 4. запускает миграции `php scripts/migrate.php`,
5. сохраняет runtime-папки (`photos`, `thumbs`, `data`).
Важно: деплой-скрипт перетирает рабочие изменения в репозитории на сервере. Важно: деплой-скрипт перетирает рабочие изменения в репозитории на сервере.

View File

@ -48,6 +48,11 @@ if ($tokenExpected === '' || !hash_equals($tokenExpected, $tokenIncoming)) {
} }
$deployConfig = (array)($config['deploy'] ?? []); $deployConfig = (array)($config['deploy'] ?? []);
$deployRemoteName = trim((string)($deployConfig['remote_name'] ?? 'origin'));
if ($deployRemoteName === '') {
$deployRemoteName = 'origin';
}
$deployRemoteUrl = trim((string)($deployConfig['remote_url'] ?? ''));
$allowedDeployBranches = ['main', 'dev']; $allowedDeployBranches = ['main', 'dev'];
$defaultDeployBranch = trim((string)($deployConfig['branch'] ?? 'main')); $defaultDeployBranch = trim((string)($deployConfig['branch'] ?? 'main'));
if (!in_array($defaultDeployBranch, $allowedDeployBranches, true)) { if (!in_array($defaultDeployBranch, $allowedDeployBranches, true)) {
@ -83,6 +88,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try { try {
$result = adminHandlePostAction($action, $isAjax, __DIR__, [ $result = adminHandlePostAction($action, $isAjax, __DIR__, [
'remote_name' => $deployRemoteName,
'remote_url' => $deployRemoteUrl,
'branch' => $deployBranch, 'branch' => $deployBranch,
'script' => $deployScript, 'script' => $deployScript,
'php_bin' => $deployPhpBin, 'php_bin' => $deployPhpBin,
@ -285,10 +292,12 @@ function assetUrl(string $path): string { $f=__DIR__ . '/' . ltrim($path,'/'); $
<hr style="border:none;border-top:1px solid #eee;margin:12px 0"> <hr style="border:none;border-top:1px solid #eee;margin:12px 0">
<h4 style="margin:0 0 8px">Обновление проекта</h4> <h4 style="margin:0 0 8px">Обновление проекта</h4>
<p class="small" style="margin:0">Выбери ветку для проверки и обновления: <strong><?= h($deployBranch) ?></strong></p> <p class="small" style="margin:0">Remote: <strong><?= h($deployRemoteName) ?></strong><?= $deployRemoteUrl !== '' ? ' (' . h($deployRemoteUrl) . ')' : '' ?></p>
<p class="small" style="margin:4px 0 0">Выбери ветку для проверки и обновления: <strong><?= h($deployBranch) ?></strong></p>
<?php if (is_array($deployStatus)): ?> <?php if (is_array($deployStatus)): ?>
<?php $deployState = (string)($deployStatus['state'] ?? ''); ?> <?php $deployState = (string)($deployStatus['state'] ?? ''); ?>
<?php $statusRemoteName = (string)($deployStatus['remote_name'] ?? $deployRemoteName); ?>
<?php $statusBranch = (string)($deployStatus['branch'] ?? $deployBranch); ?> <?php $statusBranch = (string)($deployStatus['branch'] ?? $deployBranch); ?>
<?php $deployStateMessage = $deployState === 'update_available' <?php $deployStateMessage = $deployState === 'update_available'
? 'Доступна новая версия.' ? 'Доступна новая версия.'
@ -299,7 +308,7 @@ function assetUrl(string $path): string { $f=__DIR__ . '/' . ltrim($path,'/'); $
: 'Ветка расходится с origin. Нужна ручная синхронизация.')); ?> : 'Ветка расходится с origin. Нужна ручная синхронизация.')); ?>
<div class="<?= in_array($deployState, ['local_ahead', 'diverged'], true) ? 'err' : 'ok' ?>" style="margin-top:8px"> <div class="<?= in_array($deployState, ['local_ahead', 'diverged'], true) ? 'err' : 'ok' ?>" style="margin-top:8px">
<?= h($deployStateMessage) ?><br> <?= h($deployStateMessage) ?><br>
<span class="small">Локально: <?= h((string)($deployStatus['local_ref'] ?? '-')) ?> · origin/<?= h($statusBranch) ?>: <?= h((string)($deployStatus['remote_ref'] ?? '-')) ?> · behind: <?= (int)($deployStatus['behind'] ?? 0) ?> · ahead: <?= (int)($deployStatus['ahead'] ?? 0) ?></span> <span class="small">Локально: <?= h((string)($deployStatus['local_ref'] ?? '-')) ?> · <?= h($statusRemoteName) ?>/<?= h($statusBranch) ?>: <?= h((string)($deployStatus['remote_ref'] ?? '-')) ?> · behind: <?= (int)($deployStatus['behind'] ?? 0) ?> · ahead: <?= (int)($deployStatus['ahead'] ?? 0) ?></span>
</div> </div>
<?php endif; ?> <?php endif; ?>

View File

@ -9,6 +9,8 @@ return [
'charset' => 'utf8mb4', 'charset' => 'utf8mb4',
], ],
'deploy' => [ 'deploy' => [
'remote_name' => 'origin',
'remote_url' => 'git@github.com:wrkandreev/reframe.git',
'branch' => 'main', 'branch' => 'main',
'script' => __DIR__ . '/scripts/deploy.sh', 'script' => __DIR__ . '/scripts/deploy.sh',
'php_bin' => 'php', 'php_bin' => 'php',

View File

@ -2,21 +2,26 @@
declare(strict_types=1); declare(strict_types=1);
function adminCheckForUpdates(string $projectRoot, string $branch): array function adminCheckForUpdates(string $projectRoot, string $branch, string $remoteName = 'origin', string $remoteUrl = ''): array
{ {
if (!is_dir($projectRoot . '/.git')) { if (!is_dir($projectRoot . '/.git')) {
throw new RuntimeException('Репозиторий не найден: .git отсутствует'); throw new RuntimeException('Репозиторий не найден: .git отсутствует');
} }
$fetch = adminRunShellCommand('git fetch origin ' . escapeshellarg($branch) . ' --prune', $projectRoot); $remoteName = adminNormalizeRemoteName($remoteName);
adminEnsureRemote($projectRoot, $remoteName, $remoteUrl);
$remoteRef = $remoteName . '/' . $branch;
$fetch = adminRunShellCommand('git fetch ' . escapeshellarg($remoteName) . ' ' . escapeshellarg($branch) . ' --prune', $projectRoot);
if ($fetch['code'] !== 0) { if ($fetch['code'] !== 0) {
throw new RuntimeException('Не удалось обновить данные из origin: ' . adminTailOutput($fetch['output'])); throw new RuntimeException('Не удалось обновить данные из ' . $remoteName . ': ' . adminTailOutput($fetch['output']));
} }
$local = adminRunShellCommand('git rev-parse --short=12 HEAD', $projectRoot); $local = adminRunShellCommand('git rev-parse --short=12 HEAD', $projectRoot);
$remote = adminRunShellCommand('git rev-parse --short=12 origin/' . escapeshellarg($branch), $projectRoot); $remote = adminRunShellCommand('git rev-parse --short=12 ' . escapeshellarg($remoteRef), $projectRoot);
$behindRaw = adminRunShellCommand('git rev-list --count HEAD..origin/' . escapeshellarg($branch), $projectRoot); $behindRaw = adminRunShellCommand('git rev-list --count HEAD..' . escapeshellarg($remoteRef), $projectRoot);
$aheadRaw = adminRunShellCommand('git rev-list --count origin/' . escapeshellarg($branch) . '..HEAD', $projectRoot); $aheadRaw = adminRunShellCommand('git rev-list --count ' . escapeshellarg($remoteRef) . '..HEAD', $projectRoot);
if ($local['code'] !== 0 || $remote['code'] !== 0 || $behindRaw['code'] !== 0 || $aheadRaw['code'] !== 0) { if ($local['code'] !== 0 || $remote['code'] !== 0 || $behindRaw['code'] !== 0 || $aheadRaw['code'] !== 0) {
throw new RuntimeException('Не удалось определить состояние ветки'); throw new RuntimeException('Не удалось определить состояние ветки');
@ -36,6 +41,7 @@ function adminCheckForUpdates(string $projectRoot, string $branch): array
return [ return [
'state' => $state, 'state' => $state,
'remote_name' => $remoteName,
'branch' => $branch, 'branch' => $branch,
'local_ref' => trim($local['output']), 'local_ref' => trim($local['output']),
'remote_ref' => trim($remote['output']), 'remote_ref' => trim($remote['output']),
@ -45,15 +51,19 @@ function adminCheckForUpdates(string $projectRoot, string $branch): array
]; ];
} }
function adminRunDeployScript(string $projectRoot, string $branch, string $scriptPath, string $phpBin): array function adminRunDeployScript(string $projectRoot, string $branch, string $scriptPath, string $phpBin, string $remoteName = 'origin', string $remoteUrl = ''): array
{ {
if (!is_file($scriptPath)) { if (!is_file($scriptPath)) {
throw new RuntimeException('Скрипт деплоя не найден: ' . $scriptPath); throw new RuntimeException('Скрипт деплоя не найден: ' . $scriptPath);
} }
$remoteName = adminNormalizeRemoteName($remoteName);
$run = adminRunShellCommand('bash ' . escapeshellarg($scriptPath), $projectRoot, [ $run = adminRunShellCommand('bash ' . escapeshellarg($scriptPath), $projectRoot, [
'BRANCH' => $branch, 'BRANCH' => $branch,
'PHP_BIN' => $phpBin, 'PHP_BIN' => $phpBin,
'REMOTE_NAME' => $remoteName,
'REMOTE_URL' => $remoteUrl,
]); ]);
return [ return [
@ -63,6 +73,50 @@ function adminRunDeployScript(string $projectRoot, string $branch, string $scrip
]; ];
} }
function adminEnsureRemote(string $projectRoot, string $remoteName, string $remoteUrl): void
{
$getRemote = adminRunShellCommand('git remote get-url ' . escapeshellarg($remoteName), $projectRoot);
if ($getRemote['code'] !== 0) {
if ($remoteUrl === '') {
throw new RuntimeException('Remote ' . $remoteName . ' не найден');
}
$add = adminRunShellCommand('git remote add ' . escapeshellarg($remoteName) . ' ' . escapeshellarg($remoteUrl), $projectRoot);
if ($add['code'] !== 0) {
throw new RuntimeException('Не удалось добавить remote ' . $remoteName . ': ' . adminTailOutput($add['output']));
}
return;
}
if ($remoteUrl === '') {
return;
}
$currentUrl = trim($getRemote['output']);
if ($currentUrl === $remoteUrl) {
return;
}
$set = adminRunShellCommand('git remote set-url ' . escapeshellarg($remoteName) . ' ' . escapeshellarg($remoteUrl), $projectRoot);
if ($set['code'] !== 0) {
throw new RuntimeException('Не удалось обновить remote ' . $remoteName . ': ' . adminTailOutput($set['output']));
}
}
function adminNormalizeRemoteName(string $remoteName): string
{
$remoteName = trim($remoteName);
if ($remoteName === '') {
return 'origin';
}
if (!preg_match('/^[A-Za-z0-9._-]+$/', $remoteName)) {
throw new RuntimeException('Некорректное имя remote');
}
return $remoteName;
}
function adminRunShellCommand(string $command, string $cwd, array $env = []): array function adminRunShellCommand(string $command, string $cwd, array $env = []): array
{ {
$envPrefix = ''; $envPrefix = '';

View File

@ -144,8 +144,10 @@ function adminHandlePostAction(string $action, bool $isAjax, string $projectRoot
} }
case 'check_updates': { case 'check_updates': {
$remoteName = (string)($deployOptions['remote_name'] ?? 'origin');
$remoteUrl = (string)($deployOptions['remote_url'] ?? '');
$branch = (string)($deployOptions['branch'] ?? 'main'); $branch = (string)($deployOptions['branch'] ?? 'main');
$deployStatus = adminCheckForUpdates($projectRoot, $branch); $deployStatus = adminCheckForUpdates($projectRoot, $branch, $remoteName, $remoteUrl);
$state = (string)($deployStatus['state'] ?? ''); $state = (string)($deployStatus['state'] ?? '');
if ($state === 'update_available') { if ($state === 'update_available') {
@ -162,11 +164,13 @@ function adminHandlePostAction(string $action, bool $isAjax, string $projectRoot
} }
case 'deploy_updates': { case 'deploy_updates': {
$remoteName = (string)($deployOptions['remote_name'] ?? 'origin');
$remoteUrl = (string)($deployOptions['remote_url'] ?? '');
$branch = (string)($deployOptions['branch'] ?? 'main'); $branch = (string)($deployOptions['branch'] ?? 'main');
$scriptPath = (string)($deployOptions['script'] ?? ($projectRoot . '/scripts/deploy.sh')); $scriptPath = (string)($deployOptions['script'] ?? ($projectRoot . '/scripts/deploy.sh'));
$phpBin = (string)($deployOptions['php_bin'] ?? 'php'); $phpBin = (string)($deployOptions['php_bin'] ?? 'php');
$deployStatus = adminCheckForUpdates($projectRoot, $branch); $deployStatus = adminCheckForUpdates($projectRoot, $branch, $remoteName, $remoteUrl);
if (!(bool)($deployStatus['can_deploy'] ?? false)) { if (!(bool)($deployStatus['can_deploy'] ?? false)) {
$state = (string)($deployStatus['state'] ?? ''); $state = (string)($deployStatus['state'] ?? '');
if ($state === 'up_to_date') { if ($state === 'up_to_date') {
@ -182,13 +186,13 @@ function adminHandlePostAction(string $action, bool $isAjax, string $projectRoot
throw new RuntimeException('Нельзя применить обновление в текущем состоянии ветки.'); throw new RuntimeException('Нельзя применить обновление в текущем состоянии ветки.');
} }
$deployResult = adminRunDeployScript($projectRoot, $branch, $scriptPath, $phpBin); $deployResult = adminRunDeployScript($projectRoot, $branch, $scriptPath, $phpBin, $remoteName, $remoteUrl);
$deployOutput = (string)($deployResult['output'] ?? ''); $deployOutput = (string)($deployResult['output'] ?? '');
if (!(bool)($deployResult['ok'] ?? false)) { if (!(bool)($deployResult['ok'] ?? false)) {
throw new RuntimeException('Деплой завершился с ошибкой: ' . ($deployOutput !== '' ? $deployOutput : ('код ' . (int)($deployResult['code'] ?? 1)))); throw new RuntimeException('Деплой завершился с ошибкой: ' . ($deployOutput !== '' ? $deployOutput : ('код ' . (int)($deployResult['code'] ?? 1))));
} }
$deployStatus = adminCheckForUpdates($projectRoot, $branch); $deployStatus = adminCheckForUpdates($projectRoot, $branch, $remoteName, $remoteUrl);
$message = 'Обновление выполнено.'; $message = 'Обновление выполнено.';
break; break;
} }

View File

@ -6,10 +6,14 @@ set -euo pipefail
# bash scripts/deploy.sh # bash scripts/deploy.sh
# Optional env: # Optional env:
# APP_DIR=/home/USER/www/photo-gallery # APP_DIR=/home/USER/www/photo-gallery
# REMOTE_NAME=origin
# REMOTE_URL=git@github.com:wrkandreev/reframe.git
# BRANCH=main # BRANCH=main
# PHP_BIN=php # PHP_BIN=php
APP_DIR="${APP_DIR:-$(cd "$(dirname "$0")/.." && pwd)}" APP_DIR="${APP_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
REMOTE_NAME="${REMOTE_NAME:-origin}"
REMOTE_URL="${REMOTE_URL:-}"
BRANCH="${BRANCH:-main}" BRANCH="${BRANCH:-main}"
PHP_BIN="${PHP_BIN:-php}" PHP_BIN="${PHP_BIN:-php}"
@ -41,8 +45,16 @@ if [ "$current_branch" != "$BRANCH" ]; then
git checkout "$BRANCH" git checkout "$BRANCH"
fi fi
git fetch --all --prune if [ -n "$REMOTE_URL" ]; then
git reset --hard "origin/$BRANCH" if git remote get-url "$REMOTE_NAME" >/dev/null 2>&1; then
git remote set-url "$REMOTE_NAME" "$REMOTE_URL"
else
git remote add "$REMOTE_NAME" "$REMOTE_URL"
fi
fi
git fetch "$REMOTE_NAME" "$BRANCH" --prune
git reset --hard "$REMOTE_NAME/$BRANCH"
# Run DB migrations required by current code # Run DB migrations required by current code
"$PHP_BIN" scripts/migrate.php "$PHP_BIN" scripts/migrate.php