commit14
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
use Tempo\Purchasing\Delivery\CodeDeployment;
|
||||
|
||||
require dirname(__DIR__).'/integrations/sgf/operational/CodeDeployment.php';
|
||||
require dirname(__DIR__).'/tests/Support/sgf-update-fixture.php';
|
||||
$root = dirname(__DIR__);
|
||||
$host = realpath($root.'/../sgf-tempo');
|
||||
$pointer = file_get_contents($host.'/packages/purchasing/current.json');
|
||||
$old = json_decode(file_get_contents($root.'/docs/proofs/sgf-code-deploy-delivery.json'), true, 32, JSON_THROW_ON_ERROR)['candidate'];
|
||||
$rows = [];
|
||||
exec(escapeshellarg(PHP_BINARY).' '.escapeshellarg($root.'/scripts/prepare-sgf-operational.php').' '.escapeshellarg($host), $rows, $exit);
|
||||
if ($exit !== 0) {
|
||||
throw new RuntimeException('Preparation failed');
|
||||
}
|
||||
$prepared = json_decode(implode("\n", $rows), true, 16, JSON_THROW_ON_ERROR);
|
||||
$new = $prepared['directory'];
|
||||
$oldHash = hash_file('sha256', $old.'/manifest.json');
|
||||
$newHash = hash_file('sha256', $new.'/manifest.json');
|
||||
$manifest = json_decode(file_get_contents($old.'/manifest.json'), true, 32, JSON_THROW_ON_ERROR);
|
||||
$copyBundle = function (string $source, string $target) {
|
||||
$manifest = json_decode(file_get_contents($source.'/manifest.json'), true, 32, JSON_THROW_ON_ERROR);
|
||||
foreach ([...array_keys($manifest['support_files']), 'manifest.json'] as $file) {
|
||||
if (! is_dir(dirname($target.'/'.$file))) {
|
||||
mkdir(dirname($target.'/'.$file), 0700, true);
|
||||
}copy($source.'/'.$file, $target.'/'.$file);
|
||||
}
|
||||
};
|
||||
$checks = [];
|
||||
$check = function ($ok, $name) use (&$checks) {
|
||||
if (! $ok) {
|
||||
throw new RuntimeException($name);
|
||||
}$checks[] = $name;
|
||||
};
|
||||
$check(hash_file('sha256', $prepared['archive']['file']) === $prepared['archive']['sha256'] && $prepared['archive']['manifest_sha256'] === $newHash, 'portable review ZIP matches approved bundle and manifest');
|
||||
$deny = function (callable $call, string $text) use ($check) {
|
||||
try {
|
||||
$call();
|
||||
throw new LogicException('Unexpected success');
|
||||
} catch (RuntimeException $error) {
|
||||
$check(str_contains($error->getMessage(), $text), $text);
|
||||
}
|
||||
};
|
||||
$apply = function (CodeDeployment $deployment, string $bundle, string $hash, ?callable $checkpoint = null) {
|
||||
$plan = $deployment->plan($bundle, $hash);
|
||||
|
||||
return $deployment->apply($bundle, $hash, $plan['target_sha256'], 'Synthetic version operator', true, $checkpoint);
|
||||
};
|
||||
$copy = updateFixture($host, $manifest);
|
||||
$d = new CodeDeployment($copy);
|
||||
$v1 = $apply($d, $old, $oldHash);
|
||||
$unowned = updateFixture($host, $manifest);
|
||||
$copyBundle($old, $unowned.'/packages/internal-requisitions');
|
||||
$deny(fn () => (new CodeDeployment($unowned))->plan($new, $newHash), 'sem proveniência');
|
||||
$mutant = sys_get_temp_dir().'/sgf-update-schema-'.bin2hex(random_bytes(6));
|
||||
$copyBundle($new, $mutant);
|
||||
$changed = json_decode(file_get_contents($mutant.'/manifest.json'), true, 32, JSON_THROW_ON_ERROR);
|
||||
$driver = 'integrations/sgf/operational/Installation.php';
|
||||
file_put_contents($mutant.'/'.$driver, file_get_contents($mutant.'/'.$driver)."\n// Synthetic schema-driver revision\n");
|
||||
$changed['support_files'][$driver] = hash_file('sha256', $mutant.'/'.$driver);
|
||||
file_put_contents($mutant.'/manifest.json', json_encode($changed, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
|
||||
$deny(fn () => $d->plan($mutant, hash_file('sha256', $mutant.'/manifest.json')), 'Transição de schema');
|
||||
$plan = $d->plan($new, $newHash);
|
||||
$check($plan['previous']['deployment_id'] === $v1['deployment_id'], 'plan binds update to active installation');
|
||||
$v2 = $apply($d, $new, $newHash);
|
||||
$check(hash_file('sha256', $copy.'/packages/internal-requisitions/manifest.json') === $newHash, 'new version applied without source connection');
|
||||
$check(is_dir($copy.'/storage/app/private/module-deployments/'.$v2['deployment_id'].'/previous-bundle'), 'previous version preserved locally');
|
||||
$deny(fn () => $d->rollback($v1['deployment_id'], 'Synthetic out-of-order', true), 'fora de ordem');
|
||||
$deny(fn () => $d->apply($old, $oldHash, $plan['target_sha256'], 'Synthetic stale replay', true), 'versão activa');
|
||||
$file = array_key_first($manifest['host_overlays']);
|
||||
$content = file_get_contents($copy.'/'.$file);
|
||||
file_put_contents($copy.'/'.$file, $content."\n// local work\n");
|
||||
$deny(fn () => $d->plan($new, $newHash), 'Código aplicado alterado');
|
||||
file_put_contents($copy.'/'.$file, $content);
|
||||
file_put_contents($copy.'/packages/internal-requisitions/local-extension.php', '<?php // synthetic unrecorded extension');
|
||||
$deny(fn () => $d->plan($new, $newHash), 'Conteúdo extra');
|
||||
unlink($copy.'/packages/internal-requisitions/local-extension.php');
|
||||
$d->rollback($v2['deployment_id'], 'Synthetic version rollback', true);
|
||||
$check(hash_file('sha256', $copy.'/packages/internal-requisitions/manifest.json') === $oldHash, 'rollback restores previous bundle exactly');
|
||||
$check(json_decode(file_get_contents($copy.'/storage/app/private/module-deployments/current.json'), true)['deployment_id'] === $v1['deployment_id'], 'rollback restores active provenance');
|
||||
$retryPlan = $d->plan($new, $newHash);
|
||||
$check($retryPlan['deployment_id'] !== $v2['deployment_id'], 'new attempt preserves rolled-back journal');
|
||||
$deny(fn () => $apply($d, $new, $newHash, function ($step) {
|
||||
if ($step === 'previous-bundle-preserved') {
|
||||
throw new RuntimeException('Injected update switch failure');
|
||||
}
|
||||
}), 'Injected update switch failure');
|
||||
$d->rollback($retryPlan['deployment_id'], 'Synthetic interrupted transition', true);
|
||||
$check(hash_file('sha256', $copy.'/packages/internal-requisitions/manifest.json') === $oldHash, 'interrupted transition recovers previous version');
|
||||
$again = $apply($d, $new, $newHash);
|
||||
$check($again['deployment_id'] !== $retryPlan['deployment_id'], 'reapply creates a separate audited attempt');
|
||||
$d->rollback($again['deployment_id'], 'Synthetic clean transition rollback', true);
|
||||
$d->rollback($v1['deployment_id'], 'Synthetic first installation rollback', true);
|
||||
$check(! is_dir($copy.'/packages/internal-requisitions'), 'complete rollback restores original host composition');
|
||||
|
||||
$native = updateFixture($host, $manifest, true);
|
||||
$nativeDeploy = new CodeDeployment($native);
|
||||
$environment = ['APP_ENV' => 'production', 'APP_DEBUG' => 'false', 'APP_BASE_PATH' => $native, 'APP_KEY' => 'base64:'.base64_encode(random_bytes(32)), 'DB_CONNECTION' => 'sqlite', 'DB_DATABASE' => ':memory:', 'DB_URL' => '', 'CACHE_STORE' => 'array', 'SESSION_DRIVER' => 'file', 'QUEUE_CONNECTION' => 'sync'];
|
||||
$previousEnv = [];
|
||||
foreach ($environment as $key => $value) {
|
||||
$previousEnv[$key] = getenv($key);
|
||||
putenv($key.'='.$value);
|
||||
}
|
||||
$server = null;
|
||||
$start = function () use ($native) {
|
||||
$socket = stream_socket_server('tcp://127.0.0.1:0');
|
||||
$name = stream_socket_get_name($socket, false);
|
||||
fclose($socket);
|
||||
$port = (int) substr(strrchr($name, ':'), 1);
|
||||
$process = proc_open([PHP_BINARY, '-S', '127.0.0.1:'.$port, '-t', $native.'/public'], [0 => ['pipe', 'r'], 1 => ['file', $native.'/storage/logs/server.log', 'a'], 2 => ['file', $native.'/storage/logs/server.log', 'a']], $pipes, $native);
|
||||
fclose($pipes[0]);
|
||||
for ($i = 0; $i < 100; $i++) {
|
||||
$probe = @fsockopen('127.0.0.1', $port);
|
||||
if ($probe) {
|
||||
fclose($probe);
|
||||
break;
|
||||
}usleep(50000);
|
||||
}
|
||||
|
||||
return [$process, $port];
|
||||
};
|
||||
$stop = function ($process) {
|
||||
proc_terminate($process);
|
||||
proc_close($process);
|
||||
};
|
||||
$status = function ($port, $path) {
|
||||
$context = stream_context_create(['http' => ['ignore_errors' => true, 'header' => "Accept: application/json\r\n", 'timeout' => 10]]);
|
||||
file_get_contents('http://127.0.0.1:'.$port.$path, false, $context);
|
||||
preg_match('/\s([0-9]{3})\s/', $http_response_header[0], $match);
|
||||
|
||||
return (int) $match[1];
|
||||
};
|
||||
try {
|
||||
$a = $apply($nativeDeploy, $old, $oldHash);
|
||||
$nativeDeploy->rebuildCaches($a['deployment_id'], 'Synthetic native cache V1', true);
|
||||
$check(is_file($native.'/bootstrap/cache/config.php') && is_file($native.'/bootstrap/cache/routes-v7.php') && is_file($native.'/bootstrap/cache/events.php'), 'native Laravel config route and event caches built');
|
||||
$b = $apply($nativeDeploy, $new, $newHash);
|
||||
$check(! is_file($native.'/bootstrap/cache/config.php') && ! is_file($native.'/bootstrap/cache/routes-v7.php'), 'version update invalidates previous native caches');
|
||||
$deny(fn () => $nativeDeploy->rebuildCaches($b['deployment_id'], 'Synthetic cache compilation failure', true, function ($command) {
|
||||
if ($command === 'config:cache') {
|
||||
throw new RuntimeException('Injected cache compilation interruption');
|
||||
}
|
||||
}), 'Injected cache compilation interruption');
|
||||
$cacheJournal = json_decode(file_get_contents($native.'/storage/app/private/module-deployments/'.$b['deployment_id'].'/journal.json'), true, 32, JSON_THROW_ON_ERROR);
|
||||
$check($cacheJournal['cache_state'] === 'failed' && is_file($native.'/storage/framework/down'), 'partial native cache build is journalled under maintenance');
|
||||
file_put_contents($native.'/bootstrap/cache/config.php', '<?php invalid_partial_cache(');
|
||||
$cliRows = [];
|
||||
$cliArgs = [PHP_BINARY, $new.'/scripts/sgf-code-deploy.php', 'cache', $native, $b['deployment_id'], 'Synthetic native cache V2 CLI', '--offline'];
|
||||
exec(implode(' ', array_map('escapeshellarg', $cliArgs)), $cliRows, $cliExit);
|
||||
$check($cliExit === 0 && json_decode(implode("\n", $cliRows), true, 16, JSON_THROW_ON_ERROR)['cache_state'] === 'built', 'cache CLI recovers interrupted native compilation');
|
||||
$check(! str_contains(file_get_contents($native.'/bootstrap/cache/config.php'), 'invalid_partial_cache'), 'invalid partial config cache cannot block native recovery');
|
||||
[$server,$port] = $start();
|
||||
$check($status($port, '/purchasing/internal-requisitions') === 503, 'cached deployment remains in native maintenance');
|
||||
$stop($server);
|
||||
$server = null;
|
||||
unlink($native.'/storage/framework/down');
|
||||
[$server,$port] = $start();
|
||||
$check($status($port, '/purchasing/internal-requisitions') === 401, 'cached module route survives restart and denies anonymous access');
|
||||
$stop($server);
|
||||
$server = null;
|
||||
file_put_contents($native.'/storage/framework/down', '{"status":503}');
|
||||
$compiled = file_get_contents($native.'/bootstrap/cache/config.php');
|
||||
file_put_contents($native.'/bootstrap/cache/config.php', $compiled."\n// subsequent local cache change\n");
|
||||
$deny(fn () => $nativeDeploy->rollback($b['deployment_id'], 'Synthetic conflicting cache rollback', true), 'Cache mudou após compilação');
|
||||
file_put_contents($native.'/bootstrap/cache/config.php', $compiled);
|
||||
$nativeDeploy->rollback($b['deployment_id'], 'Synthetic cached version rollback', true);
|
||||
$check(! is_file($native.'/bootstrap/cache/config.php') && ! is_file($native.'/bootstrap/cache/routes-v7.php'), 'rollback invalidates registered rebuilt caches');
|
||||
$nativeDeploy->rebuildCaches($a['deployment_id'], 'Synthetic native cache restored V1', true);
|
||||
$previousCacheHash = hash_file('sha256', $native.'/bootstrap/cache/config.php');
|
||||
$replayedRollback = $nativeDeploy->rollback($b['deployment_id'], 'Synthetic repeated version rollback', true);
|
||||
$check($replayedRollback['replayed'] && hash_file('sha256', $native.'/bootstrap/cache/config.php') === $previousCacheHash, 'repeated rollback preserves previous-version rebuilt caches');
|
||||
[$server,$port] = $start();
|
||||
$check($status($port, '/purchasing/internal-requisitions') === 503, 'previous version restarts with native caches and maintenance');
|
||||
$stop($server);
|
||||
$server = null;
|
||||
$nativeDeploy->rollback($a['deployment_id'], 'Synthetic native baseline rollback', true);
|
||||
unlink($native.'/storage/framework/down');
|
||||
[$server,$port] = $start();
|
||||
$check($status($port, '/purchasing/internal-requisitions') === 404, 'restarted baseline exposes no removed module route');
|
||||
$check($status($port, '/up') === 200, 'baseline starts after cached deployment rollback');
|
||||
} finally {
|
||||
if (is_resource($server)) {
|
||||
$stop($server);
|
||||
}foreach ($previousEnv as $key => $value) {
|
||||
putenv($value === false ? $key : $key.'='.$value);
|
||||
}
|
||||
}
|
||||
$check(file_get_contents($host.'/packages/purchasing/current.json') === $pointer, 'destination remains untouched on 0.1.1');
|
||||
$proof = ['passed' => true, 'checks' => $checks, 'old_candidate' => $old, 'new_candidate' => $new, 'archive' => $prepared['archive'], 'copies' => [$copy, $native, $unowned, $mutant], 'checked_at' => gmdate('c'), 'destination_version' => '0.1.1', 'destination_applied' => false, 'prior_suites_repeated' => false, 'actual_destination_services_touched' => false, 'native_caches_built' => true, 'isolated_server_restarts_verified' => true];
|
||||
file_put_contents($root.'/docs/proofs/sgf-update-cache-delivery.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
|
||||
echo json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)."\n";
|
||||
Reference in New Issue
Block a user