Files
compras/scripts/build-internal-requisition-candidate.php
2026-11-02 08:08:05 +02:00

99 lines
5.6 KiB
PHP

<?php
declare(strict_types=1);
use Tempo\Distribution\Release;
// Produces review artifacts, never installs, migrates, updates a release channel or changes pointers.
$root = dirname(__DIR__);
$runtime = [];
$verification = [];
$add = function (string $relative, array &$list) use ($root): void {
$path = $root.'/'.$relative;
if (! is_file($path) || is_link($path)) {
throw new RuntimeException('Ficheiro de candidata inexistente ou link: '.$relative);
}
$list[$relative] = hash_file('sha256', $path);
};
foreach (['packages/purchasing', 'integrations/sgf/internal-requisitions'] as $directory) {
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root.'/'.$directory, FilesystemIterator::SKIP_DOTS)) as $file) {
if ($file->isLink()) {
throw new RuntimeException('Links recusados.');
}
if ($file->isFile()) {
$add(str_replace('\\', '/', substr($file->getPathname(), strlen($root) + 1)), $runtime);
}
}
}
foreach (['app/Services/InternalRequisitionPort.php', 'app/Services/RequisitionWorkflow.php', 'app/Models/Requisition.php', 'app/Providers/AppServiceProvider.php', 'app/Http/Requests/StoreRequisitionRequest.php', 'app/Http/Controllers/RequisitionController.php', 'app/Http/Middleware/HandleInertiaRequests.php', 'resources/js/pages/requisitions/form.tsx', 'resources/js/pages/requisitions/show.tsx', 'resources/js/types/procurement.ts', 'database/migrations/2026_11_01_000046_add_requisition_command_versions.php'] as $file) {
$add($file, $runtime);
}
foreach (['scripts/internal-requisitions-proof.php', 'scripts/internal-requisitions-sgf-proof.php', 'scripts/internal-requisition-screen-check.mjs', 'tests/Support/internal-requisition-sgf-router.php', 'tests/Support/internal-requisition-sgf-worker.php', 'tests/Support/internal-requisition-sgf-restored.php', 'tests/Feature/InternalRequisitionCandidateTest.php'] as $file) {
$add($file, $verification);
}
ksort($runtime);
ksort($verification);
$hash = hash('sha256', json_encode([$runtime, $verification], JSON_THROW_ON_ERROR));
$id = 'IR-CANDIDATE-'.substr($hash, 0, 16);
$destination = $root.'/storage/app/private/candidates/'.$id;
if (! is_dir($destination)) {
mkdir($destination, 0700, true);
}
$manifest = ['format' => 'tempo.integration-candidate.v1', 'candidate_id' => $id, 'source_sha256' => $hash, 'installable_by_code_updater' => false, 'activation_authorized' => false,
'destination_version_unchanged' => '0.1.1', 'context' => 'procurement.internal', 'scope' => 'draft -> submit -> independent approval -> consultation',
'validated_scope_excludes' => ['people.third_party', 'equipment/spare-part technical workflows', 'supplier sourcing and purchase orders'],
'host_schema_changes' => ['compras' => 'baseline 45 + additive migration 46', 'sgf' => 'ir-v1 + permission column widening/private catalogue check extension; no grants'],
'rollback' => ['code' => 'separate pointer mechanism; candidate not selected', 'schema_data' => 'transactional DDL, compatible additive column and backup/restore tested separately; no automatic down/restore'],
'runtime_files' => $runtime, 'verification_files' => $verification];
$manifestText = json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)."\n";
foreach ($runtime + $verification as $file => $expected) {
$target = $destination.'/'.$file;
if (is_file($target)) {
if (hash_file('sha256', $target) !== $expected) {
throw new RuntimeException('Candidata imutável divergente.');
}
continue;
}
if (! is_dir(dirname($target))) {
mkdir(dirname($target), 0700, true);
}
if (! copy($root.'/'.$file, $target) || hash_file('sha256', $target) !== $expected) {
throw new RuntimeException('Origem mudou durante a preparação.');
}
}
if (is_file($destination.'/manifest.json') && file_get_contents($destination.'/manifest.json') !== $manifestText) {
throw new RuntimeException('Manifesto imutável divergente.');
}
file_put_contents($destination.'/manifest.json', $manifestText);
$archives = [];
foreach (['runtime' => $runtime, 'verification' => $verification] as $kind => $files) {
$archive = $destination.'/'.$kind.'.zip';
if (! is_file($archive)) {
$zip = new ZipArchive;
if ($zip->open($archive, ZipArchive::CREATE | ZipArchive::EXCL) !== true) {
throw new RuntimeException('Falha no arquivo da candidata.');
}
foreach ($files as $file => $expected) {
$zip->addFile($destination.'/'.$file, $file);
}
$zip->addFromString('manifest.json', $manifestText);
if (! $zip->close()) {
throw new RuntimeException('Arquivo incompleto.');
}
}
$archives[$kind] = ['file' => $archive, 'sha256' => hash_file('sha256', $archive)];
}
require_once $root.'/packages/module-updates/Release.php';
$rejected = false;
try {
Release::inspect($archives['runtime']['file'], hash('sha256', $manifestText));
} catch (RuntimeException $e) {
$rejected = true;
}
if (! $rejected) {
throw new RuntimeException('A candidata não deve ser instalável pelo updater de código.');
}
$result = ['candidate_id' => $id, 'directory' => $destination, 'manifest_sha256' => hash('sha256', $manifestText), 'archives' => $archives, 'standard_updater_refuses_candidate' => true, 'activation_performed' => false];
file_put_contents($root.'/docs/proofs/internal-requisitions-candidate.json', json_encode($result,JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR));
echo json_encode($result,JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)."\n";