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

85 lines
5.4 KiB
Python

"""Prepare isolated stabilization sources; never replace active sources/configuration."""
from pathlib import Path
from datetime import datetime, timezone
import hashlib, json, shutil, subprocess
ROOT = Path(__file__).resolve().parent.parent
PRIVATE = ROOT / 'storage/app/private/investmoz-v2/portal-audit'
read = lambda p: json.loads(Path(p).read_text(encoding='utf-8-sig'))
sha = lambda p: hashlib.sha256(Path(p).read_bytes()).hexdigest()
stamp = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
SOURCE = PRIVATE / ('portal-stability-source-' + stamp)
BASE = Path(read(PRIVATE / 'country-https-latest.json')['source'])
baseline = {}
for directory in ['app', 'config', 'routes', 'resources', 'database', 'bootstrap', 'tests']:
for p in (BASE / directory).rglob('*'):
if not p.is_file() or 'cache' in p.relative_to(BASE / directory).parts:
continue
rel = p.relative_to(BASE)
target = SOURCE / rel
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(p, target)
baseline[rel.as_posix()] = sha(p)
for name in ['artisan', 'composer.json', 'composer.lock', 'package.json', 'package-lock.json',
'tsconfig.json', 'vite.config.ts', 'phpunit.xml', 'phpstan.neon', 'qa-autoload.php', '.env.testing']:
if (BASE / name).is_file():
shutil.copy2(BASE / name, SOURCE / name)
baseline[name] = sha(BASE / name)
for directory in ['bootstrap/cache', 'public', 'storage/framework/views', 'storage/framework/cache',
'storage/framework/sessions', 'storage/logs', 'storage/app/private/investmoz-v2/portal-audit']:
(SOURCE / directory).mkdir(parents=True, exist_ok=True)
for name in ['packages.php', 'services.php']:
if (BASE / 'bootstrap/cache' / name).is_file():
shutil.copy2(BASE / 'bootstrap/cache' / name, SOURCE / 'bootstrap/cache' / name)
for name in ['vendor', 'node_modules']:
subprocess.run(['powershell', '-NoProfile', '-Command',
f"New-Item -ItemType Junction -Path '{SOURCE / name}' -Target '{ROOT / name}' | Out-Null"], check=True)
# One cryptographically secure UUID mechanism for the 25 remaining calls. It does not
# change any business payload, idempotency scope, source identifier or existing UUID.
helper = SOURCE / 'resources/js/lib/operation-key.ts'
helper.write_text('''/** UUID v4 for operation identifiers; retries keep the caller's existing key. */
export function createOperationKey(): string {
const secureRandom = globalThis.crypto;
if (typeof secureRandom?.randomUUID === 'function') return secureRandom.randomUUID();
if (typeof secureRandom?.getRandomValues !== 'function') {
throw new Error('O navegador não disponibiliza geração criptográfica segura. Reabra o portal no endereço HTTPS configurado.');
}
const bytes = secureRandom.getRandomValues(new Uint8Array(16));
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
''', encoding='utf-8')
calls = []
for file in (SOURCE / 'resources/js').rglob('*.tsx'):
text = file.read_text(encoding='utf-8-sig')
count = text.count('crypto.randomUUID()')
if not count:
continue
text = text.replace('globalThis.crypto.randomUUID()', 'createOperationKey()').replace('crypto.randomUUID()', 'createOperationKey()')
text = "import { createOperationKey } from '@/lib/operation-key';\n" + text
file.write_text(text, encoding='utf-8')
calls.append({'file': file.relative_to(SOURCE).as_posix(), 'occurrences': count})
assert sum(item['occurrences'] for item in calls) == 25
assert len(calls) == 14
# Installing and bootstrapping are different operations. Repeating either attempt
# retains its UUID through validation/network errors instead of allocating per click.
profile = SOURCE / 'resources/js/pages/profiles/installation.tsx'
text = profile.read_text(encoding='utf-8')
anchor = " const [message, setMessage] = useState('');"
assert text.count(anchor) == 1
text = text.replace(anchor, anchor + "\n const [installationKey] = useState(createOperationKey);\n const [bootstrapKey] = useState(createOperationKey);")
assert text.count('idempotency_key: createOperationKey(),') == 1
text = text.replace('idempotency_key: createOperationKey(),', 'idempotency_key: bootstrap ? bootstrapKey : installationKey,')
profile.write_text(text, encoding='utf-8')
changed = {item['file']: {'before': baseline[item['file']], 'after': sha(SOURCE / item['file'])} for item in calls}
changed['resources/js/lib/operation-key.ts'] = {'before': None, 'after': sha(helper)}
plan = {'at': datetime.now(timezone.utc).isoformat(), 'source': str(SOURCE), 'baseline_candidate': str(BASE),
'baseline_sources': baseline, 'changed_files': changed, 'uuid_consumers': calls,
'approved_delivery_preserved': '13644ed74a48', 'approved_code_preserved': '162daff6ce08',
'activation_suspended': True, 'active_sources_modified': False, 'active_database_writes': 0}
(PRIVATE / 'portal-stability-latest.json').write_text(json.dumps(plan, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps({'result': 'QA sources prepared', 'source': str(SOURCE), 'uuid_calls': 25, 'components': 14,
'changed_files': len(changed), 'active_sources_modified': False, 'active_database_writes': 0}))