feat: единая админка runtime-конфигов и indpic (#22)
deploy-preland / deploy zoomstage.online (push) Successful in 1m1s

This commit is contained in:
Panfilov Konstantin committed 2026-10-25 11:33:56 +03:00
1 parent 5b6b2d2053
commit 9515c1c62b
52 files changed
+2459 -34

No files matched your search

+11 -3
View File
@@ -50,9 +50,9 @@ foreach ([
$workflow = (string) file_get_contents($root . '/.gitea/workflows/deploy.yml');
foreach ([
'"$PRELAND_URL/1/01_desk_1-source.avif"',
'"$PRELAND_URL/2/01_desk_1-source.avif"',
'2/01_mob_1-source.avif',
'php bin/release-assets.php --stage-sets "$PRELAND_URL"',
'php bin/release-assets.php --remote "$PRELAND_URL"',
'php-imagick',
'Remove obsolete demo directories',
] as $requiredStageSmokeCheck) {
if (!str_contains($workflow, $requiredStageSmokeCheck)) {
@@ -60,6 +60,14 @@ foreach ([
}
}
// #22: реальный runtime config выбирает наборы; удаление неиспользуемого /2/
// не должно блокировать выпуск. Обязательные файлы/размеры проверяет guard.
require_once $root . '/bin/release-assets.php';
foreach (releaseFiles($root) as $file) {
if (preg_match('~^(admin/|config\.json$|[0-9]+/)~', $file)) $failures[] = 'Runtime/приватный файл в поставке: ' . $file;
}
if (runtimeSetNumbers(['utm' => ['indpic' => ['default' => '3', 'weights' => ['3' => 1]]]]) !== [3]) $failures[] = 'Наборы должны определяться фактическим config';
if (!str_contains($workflow, '|[0-9]+/)')) {
$failures[] = 'Stage-деплой не должен перезаписывать корневые наборы /N/: stage владеет их содержимым.';
}
+54
View File
@@ -0,0 +1,54 @@
<?php
/** preland#22: runtime-контент независим от Git, контракт изображений обязателен. */
declare(strict_types=1);
require_once dirname(__DIR__) . '/bin/check-runtime-sets.php';
require_once dirname(__DIR__) . '/bin/release-assets.php';
$root = dirname(__DIR__);
$fixture = sys_get_temp_dir() . '/preland22-' . bin2hex(random_bytes(6));
mkdir($fixture);
mkdir($fixture . '/1');
$assertFails = static function (callable $operation): void {
try { $operation(); } catch (RuntimeException $expected) { return; }
throw new LogicException('Повреждённый runtime-контракт принят');
};
try {
foreach (runtimeSetDimensions() as $name => $dimensions) copy($root . '/1/' . $name, $fixture . '/1/' . $name);
$config = ['utm' => ['indpic' => ['default' => '1', 'weights' => ['1' => 1]]]];
// Замена изображения корректным новым изображением не требует Git SHA.
$image = new Imagick($fixture . '/1/01_desk_1-source.jpg');
$image->setImageProperty('comment', 'runtime replacement preland#22');
$image->writeImage($fixture . '/1/01_desk_1-source.jpg');
$image->clear();
if (hash_file('sha256', $fixture . '/1/01_desk_1-source.jpg') === hash_file('sha256', $root . '/1/01_desk_1-source.jpg')) throw new LogicException('Fixture не изменён');
checkRuntimeSets($config, runtimeLocalReader($fixture), $fixture);
$requests = [];
checkRuntimeSets($config, static function (string $path) use ($fixture, &$requests): string { $requests[] = $path; return runtimeLocalReader($fixture)($path); });
if (count($requests) !== 90) throw new LogicException('HTTP-путь обязан прочитать все 90 файлов');
$bytes = file_get_contents($fixture . '/1/01_desk_1-source.jpg');
file_put_contents($fixture . '/1/01_desk_1-source.jpg', '<html>HTTP 200</html>');
$assertFails(fn() => checkRuntimeSets($config, runtimeLocalReader($fixture), $fixture));
file_put_contents($fixture . '/1/01_desk_1-source.jpg', $bytes);
$wrong = new Imagick(); $wrong->newImage(1, 1, 'red', 'jpeg'); $wrong->writeImage($fixture . '/1/01_desk_1-source.jpg'); $wrong->clear();
$assertFails(fn() => checkRuntimeSets($config, runtimeLocalReader($fixture), $fixture));
file_put_contents($fixture . '/1/01_desk_1-source.jpg', $bytes);
file_put_contents($fixture . '/1/extra.txt', 'extra');
$assertFails(fn() => checkRuntimeSets($config, runtimeLocalReader($fixture), $fixture));
unlink($fixture . '/1/extra.txt');
unlink($fixture . '/1/05_mob_3-thumb.avif');
$assertFails(fn() => checkRuntimeSets($config, runtimeLocalReader($fixture), $fixture));
copy($root . '/1/05_mob_3-thumb.avif', $fixture . '/1/05_mob_3-thumb.avif');
foreach (['0', '10', '../1', false, null] as $default) {
$bad = $config; $bad['utm']['indpic']['default'] = $default;
$assertFails(fn() => checkRuntimeSets($bad, runtimeLocalReader($fixture), $fixture));
}
$bad = $config; $bad['utm']['indpic']['weights']['2'] = 1;
$assertFails(fn() => checkRuntimeSets($bad, runtimeLocalReader($fixture), $fixture));
$bad = $config; $bad['utm']['indpic']['default'] = '2';
$assertFails(fn() => checkRuntimeSets($bad, runtimeLocalReader($fixture), $fixture));
checkRuntimeSets([], runtimeLocalReader($fixture), $fixture); // штатный fallback /1/
foreach (releaseFiles($root) as $file) if (preg_match('~^(admin/|config\.json$|[0-9]+/)~', $file)) throw new LogicException('Приватный/runtime файл в поставке');
echo "OK: runtime replacement, decode, dimensions, missing/extra files, default, active links, release isolation\n";
} finally {
foreach (glob($fixture . '/1/*') as $file) unlink($file);
rmdir($fixture . '/1'); rmdir($fixture);
}