Files
Investidor-V2/tools/portal-internal-designation-rehearsal.py
T
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

122 lines
8.0 KiB
Python

"""Rehearse the exact fixed ZIP on a private QA source tree, then restore its base."""
from pathlib import Path
from datetime import datetime, timezone
import hashlib, json, shutil, subprocess, zipfile
R = Path(__file__).resolve().parent.parent
P = R / 'storage/app/private/investmoz-v2/portal-audit'
O = R / 'outputs/investmoz-internal-designation-20261011'
load = lambda p: json.loads(p.read_text(encoding='utf-8-sig'))
sha = lambda p: hashlib.sha256(p.read_bytes()).hexdigest()
m = load(O / 'manifesto.json')
qa = load(P / 'internal-designation-plan.json')
source = Path(qa['source'])
assert qa['target']['port'] == 55441 and source.parent == P
stamp = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
stage = P / ('internal-designation-rehearsal-' + stamp)
assert not stage.exists() and stage.parent == P
stage.mkdir()
for folder in ['app', 'config', 'routes', 'resources', 'bootstrap']:
shutil.copytree(R / folder, stage / folder)
shutil.copytree(R / 'public/build', stage / 'public/build')
for file in ['.env', 'qa-autoload.php', 'artisan', 'public/index.php']:
(stage / file).parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source / file, stage / file)
for folder in ['storage/app/private', 'storage/logs', 'storage/framework/cache/data', 'storage/framework/sessions', 'storage/framework/views', 'bootstrap/cache']:
(stage / folder).mkdir(parents=True, exist_ok=True)
assert not list((stage / 'bootstrap/cache').glob('config.php'))
shutil.copytree(R / 'database', stage / 'database')
for file in ['composer.json', 'composer.lock']:
shutil.copyfile(R / file, stage / file)
stage_vendor = str(stage / 'vendor').replace("'", "''")
root_vendor = str(R / 'vendor').replace("'", "''")
subprocess.run(['powershell.exe', '-NoProfile', '-Command', f"New-Item -ItemType Junction -Path '{stage_vendor}' -Target '{root_vendor}' | Out-Null"], capture_output=True, check=True)
env = stage / '.env'
text = env.read_text(encoding='utf-8')
assert text.count('PORTAL_PROFILE_ADMINISTRATION_ENABLED=true') == 1
env.write_text(text.replace('PORTAL_PROFILE_ADMINISTRATION_ENABLED=true','PORTAL_PROFILE_ADMINISTRATION_ENABLED=false'), encoding='utf-8')
old_env_hash = sha(env)
before = {x['path']: sha(stage / x['path']) if (stage / x['path']).is_file() else None for x in m['files']}
assert before == {x['path']: x['before_sha256'] for x in m['files']}
original_bytes = {p: (stage / p).read_bytes() for p, v in before.items() if v is not None}
old_manifest = (stage / 'public/build/manifest.json').read_bytes()
php = 'C:/laragon/bin/php/php-8.5.11-Win32-vs17-x64/php.exe'
probe = stage / 'release-probe.php'
probe.write_text('''<?php
ini_set('zend.exception_ignore_args','1');
require __DIR__.'/qa-autoload.php';
$app=require __DIR__.'/bootstrap/app.php';
$app->make(Illuminate\\Contracts\\Console\\Kernel::class)->bootstrap();
$c=config('database.connections.'.config('database.default'));
throw_unless((int)$c['port']===55441 && str_starts_with($c['database'],'onboarding_') && str_ends_with($c['database'],'_test'),RuntimeException::class,'QA required.');
DB::connection()->getPdo()->exec('BEGIN READ ONLY');
$a=app(App\\Services\\PortalProfileAccess::class);
$new=method_exists($a,'governanceAvailable');
$flags=['administration'=>(bool)config('portal.profile_administration_enabled'),'operations'=>(bool)config('portal.profile_access_enabled'),'country'=>(bool)config('portal.organization_country_enabled')];
$proof=['stage'=>$argv[1],'flags'=>$flags,'new_code'=>$new,'installed'=>$new?$a->installed():null,'operations_available'=>$a->available(),
'governance_available'=>$new?$a->governanceAvailable():null,'config_cached'=>$app->configurationIsCached(),
'middleware_loadable'=>$new?class_exists(App\\Http\\Middleware\\EnsurePortalProfileAdministration::class):false,
'source_file'=>(new ReflectionClass($a))->getFileName(), 'port'=>(int)$c['port'], 'database'=>$c['database']];
$proof['operations_middleware']=array_values(array_filter(app('router')->getRoutes()->getByAction(App\\Http\\Controllers\\PortalProfileController::class.'@opportunities')->gatherMiddleware(),fn($v)=>str_contains($v,'EnsurePortal')));
if($new)$proof['administration_middleware']=array_values(array_filter(app('router')->getRoutes()->getByAction(App\\Http\\Controllers\\PortalProfileController::class.'@administration')->gatherMiddleware(),fn($v)=>str_contains($v,'EnsurePortal')));
DB::connection()->getPdo()->exec('ROLLBACK');
echo json_encode($proof,JSON_THROW_ON_ERROR).PHP_EOL;
''', encoding='utf-8')
def inspect(label):
r = subprocess.run([php,str(probe),label],cwd=stage,capture_output=True,text=True,encoding='utf-8',timeout=30)
if r.returncode:
(P / ('internal-designation-rehearsal-' + label + '-error.log')).write_text(r.stderr,encoding='utf-8')
raise RuntimeError('QA rehearsal failed; private diagnostic retained')
v=json.loads(r.stdout)
assert str(stage) in v['source_file'] and v['port']==55441 and not v['operations_available'] and v['flags']['country']
return v
proofs=[inspect('before')]
archive=O/m['package']
assert sha(archive)==m['package_sha256']
expected={x['path']:x['after_sha256'] for x in m['files']}|{x['path']:x['sha256'] for x in m['assets']}
def apply():
with zipfile.ZipFile(archive) as z:
assert set(z.namelist())==set(expected)
# Hashed resources first, switch the manifest last; retain previous hashed assets.
for relative in sorted(expected,key=lambda p:(p=='public/build/manifest.json',p)):
assert '..' not in Path(relative).parts and not Path(relative).is_absolute()
dest=stage/relative
assert dest.resolve().is_relative_to(stage.resolve())
data=z.read(relative)
assert hashlib.sha256(data).hexdigest()==expected[relative]
dest.parent.mkdir(parents=True,exist_ok=True)
dest.write_bytes(data)
assert all(sha(stage/p)==h for p,h in expected.items()) and sha(env)==old_env_hash
apply()
proofs.append(inspect('code_with_flag_off'))
apply()
proofs.append(inspect('same_zip_reapplied_without_data_actions'))
text=env.read_text(encoding='utf-8')
env.write_text(text.replace('PORTAL_PROFILE_ADMINISTRATION_ENABLED=false','PORTAL_PROFILE_ADMINISTRATION_ENABLED=true'),encoding='utf-8')
proofs.append(inspect('administration_only_on'))
assert proofs[-1]['governance_available'] and not proofs[-1]['flags']['operations'] and proofs[-1]['middleware_loadable']
assert any('EnsurePortalProfileAdministration' in v for v in proofs[-1]['administration_middleware'])
assert any(v.endswith('EnsurePortalProfiles') for v in proofs[-1]['operations_middleware'])
# Do not add configuration caching to the destination: its current state is uncached.
text=env.read_text(encoding='utf-8')
env.write_text(text.replace('PORTAL_PROFILE_ADMINISTRATION_ENABLED=true','PORTAL_PROFILE_ADMINISTRATION_ENABLED=false'),encoding='utf-8')
proofs.append(inspect('containment_flag_off'))
assert not proofs[-1]['governance_available']
for relative in before:
dest=stage/relative
if before[relative] is None:
assert dest.resolve().is_relative_to(stage.resolve())
dest.unlink()
else:
dest.write_bytes(original_bytes[relative])
(stage/'public/build/manifest.json').write_bytes(old_manifest)
proofs.append(inspect('base_code_and_manifest_restored'))
assert sha(env)==old_env_hash and before=={p:sha(stage/p) if (stage/p).is_file() else None for p in before}
result={'result':'passed','at':datetime.now(timezone.utc).isoformat(),'delta':m['delta_sha256'],
'package_sha256':sha(archive),'QA_only':True,'active_application_changes':0,'active_database_writes':0,
'rehearsal':proofs,'base_restored':True,'configuration_state_preserved':'uncached',
'hashed_assets_retained_for_existing_tabs':True,'credentials_or_secrets_in_evidence':False,
'HTTP_visual_criteria_reused':'atribuicoes-qa-navegador.json; no A/B or C37 rerun'}
(O/'ensaio-disponibilizacao-reversao.json').write_text(json.dumps(result,indent=2,ensure_ascii=False),encoding='utf-8')
print(json.dumps({k:result[k] for k in ['result','delta','QA_only','active_application_changes','active_database_writes','base_restored']}))