From b410542f0a9127bdf473dda3d14943db7c866da7 Mon Sep 17 00:00:00 2001 From: homelab-runtime-publisher Date: Mon, 7 Sep 2026 20:36:06 +0000 Subject: [PATCH] runtime: publish NEWFI-260907-A-PORTAL200-APPLY4DESIGN7 --- ...EWFI-260907-A-PORTAL200-APPLY4DESIGN7.json | 38 + ...NEWFI-260907-A-PORTAL200-APPLY4DESIGN7.txt | 50 + runtime/latest.json | 16 +- runtime/latest.txt | 1641 +---------------- 4 files changed, 119 insertions(+), 1626 deletions(-) create mode 100644 runtime/history/NEWFI-260907-A-PORTAL200-APPLY4DESIGN7.json create mode 100644 runtime/history/NEWFI-260907-A-PORTAL200-APPLY4DESIGN7.txt diff --git a/runtime/history/NEWFI-260907-A-PORTAL200-APPLY4DESIGN7.json b/runtime/history/NEWFI-260907-A-PORTAL200-APPLY4DESIGN7.json new file mode 100644 index 00000000..1b678f48 --- /dev/null +++ b/runtime/history/NEWFI-260907-A-PORTAL200-APPLY4DESIGN7.json @@ -0,0 +1,38 @@ +{ + "schema_version": 1, + "channel": "homelab-runtime", + "command_id": "NEWFI-260907-A-PORTAL200-APPLY4DESIGN7", + "status": "OK", + "rc": 0, + "host": "pve01", + "mode": "read-only", + "component": "newfi-portal200-apply4-static-ast-recovery", + "started_at_utc": "2026-09-07T20:36:05Z", + "finished_at_utc": "2026-09-07T20:36:05Z", + "reference_register_checked": true, + "reference_sha256": "5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66", + "error_register_checked": true, + "error_register_sha256": "3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0", + "command_sha256": "f496660f98e2d5bd57395b705f69565e9b9b4fa3823630155bbe079f561a2876", + "duplicate_failed_command_blocked": false, + "block_reason": null, + "execution_started": true, + "change_declared": false, + "result_contract_valid": true, + "result_contract_status": null, + "result_contract_error": null, + "command_rc": 0, + "changes_made": false, + "rollback_started": false, + "rollback_restored": null, + "mutation_outcome": "NO_MUTATION", + "sanitized": true, + "secrets_included": false, + "private_addresses_included": false, + "raw_evidence_retained_locally": true, + "raw_evidence_sha256": "b0e750afcc8c28e33530e823fa9ca879ddd22ce66e758296aeb27ffd61aa5d1a", + "sanitized_output_sha256": "b0e750afcc8c28e33530e823fa9ca879ddd22ce66e758296aeb27ffd61aa5d1a", + "output_truncated_in_json": false, + "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt", + "output": "AST_RECOVERY_BEGIN=true\nOLD_CARRIER_SHA256=573c5c94fe59ed072e094281668ce9847dc4a9882019a2b89d8ba5f190d92176\nOLD_INNER_SHA256=97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184\nOLD_INNER_BYTES=34155\nTOP_LEVEL_LITERAL_ASSIGNMENTS={\"BRANCH\":\"implementation/newfi-closed-mvp-20260906\",\"CID\":\"NEWFI-260907-PORTAL200-APPLY3\",\"ERRORS_PHP_TEMPLATE\":\"db();\\n$b=%d;\\n$rows=$db->fetchAll(\\n \\\"SELECT error_id,exception_type,message,filename,line FROM xf_error_log WHERE error_id>? ORDER BY error_id\\\",\\n [$b]\\n);\\necho json_encode(\\n $rows,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\\n?>\",\"EXPECTED_NEW\":{\"Pub/Controller/Portal.php\":[363,\"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\"],\"Service/PortalData.php\":[5213,\"4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d\"],\"_data/templates.xml\":[10977,\"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\"]},\"EXPECTED_OLD\":{\"Pub/Controller/Portal.php\":[247,\"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\"],\"_data/templates.xml\":[1461,\"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\"]},\"HTTP_VERIFY_PY\":\"import html,json,re,subprocess,sys,urllib.parse\\nmode=sys.argv[1]\\nstate=sys.argv[2]\\nif mode=='staging':\\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\\nelif mode=='prod':\\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\\nelse:\\n raise SystemExit(40)\\ndef fetch(url):\\n cmd=[\\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\\n ]\\n if mode=='staging':\\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\\n cmd += [\\n '-o','-','-w','\\\\n__PORTAL_STATUS__%{http_code}',url\\n ]\\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\\n if p.returncode!=0: raise SystemExit(41)\\n text=p.stdout.decode('utf-8','replace')\\n marker='\\\\n__PORTAL_STATUS__'\\n if marker not in text: raise SystemExit(42)\\n body,status=text.rsplit(marker,1)\\n return int(status.strip()),body\\nroot=f'{scheme}://{host}/'\\nstatus,body=fetch(root)\\nhrefs=[]\\nfor raw in re.findall(r\\\"href=[\\\\\\\"']([^\\\\\\\"']*css\\\\.php[^\\\\\\\"']*)[\\\\\\\"']\\\",body,re.I):\\n raw=html.unescape(raw)\\n hrefs.append(urllib.parse.urljoin(root,raw))\\nnormal_css=[]\\nnormal_status=[]\\nfor u in hrefs:\\n s,b=fetch(u)\\n normal_status.append(s)\\n normal_css.append(b)\\nnormal_css_text='\\\\n'.join(normal_css)\\nforced_css_marker=None\\nforced_css_status=None\\nif mode=='staging' and state=='new' and hrefs:\\n u=urllib.parse.urlsplit(hrefs[0])\\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\\n out=[]\\n found=False\\n for k,v in pairs:\\n if k=='css':\\n found=True\\n parts=[x for x in v.split(',') if x]\\n if 'public:newfi_portal.less' not in parts:\\n parts.append('public:newfi_portal.less')\\n v=','.join(parts)\\n out.append((k,v))\\n if found:\\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\\n forced_css_status,forced_body=fetch(forced)\\n forced_css_marker='--newfi-portal200' in forced_body\\nresult={\\n 'status':status,\\n 'bytes':len(body.encode()),\\n 'html_marker':'data-newfi-portal200=\\\"1\\\"' in body,\\n 'css_marker':'--newfi-portal200' in normal_css_text,\\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\\n 'css_count':len(hrefs),\\n 'css_status':normal_status,\\n 'forced_css_status':forced_css_status,\\n 'forced_css_marker':forced_css_marker,\\n}\\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\\nif status!=expected_status: raise SystemExit(45)\\nif mode=='prod':\\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\\nelif mode=='staging':\\n if any(x!=200 for x in normal_status): raise SystemExit(46)\\nelse:\\n raise SystemExit(50)\\nif mode=='prod' and state=='new':\\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\\nelif mode=='prod' and state=='old':\\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\\nelif mode=='staging' and state=='new':\\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\\nelif mode=='staging' and state=='old':\\n pass\\nelse:\\n raise SystemExit(50)\\n\",\"IMPORT_PHP\":\"addOnManager()->getById('Newfi/Portal');\\nif (!$addOn || !$addOn->isInstalled()) {\\n fwrite(STDERR, \\\"ADDON_NOT_INSTALLED\\\\n\\\");\\n exit(30);\\n}\\n$addOn->preRebuild();\\n$dataManager = $app->addOnDataManager();\\n$imported = [];\\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\\n $handler = $dataManager->getDataTypeHandler($dataType);\\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\\n if (!$xml || !$xml->count()) {\\n $handler->deleteAddOnData($addOn->addon_id);\\n continue;\\n }\\n $start = 0;\\n $guard = 0;\\n while (true) {\\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\\n if (!$last) { break; }\\n if ($last <= $start || ++$guard > 100000) {\\n fwrite(STDERR, \\\"IMPORT_PROGRESS_INVALID\\\\n\\\");\\n exit(31);\\n }\\n $start = $last;\\n }\\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\\n \\\\XF::triggerRunOnce();\\n $imported[] = $handler->getContainerTag();\\n}\\n$addOn->postDataImport();\\n$repo = $app->repository(\\\\XF\\\\Repository\\\\StyleRepository::class);\\n$repo->updateAllStylesLastModifiedDateLater();\\n$addOn->postRebuild();\\necho \\\"PORTAL200_IMPORT_JSON=\\\" . json_encode(\\n ['result'=>'PASS','containers'=>$imported],\\n JSON_UNESCAPED_SLASHES\\n) . \\\"\\\\n\\\";\\n?>\",\"INSPECT_PHP\":\"db();\\nfunction srcMeta(string $path): ?array {\\n if (!is_file($path)) { return null; }\\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\\n}\\n$portal = $root . '/src/addons/Newfi/Portal';\\n$templates = $db->fetchAll(\\n \\\"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \\\" .\\n \\\"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \\\" .\\n \\\"LOCATE('--newfi-portal200', template) > 0 AS css_marker \\\" .\\n \\\"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\\\"\\n);\\n$result = [\\n 'hostname' => gethostname(),\\n 'application' => [\\n 'boardActive' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\\\"),\\n 'boardUrl' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\\\"),\\n 'indexRoute' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\\\"),\\n 'defaultStyleId' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\\\"),\\n 'error_count' => (int)$db->fetchOne(\\\"SELECT COUNT(*) FROM xf_error_log\\\"),\\n 'error_max_id' => (int)$db->fetchOne(\\\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\\\")\\n ],\\n 'addon' => $db->fetchRow(\\n \\\"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \\\" .\\n \\\"FROM xf_addon WHERE addon_id='Newfi/Portal'\\\"\\n ),\\n 'source' => [\\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\\n ],\\n 'templates' => $templates\\n];\\necho \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\";\\necho json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\\necho \\\"\\\\nPORTAL200_INSPECT_JSON_END\\\\n\\\";\\n?>\",\"OLD_HEAD\":\"06bab5acd124e768f70305b3d7eca2f595871890\",\"RUNTIME_VERIFY_PHP_TEMPLATE\":\"$state];\\nif ($state === 'new') {\\n $service = new \\\\Newfi\\\\Portal\\\\Service\\\\PortalData($app);\\n $portal = $service->build();\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\\n $result['sections'] = [];\\n foreach (['forums','resources','media','events','dogs'] as $key) {\\n $section = $portal[$key] ?? null;\\n if (!is_array($section)) { fwrite(STDERR, \\\"SECTION_MISSING\\\\n\\\"); exit(60); }\\n $result['sections'][$key] = [\\n 'available'=>(bool)($section['available'] ?? false),\\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\\n 'status'=>(string)($section['status'] ?? '')\\n ];\\n }\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\\\"1\\\"') !== false;\\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\\\"resources\\\"') !== false;\\n $result['locked_media'] = strpos($html, 'data-portal-locked=\\\"media\\\"') !== false;\\n $result['html_bytes'] = strlen($html);\\n if (!$result['html_marker']) { fwrite(STDERR, \\\"HTML_MARKER_MISSING\\\\n\\\"); exit(61); }\\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\\n fwrite(STDERR, \\\"GUEST_PERMISSION_CONTRACT_CHANGED\\\\n\\\"); exit(62);\\n }\\n if (!$result['locked_resources'] || !$result['locked_media']) {\\n fwrite(STDERR, \\\"LOCKED_COPY_MISSING\\\\n\\\"); exit(63);\\n }\\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\\n fwrite(STDERR, \\\"PUBLIC_SECTION_UNAVAILABLE\\\\n\\\"); exit(64);\\n }\\n} else if ($state === 'old') {\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\\n $result['html_bytes'] = strlen($html);\\n if ($result['html_marker'] || !$result['old_copy']) {\\n fwrite(STDERR, \\\"OLD_RENDER_CONTRACT\\\\n\\\"); exit(65);\\n }\\n} else {\\n exit(66);\\n}\\necho \\\"PORTAL200_RUNTIME_JSON=\\\" . json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n) . \\\"\\\\n\\\";\\n?>\",\"commit_created\":false,\"failure_rollback_started\":false,\"git_files_changed\":false,\"old_git_bytes\":{},\"prod_backup\":false,\"prod_mutated\":false,\"push_confirmed\":false,\"staging_backup\":false,\"staging_final_new\":false,\"staging_mutated\":false}\nCONST_CID=NEWFI-260907-PORTAL200-APPLY3\nCONST_BRANCH=implementation/newfi-closed-mvp-20260906\nCONST_OLD_HEAD=06bab5acd124e768f70305b3d7eca2f595871890\nCONST_WT=UNPROVEN\nCONST_CAND_ROOT=UNPROVEN\nRELEVANT_CONSTANTS=[{\"lineno\":10,\"segment\":\"\\\"NEWFI-260907-PORTAL200-APPLY3\\\"\",\"value\":\"NEWFI-260907-PORTAL200-APPLY3\"},{\"lineno\":147,\"segment\":\"r'''db();\\nfunction srcMeta(string $path): ?array {\\n if (!is_file($path)) { return null; }\\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\\n}\\n$portal = $root . '/src/addons/Newfi/Portal';\\n$templates = $db->fetchAll(\\n \\\"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \\\" .\\n \\\"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \\\" .\\n \\\"LOCATE('--newfi-portal200', template) > 0 AS css_marker \\\" .\\n \\\"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\\\"\\n);\\n$result = [\\n 'hostname' => gethostname(),\\n 'application' => [\\n 'boardActive' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\\\"),\\n 'boardUrl' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\\\"),\\n 'indexRoute' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\\\"),\\n 'defaultStyleId' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\\\"),\\n 'error_count' => (int)$db->fetchOne(\\\"SELECT COUNT(*) FROM xf_error_log\\\"),\\n 'error_max_id' => (int)$db->fetchOne(\\\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\\\")\\n ],\\n 'addon' => $db->fetchRow(\\n \\\"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \\\" .\\n \\\"FROM xf_addon WHERE addon_id='Newfi/Portal'\\\"\\n ),\\n 'source' => [\\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\\n ],\\n 'templates' => $templates\\n];\\necho \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\";\\necho json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\\necho \\\"\\\\nPORTAL200_INSPECT_JSO\",\"value\":\"db();\\nfunction srcMeta(string $path): ?array {\\n if (!is_file($path)) { return null; }\\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\\n}\\n$portal = $root . '/src/addons/Newfi/Portal';\\n$templates = $db->fetchAll(\\n \\\"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \\\" .\\n \\\"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \\\" .\\n \\\"LOCATE('--newfi-portal200', template) > 0 AS css_marker \\\" .\\n \\\"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\\\"\\n);\\n$result = [\\n 'hostname' => gethostname(),\\n 'application' => [\\n 'boardActive' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\\\"),\\n 'boardUrl' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\\\"),\\n 'indexRoute' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\\\"),\\n 'defaultStyleId' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\\\"),\\n 'error_count' => (int)$db->fetchOne(\\\"SELECT COUNT(*) FROM xf_error_log\\\"),\\n 'error_max_id' => (int)$db->fetchOne(\\\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\\\")\\n ],\\n 'addon' => $db->fetchRow(\\n \\\"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \\\" .\\n \\\"FROM xf_addon WHERE addon_id='Newfi/Portal'\\\"\\n ),\\n 'source' => [\\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\\n ],\\n 'templates' => $templates\\n];\\necho \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\";\\necho json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\\necho \\\"\\\\nPORTAL200_INSPECT_JSON_END\\\\n\\\";\\n?>\"},{\"lineno\":194,\"segment\":\"r'''addOnManager()->getById('Newfi/Portal');\\nif (!$addOn || !$addOn->isInstalled()) {\\n fwrite(STDERR, \\\"ADDON_NOT_INSTALLED\\\\n\\\");\\n exit(30);\\n}\\n$addOn->preRebuild();\\n$dataManager = $app->addOnDataManager();\\n$imported = [];\\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\\n $handler = $dataManager->getDataTypeHandler($dataType);\\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\\n if (!$xml || !$xml->count()) {\\n $handler->deleteAddOnData($addOn->addon_id);\\n continue;\\n }\\n $start = 0;\\n $guard = 0;\\n while (true) {\\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\\n if (!$last) { break; }\\n if ($last <= $start || ++$guard > 100000) {\\n fwrite(STDERR, \\\"IMPORT_PROGRESS_INVALID\\\\n\\\");\\n exit(31);\\n }\\n $start = $last;\\n }\\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\\n \\\\XF::triggerRunOnce();\\n $imported[] = $handler->getContainerTag();\\n}\\n$addOn->postDataImport();\\n$repo = $app->repository(\\\\XF\\\\Repository\\\\StyleRepository::class);\\n$repo->updateAllStylesLastModifiedDateLater();\\n$addOn->postRebuild();\\necho \\\"PORTAL200_IMPORT_JSON=\\\" . json_encode(\\n ['result'=>'PASS','containers'=>$imported],\\n JSON_UNESCAPED_SLASHES\\n) . \\\"\\\\n\\\";\\n?>'''\",\"value\":\"addOnManager()->getById('Newfi/Portal');\\nif (!$addOn || !$addOn->isInstalled()) {\\n fwrite(STDERR, \\\"ADDON_NOT_INSTALLED\\\\n\\\");\\n exit(30);\\n}\\n$addOn->preRebuild();\\n$dataManager = $app->addOnDataManager();\\n$imported = [];\\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\\n $handler = $dataManager->getDataTypeHandler($dataType);\\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\\n if (!$xml || !$xml->count()) {\\n $handler->deleteAddOnData($addOn->addon_id);\\n continue;\\n }\\n $start = 0;\\n $guard = 0;\\n while (true) {\\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\\n if (!$last) { break; }\\n if ($last <= $start || ++$guard > 100000) {\\n fwrite(STDERR, \\\"IMPORT_PROGRESS_INVALID\\\\n\\\");\\n exit(31);\\n }\\n $start = $last;\\n }\\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\\n \\\\XF::triggerRunOnce();\\n $imported[] = $handler->getContainerTag();\\n}\\n$addOn->postDataImport();\\n$repo = $app->repository(\\\\XF\\\\Repository\\\\StyleRepository::class);\\n$repo->updateAllStylesLastModifiedDateLater();\\n$addOn->postRebuild();\\necho \\\"PORTAL200_IMPORT_JSON=\\\" . json_encode(\\n ['result'=>'PASS','containers'=>$imported],\\n JSON_UNESCAPED_SLASHES\\n) . \\\"\\\\n\\\";\\n?>\"},{\"lineno\":258,\"segment\":\"r'''$state];\\nif ($state === 'new') {\\n $service = new \\\\Newfi\\\\Portal\\\\Service\\\\PortalData($app);\\n $portal = $service->build();\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\\n $result['sections'] = [];\\n foreach (['forums','resources','media','events','dogs'] as $key) {\\n $section = $portal[$key] ?? null;\\n if (!is_array($section)) { fwrite(STDERR, \\\"SECTION_MISSING\\\\n\\\"); exit(60); }\\n $result['sections'][$key] = [\\n 'available'=>(bool)($section['available'] ?? false),\\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\\n 'status'=>(string)($section['status'] ?? '')\\n ];\\n }\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\\\"1\\\"') !== false;\\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\\\"resources\\\"') !== false;\\n $result['locked_media'] = strpos($html, 'data-portal-locked=\\\"media\\\"') !== false;\\n $result['html_bytes'] = strlen($html);\\n if (!$result['html_marker']) { fwrite(STDERR, \\\"HTML_MARKER_MISSING\\\\n\\\"); exit(61); }\\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\\n fwrite(STDERR, \\\"GUEST_PERMISSION_CONTRACT_CHANGED\\\\n\\\"); exit(62);\\n }\\n if (!$result['locked_resources'] || !$result['locked_media']) {\\n fwrite(STDERR, \\\"LOCKED_COPY_MISSING\\\\n\\\"); exit(63);\\n }\\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\\n fwrite(STDERR, \\\"PUBLIC_SECTION_UNAVAILABLE\\\\n\\\"); exit(64);\\n }\\n} else if ($state === 'old') {\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\\n $result['o\",\"value\":\"$state];\\nif ($state === 'new') {\\n $service = new \\\\Newfi\\\\Portal\\\\Service\\\\PortalData($app);\\n $portal = $service->build();\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\\n $result['sections'] = [];\\n foreach (['forums','resources','media','events','dogs'] as $key) {\\n $section = $portal[$key] ?? null;\\n if (!is_array($section)) { fwrite(STDERR, \\\"SECTION_MISSING\\\\n\\\"); exit(60); }\\n $result['sections'][$key] = [\\n 'available'=>(bool)($section['available'] ?? false),\\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\\n 'status'=>(string)($section['status'] ?? '')\\n ];\\n }\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\\\"1\\\"') !== false;\\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\\\"resources\\\"') !== false;\\n $result['locked_media'] = strpos($html, 'data-portal-locked=\\\"media\\\"') !== false;\\n $result['html_bytes'] = strlen($html);\\n if (!$result['html_marker']) { fwrite(STDERR, \\\"HTML_MARKER_MISSING\\\\n\\\"); exit(61); }\\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\\n fwrite(STDERR, \\\"GUEST_PERMISSION_CONTRACT_CHANGED\\\\n\\\"); exit(62);\\n }\\n if (!$result['locked_resources'] || !$result['locked_media']) {\\n fwrite(STDERR, \\\"LOCKED_COPY_MISSING\\\\n\\\"); exit(63);\\n }\\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\\n fwrite(STDERR, \\\"PUBLIC_SECTION_UNAVAILABLE\\\\n\\\"); exit(64);\\n }\\n} else if ($state === 'old') {\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\\n $result['html_bytes'] = strlen($html);\\n if ($result['html_marker'] || !$result['old_copy']) {\\n fwrite(STDERR, \\\"OLD_RENDER_CONTRACT\\\\n\\\"); exit(65);\\n }\\n} else {\\n exit(66);\\n}\\necho \\\"PORTAL200_RUNTIME_JSON=\\\" . json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n) . \\\"\\\\n\\\";\\n?>\"},{\"lineno\":311,\"segment\":\"r'''import html,json,re,subprocess,sys,urllib.parse\\nmode=sys.argv[1]\\nstate=sys.argv[2]\\nif mode=='staging':\\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\\nelif mode=='prod':\\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\\nelse:\\n raise SystemExit(40)\\ndef fetch(url):\\n cmd=[\\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\\n ]\\n if mode=='staging':\\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\\n cmd += [\\n '-o','-','-w','\\\\n__PORTAL_STATUS__%{http_code}',url\\n ]\\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\\n if p.returncode!=0: raise SystemExit(41)\\n text=p.stdout.decode('utf-8','replace')\\n marker='\\\\n__PORTAL_STATUS__'\\n if marker not in text: raise SystemExit(42)\\n body,status=text.rsplit(marker,1)\\n return int(status.strip()),body\\nroot=f'{scheme}://{host}/'\\nstatus,body=fetch(root)\\nhrefs=[]\\nfor raw in re.findall(r\\\"href=[\\\\\\\"']([^\\\\\\\"']*css\\\\.php[^\\\\\\\"']*)[\\\\\\\"']\\\",body,re.I):\\n raw=html.unescape(raw)\\n hrefs.append(urllib.parse.urljoin(root,raw))\\nnormal_css=[]\\nnormal_status=[]\\nfor u in hrefs:\\n s,b=fetch(u)\\n normal_status.append(s)\\n normal_css.append(b)\\nnormal_css_text='\\\\n'.join(normal_css)\\nforced_css_marker=None\\nforced_css_status=None\\nif mode=='staging' and state=='new' and hrefs:\\n u=urllib.parse.urlsplit(hrefs[0])\\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\\n out=[]\\n found=False\\n for k,v in pairs:\\n if k=='css':\\n found=True\\n parts=[x for x in v.split(',') if x]\\n if 'public:newfi_portal.less' not in parts:\\n parts.append('public:newfi_portal.less')\\n v=','.join(parts)\\n out.append((k,v))\\n if found:\\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\\n forced_css_status,forced_body=fetch(forced)\\n forced_css_marker='--newfi-portal200' in \",\"value\":\"import html,json,re,subprocess,sys,urllib.parse\\nmode=sys.argv[1]\\nstate=sys.argv[2]\\nif mode=='staging':\\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\\nelif mode=='prod':\\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\\nelse:\\n raise SystemExit(40)\\ndef fetch(url):\\n cmd=[\\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\\n ]\\n if mode=='staging':\\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\\n cmd += [\\n '-o','-','-w','\\\\n__PORTAL_STATUS__%{http_code}',url\\n ]\\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\\n if p.returncode!=0: raise SystemExit(41)\\n text=p.stdout.decode('utf-8','replace')\\n marker='\\\\n__PORTAL_STATUS__'\\n if marker not in text: raise SystemExit(42)\\n body,status=text.rsplit(marker,1)\\n return int(status.strip()),body\\nroot=f'{scheme}://{host}/'\\nstatus,body=fetch(root)\\nhrefs=[]\\nfor raw in re.findall(r\\\"href=[\\\\\\\"']([^\\\\\\\"']*css\\\\.php[^\\\\\\\"']*)[\\\\\\\"']\\\",body,re.I):\\n raw=html.unescape(raw)\\n hrefs.append(urllib.parse.urljoin(root,raw))\\nnormal_css=[]\\nnormal_status=[]\\nfor u in hrefs:\\n s,b=fetch(u)\\n normal_status.append(s)\\n normal_css.append(b)\\nnormal_css_text='\\\\n'.join(normal_css)\\nforced_css_marker=None\\nforced_css_status=None\\nif mode=='staging' and state=='new' and hrefs:\\n u=urllib.parse.urlsplit(hrefs[0])\\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\\n out=[]\\n found=False\\n for k,v in pairs:\\n if k=='css':\\n found=True\\n parts=[x for x in v.split(',') if x]\\n if 'public:newfi_portal.less' not in parts:\\n parts.append('public:newfi_portal.less')\\n v=','.join(parts)\\n out.append((k,v))\\n if found:\\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\\n forced_css_status,forced_body=fetch(forced)\\n forced_css_marker='--newfi-portal200' in forced_body\\nresult={\\n 'status':status,\\n 'bytes':len(body.encode()),\\n 'html_marker':'data-newfi-portal200=\\\"1\\\"' in body,\\n 'css_marker':'--newfi-portal200' in normal_css_text,\\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\\n 'css_count':len(hrefs),\\n 'css_status':normal_status,\\n 'forced_css_status':forced_css_status,\\n 'forced_css_marker':forced_css_marker,\\n}\\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\\nif status!=expected_status: raise SystemExit(45)\\nif mode=='prod':\\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\\nelif mode=='staging':\\n if any(x!=200 for x in normal_status): raise SystemExit(46)\\nelse:\\n raise SystemExit(50)\\nif mode=='prod' and state=='new':\\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\\nelif mode=='prod' and state=='old':\\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\\nelif mode=='staging' and state=='new':\\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\\nelif mode=='staging' and state=='old':\\n pass\\nelse:\\n raise SystemExit(50)\\n\"},{\"lineno\":14,\"segment\":\"\\\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\\\"\",\"value\":\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\"},{\"lineno\":20,\"segment\":\"\\\"_data/templates.xml\\\"\",\"value\":\"_data/templates.xml\"},{\"lineno\":24,\"segment\":\"\\\"_data/templates.xml\\\"\",\"value\":\"_data/templates.xml\"},{\"lineno\":27,\"segment\":\"/root/newfi-portal200-backup-\",\"value\":\"/root/newfi-portal200-backup-\"},{\"lineno\":401,\"segment\":\"\\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\"\",\"value\":\"PORTAL200_INSPECT_JSON_BEGIN\\n\"},{\"lineno\":402,\"segment\":\"\\\"\\\\nPORTAL200_INSPECT_JSON_END\\\"\",\"value\":\"\\nPORTAL200_INSPECT_JSON_END\"},{\"lineno\":734,\"segment\":\"\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\\\"\",\"value\":\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\"},{\"lineno\":736,\"segment\":\"\\\"PORTAL200_FLOW\\\"\",\"value\":\"PORTAL200_FLOW\"},{\"lineno\":737,\"segment\":\"\\\"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\\\"\",\"value\":\"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\"},{\"lineno\":497,\"segment\":\"\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest ! -e \\\"$B\\\"\\ninstall -d -m 700 \\\"$B/Pub/Controller\\\" \\\"$B/_data\\\"\\ncp -p -- \\\"$P/Pub/Controller/Portal.php\\\" \\\"$B/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$P/_data/templates.xml\\\" \\\"$B/_data/templates.xml\\\"\\ntest ! -e \\\"$P/Service/PortalData.php\\\"\\nprintf 'PORTAL200_BACKUP=PASS\\\\n'\\n\",\"value\":\"\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest ! -e \\\"$B\\\"\\ninstall -d -m 700 \\\"$B/Pub/Controller\\\" \\\"$B/_data\\\"\\ncp -p -- \\\"$P/Pub/Controller/Portal.php\\\" \\\"$B/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$P/_data/templates.xml\\\" \\\"$B/_data/templates.xml\\\"\\ntest ! -e \\\"$P/Service/PortalData.php\\\"\\nprintf 'PORTAL200_BACKUP=PASS\\n'\\n\"},{\"lineno\":507,\"segment\":\"\\\"PORTAL200_BACKUP=PASS\\\"\",\"value\":\"PORTAL200_BACKUP=PASS\"},{\"lineno\":523,\"segment\":\"\\\"; printf 'PORTAL200_WRITE=PASS\\\\\\\\n'\\\"\",\"value\":\"; printf 'PORTAL200_WRITE=PASS\\\\n'\"},{\"lineno\":531,\"segment\":\"\\\"PORTAL200_WRITE=PASS\\\"\",\"value\":\"PORTAL200_WRITE=PASS\"},{\"lineno\":539,\"segment\":\"\\\"_data/templates.xml\\\"\",\"value\":\"_data/templates.xml\"},{\"lineno\":546,\"segment\":\"\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest -f \\\"$B/Pub/Controller/Portal.php\\\"\\ntest -f \\\"$B/_data/templates.xml\\\"\\ncp -p -- \\\"$B/Pub/Controller/Portal.php\\\" \\\"$P/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$B/_data/templates.xml\\\" \\\"$P/_data/templates.xml\\\"\\nrm -f -- \\\"$P/Service/PortalData.php\\\"\\nrmdir -- \\\"$P/Service\\\" 2>/dev/null || true\\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\\\n'\\n\",\"value\":\"\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest -f \\\"$B/Pub/Controller/Portal.php\\\"\\ntest -f \\\"$B/_data/templates.xml\\\"\\ncp -p -- \\\"$B/Pub/Controller/Portal.php\\\" \\\"$P/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$B/_data/templates.xml\\\" \\\"$P/_data/templates.xml\\\"\\nrm -f -- \\\"$P/Service/PortalData.php\\\"\\nrmdir -- \\\"$P/Service\\\" 2>/dev/null || true\\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\\n\"},{\"lineno\":557,\"segment\":\"\\\"PORTAL200_SOURCE_RESTORE=PASS\\\"\",\"value\":\"PORTAL200_SOURCE_RESTORE=PASS\"},{\"lineno\":568,\"segment\":\"\\\"PORTAL200_IMPORT_JSON=\\\"\",\"value\":\"PORTAL200_IMPORT_JSON=\"},{\"lineno\":585,\"segment\":\"\\\"PORTAL200_RUNTIME\\\"\",\"value\":\"PORTAL200_RUNTIME\"},{\"lineno\":647,\"segment\":\"\\\"PORTAL200_HTTP\\\"\",\"value\":\"PORTAL200_HTTP\"},{\"lineno\":715,\"segment\":\"\\\"PORTAL200_CLUSTER\\\"\",\"value\":\"PORTAL200_CLUSTER\"},{\"lineno\":758,\"segment\":\"\\\"\\\"\",\"value\":\"\"},{\"lineno\":781,\"segment\":\"\\\"PORTAL200_CANDIDATE_IDENTITY\\\"\",\"value\":\"PORTAL200_CANDIDATE_IDENTITY\"},{\"lineno\":783,\"segment\":\"\\\"PORTAL200_ORCHESTRATION_FIXES\\\"\",\"value\":\"PORTAL200_ORCHESTRATION_FIXES\"},{\"lineno\":800,\"segment\":\"\\\"PORTAL200_GIT_PRESTATE\\\"\",\"value\":\"PORTAL200_GIT_PRESTATE\"},{\"lineno\":811,\"segment\":\"\\\"PORTAL200_RUNTIME_HELPER_LINT\\\"\",\"value\":\"PORTAL200_RUNTIME_HELPER_LINT\"},{\"lineno\":825,\"segment\":\"\\\"PORTAL200_ERROR_BASELINE\\\"\",\"value\":\"PORTAL200_ERROR_BASELINE\"},{\"lineno\":831,\"segment\":\"\\\"PORTAL200_PREFLIGHT\\\"\",\"value\":\"PORTAL200_PREFLIGHT\"},{\"lineno\":839,\"segment\":\"\\\"PORTAL200_STAGING_FIRST_APPLY\\\"\",\"value\":\"PORTAL200_STAGING_FIRST_APPLY\"},{\"lineno\":845,\"segment\":\"\\\"PORTAL200_STAGING_ROLLBACK_PROOF\\\"\",\"value\":\"PORTAL200_STAGING_ROLLBACK_PROOF\"},{\"lineno\":852,\"segment\":\"\\\"PORTAL200_STAGING_FINAL\\\"\",\"value\":\"PORTAL200_STAGING_FINAL\"},{\"lineno\":885,\"segment\":\"\\\"PORTAL200_GIT_PROMOTION\\\"\",\"value\":\"PORTAL200_GIT_PROMOTION\"},{\"lineno\":905,\"segment\":\"\\\"PORTAL200_PRODUCTION\\\"\",\"value\":\"PORTAL200_PRODUCTION\"},{\"lineno\":922,\"segment\":\"\\\"PORTAL200_FINAL_EVIDENCE\\\"\",\"value\":\"PORTAL200_FINAL_EVIDENCE\"},{\"lineno\":931,\"segment\":\"\\\"PORTAL200_PORTAL\\\"\",\"value\":\"PORTAL200_PORTAL\"},{\"lineno\":932,\"segment\":\"\\\"PORTAL200_STAGING\\\"\",\"value\":\"PORTAL200_STAGING\"},{\"lineno\":933,\"segment\":\"\\\"PORTAL200_PRODUCTION\\\"\",\"value\":\"PORTAL200_PRODUCTION\"},{\"lineno\":934,\"segment\":\"\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\\\"\",\"value\":\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\"},{\"lineno\":460,\"segment\":\"1000010\",\"value\":1000010},{\"lineno\":484,\"segment\":\"\\\"PORTAL200_NEW_XF_ERRORS\\\"\",\"value\":\"PORTAL200_NEW_XF_ERRORS\"},{\"lineno\":763,\"segment\":\"\\\"_data/templates.xml\\\"\",\"value\":\"_data/templates.xml\"},{\"lineno\":766,\"segment\":\"\\\"CANDIDATE_TEMPLATE_SET\\\"\",\"value\":\"CANDIDATE_TEMPLATE_SET\"},{\"lineno\":780,\"segment\":\"\\\"CANDIDATE_MUTATION_SCAN\\\"\",\"value\":\"CANDIDATE_MUTATION_SCAN\"},{\"lineno\":941,\"segment\":\"\\\"PORTAL200_FAILURE\\\"\",\"value\":\"PORTAL200_FAILURE\"},{\"lineno\":1004,\"segment\":\"\\\"PORTAL200_ROADMAP_REMAINS\\\"\",\"value\":\"PORTAL200_ROADMAP_REMAINS\"},{\"lineno\":1009,\"segment\":\"\\\"PORTAL200_FAILURE\\\"\",\"value\":\"PORTAL200_FAILURE\"},{\"lineno\":1016,\"segment\":\"\\\"PORTAL200_ROADMAP_REMAINS\\\"\",\"value\":\"PORTAL200_ROADMAP_REMAINS\"},{\"lineno\":461,\"segment\":\"\\\"version_string\\\"\",\"value\":\"version_string\"},{\"lineno\":618,\"segment\":\"\\\"PORTAL200_HTTP_EXEC\\\"\",\"value\":\"PORTAL200_HTTP_EXEC\"},{\"lineno\":762,\"segment\":\"\\\"CANDIDATE_IDENTITY\\\"\",\"value\":\"CANDIDATE_IDENTITY\"},{\"lineno\":460,\"segment\":\"\\\"version_id\\\"\",\"value\":\"version_id\"},{\"lineno\":576,\"segment\":\"\\\"PORTAL200_RUNTIME_JSON=\\\"\",\"value\":\"PORTAL200_RUNTIME_JSON=\"},{\"lineno\":613,\"segment\":\"\\\"PORTAL200_HTTP\\\"\",\"value\":\"PORTAL200_HTTP\"},{\"lineno\":638,\"segment\":\"\\\"PORTAL200_HTTP_JSON=\\\"\",\"value\":\"PORTAL200_HTTP_JSON=\"},{\"lineno\":991,\"segment\":\"\\\"PORTAL200_FAILURE_GIT_HISTORY\\\"\",\"value\":\"PORTAL200_FAILURE_GIT_HISTORY\"},{\"lineno\":606,\"segment\":\"\\\"PORTAL200_HTTP_JSON=\\\"\",\"value\":\"PORTAL200_HTTP_JSON=\"},{\"lineno\":952,\"segment\":\"\\\"PORTAL200_FAILURE_PROD_ROLLBACK\\\"\",\"value\":\"PORTAL200_FAILURE_PROD_ROLLBACK\"},{\"lineno\":966,\"segment\":\"\\\"PORTAL200_FAILURE_STAGING_ROLLBACK\\\"\",\"value\":\"PORTAL200_FAILURE_STAGING_ROLLBACK\"},{\"lineno\":985,\"segment\":\"\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\"\",\"value\":\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\"},{\"lineno\":955,\"segment\":\"\\\"PORTAL200_FAILURE_PROD_ROLLBACK\\\"\",\"value\":\"PORTAL200_FAILURE_PROD_ROLLBACK\"},{\"lineno\":969,\"segment\":\"\\\"PORTAL200_FAILURE_STAGING_ROLLBACK\\\"\",\"value\":\"PORTAL200_FAILURE_STAGING_ROLLBACK\"},{\"lineno\":987,\"segment\":\"\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\"\",\"value\":\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\"},{\"lineno\":519,\"segment\":\"\\\"/.portal200.XXXXXX\\\"\",\"value\":\"/.portal200.XXXXXX\"}]\nRELEVANT_CALLS=[{\"args\":[\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\"],\"kwargs\":{},\"lineno\":14,\"name\":\"pathlib.Path\",\"source\":\"pathlib.Path(\\\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\\\")\"},{\"args\":[\"src/addons/Newfi/Portal\"],\"kwargs\":{},\"lineno\":15,\"name\":\"pathlib.Path\",\"source\":\"pathlib.Path(\\\"src/addons/Newfi/Portal\\\")\"},{\"args\":[\"PORTAL200_FLOW\",\"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\"],\"kwargs\":{},\"lineno\":735,\"name\":\"emit\",\"source\":\"emit(\\n \\\"PORTAL200_FLOW\\\",\\n \\\"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\\\",\\n)\"},{\"args\":[\"[\\\"/usr/bin/git\\\", \\\"-C\\\", str(WT), *args]\"],\"kwargs\":{\"ok\":\"ok\",\"timeout\":\"timeout\"},\"lineno\":77,\"name\":\"run\",\"source\":\"run([\\\"/usr/bin/git\\\", \\\"-C\\\", str(WT), *args], timeout=timeout, ok=ok)\"},{\"args\":[\"cmd\"],\"kwargs\":{\"input_bytes\":\"input_bytes\",\"timeout\":\"timeout + 30\"},\"lineno\":135,\"name\":\"run\",\"source\":\"run(cmd, input_bytes=input_bytes, timeout=timeout + 30)\"},{\"args\":[\"__STATE__\",\"state\"],\"kwargs\":{},\"lineno\":573,\"name\":\"RUNTIME_VERIFY_PHP_TEMPLATE.replace\",\"source\":\"RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", state)\"},{\"args\":[\"target\",\"new\"],\"kwargs\":{},\"lineno\":659,\"name\":\"runtime_verify\",\"source\":\"runtime_verify(target, \\\"new\\\")\"},{\"args\":[[\"/usr/sbin/qm\",\"status\",\"210\"]],\"kwargs\":{\"timeout\":30},\"lineno\":693,\"name\":\"run\",\"source\":\"run([\\\"/usr/sbin/qm\\\", \\\"status\\\", \\\"210\\\"], timeout=30)\"},{\"args\":[[\"/usr/bin/ssh\",\"-T\",\"-o\",\"BatchMode=yes\",\"-o\",\"StrictHostKeyChecking=yes\",\"-o\",\"UpdateHostKeys=no\",\"-o\",\"ConnectTimeout=10\",\"root@pve03\",\"/usr/sbin/qm status 211\"]],\"kwargs\":{\"timeout\":40},\"lineno\":696,\"name\":\"run\",\"source\":\"run(\\n [\\n \\\"/usr/bin/ssh\\\",\\n \\\"-T\\\",\\n \\\"-o\\\",\\n \\\"BatchMode=yes\\\",\\n \\\"-o\\\",\\n \\\"StrictHostKeyChecking=yes\\\",\\n \\\"-o\\\",\\n \\\"UpdateHostKeys=no\\\",\\n \\\"-o\\\",\\n \\\"ConnectTimeout=10\\\",\\n \\\"root@pve03\\\",\\n \\\"/usr/sbin/qm status 211\\\",\\n ],\\n timeout=40,\\n )\"},{\"args\":[\"CAND_ROOT / \\\"_data/templates.xml\\\"\"],\"kwargs\":{},\"lineno\":763,\"name\":\"ET.parse\",\"source\":\"ET.parse(CAND_ROOT / \\\"_data/templates.xml\\\")\"},{\"args\":[\"utf-8\"],\"kwargs\":{},\"lineno\":767,\"name\":\"read_text\",\"source\":\"(CAND_ROOT / \\\"Service/PortalData.php\\\").read_text(\\\"utf-8\\\")\"},{\"args\":[\"PORTAL200_CANDIDATE_IDENTITY\",\"PASS\"],\"kwargs\":{},\"lineno\":781,\"name\":\"emit\",\"source\":\"emit(\\\"PORTAL200_CANDIDATE_IDENTITY\\\", \\\"PASS\\\")\"},{\"args\":[\"prod\",\"old\"],\"kwargs\":{},\"lineno\":827,\"name\":\"runtime_verify\",\"source\":\"runtime_verify(\\\"prod\\\", \\\"old\\\")\"},{\"args\":[\"staging\"],\"kwargs\":{},\"lineno\":836,\"name\":\"install_candidate\",\"source\":\"install_candidate(\\\"staging\\\")\"},{\"args\":[\"staging\"],\"kwargs\":{},\"lineno\":848,\"name\":\"install_candidate\",\"source\":\"install_candidate(\\\"staging\\\")\"},{\"args\":[\"prod\"],\"kwargs\":{},\"lineno\":902,\"name\":\"install_candidate\",\"source\":\"install_candidate(\\\"prod\\\")\"},{\"args\":[\"cmd\"],\"kwargs\":{\"check\":false,\"input\":\"input_bytes\",\"stderr\":\"subprocess.PIPE\",\"stdout\":\"subprocess.PIPE\",\"timeout\":\"timeout\"},\"lineno\":61,\"name\":\"subprocess.run\",\"source\":\"subprocess.run(\\n cmd,\\n input=input_bytes,\\n stdout=subprocess.PIPE,\\n stderr=subprocess.PIPE,\\n timeout=timeout,\\n check=False,\\n )\"},{\"args\":[\"target\",\"rel\",\"(CAND_ROOT / rel).read_bytes()\"],\"kwargs\":{},\"lineno\":541,\"name\":\"guest_write\",\"source\":\"guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\"},{\"args\":[\"target\",\"old\"],\"kwargs\":{},\"lineno\":674,\"name\":\"runtime_verify\",\"source\":\"runtime_verify(target, \\\"old\\\")\"},{\"args\":[\"CANDIDATE_TEMPLATE_SET\"],\"kwargs\":{},\"lineno\":766,\"name\":\"fail\",\"source\":\"fail(\\\"CANDIDATE_TEMPLATE_SET\\\")\"},{\"args\":[\"CANDIDATE_MUTATION_SCAN\"],\"kwargs\":{},\"lineno\":780,\"name\":\"fail\",\"source\":\"fail(\\\"CANDIDATE_MUTATION_SCAN\\\")\"},{\"args\":[\"__STATE__\",\"new\"],\"kwargs\":{},\"lineno\":807,\"name\":\"RUNTIME_VERIFY_PHP_TEMPLATE.replace\",\"source\":\"RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", \\\"new\\\")\"},{\"args\":[\"__STATE__\",\"new\"],\"kwargs\":{},\"lineno\":810,\"name\":\"RUNTIME_VERIFY_PHP_TEMPLATE.replace\",\"source\":\"RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", \\\"new\\\")\"},{\"args\":[\"CAND_ROOT / rel\",\"dest\"],\"kwargs\":{},\"lineno\":858,\"name\":\"shutil.copyfile\",\"source\":\"shutil.copyfile(CAND_ROOT / rel, dest)\"},{\"args\":[\"addon.get(\\\"version_id\\\", 0)\"],\"kwargs\":{},\"lineno\":460,\"name\":\"int\",\"source\":\"int(addon.get(\\\"version_id\\\", 0))\"},{\"args\":[\"version_string\"],\"kwargs\":{},\"lineno\":461,\"name\":\"addon.get\",\"source\":\"addon.get(\\\"version_string\\\")\"},{\"args\":[],\"kwargs\":{},\"lineno\":541,\"name\":\"read_bytes\",\"source\":\"(CAND_ROOT / rel).read_bytes()\"},{\"args\":[\"argv\"],\"kwargs\":{\"check\":false,\"input\":\"HTTP_VERIFY_PY.encode()\",\"stderr\":\"subprocess.PIPE\",\"stdout\":\"subprocess.PIPE\",\"timeout\":180},\"lineno\":593,\"name\":\"subprocess.run\",\"source\":\"subprocess.run(\\n argv,\\n input=HTTP_VERIFY_PY.encode(),\\n stdout=subprocess.PIPE,\\n stderr=subprocess.PIPE,\\n timeout=180,\\n check=False,\\n )\"},{\"args\":[\"CANDIDATE_IDENTITY\"],\"kwargs\":{},\"lineno\":762,\"name\":\"fail\",\"source\":\"fail(\\\"CANDIDATE_IDENTITY\\\")\"},{\"args\":[\"version_id\",0],\"kwargs\":{},\"lineno\":460,\"name\":\"addon.get\",\"source\":\"addon.get(\\\"version_id\\\", 0)\"},{\"args\":[\"old\"],\"kwargs\":{},\"lineno\":979,\"name\":\"dest.write_bytes\",\"source\":\"dest.write_bytes(old)\"}]\nRELEVANT_SOURCE_CONTEXTS=[{\"line\":10,\"text\":\"import subprocess\\nimport sys\\nimport xml.etree.ElementTree as ET\\n\\nCID = \\\"NEWFI-260907-PORTAL200-APPLY3\\\"\\nBRANCH = \\\"implementation/newfi-closed-mvp-20260906\\\"\\nOLD_HEAD = \\\"06bab5acd124e768f70305b3d7eca2f595871890\\\"\\nWT = pathlib.Path(\\\"/root/newfi-portal/.worktrees/implementation-newfi-closed-mvp-20260906\\\")\\nCAND_ROOT = pathlib.Path(\\\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\\\")\\nPORTAL_REL = pathlib.Path(\\\"src/addons/Newfi/Portal\\\")\\nGIT_PORTAL = WT / PORTAL_REL\\nEXPECTED_NEW = {\\n \\\"Pub/Controller/Portal.php\\\": (363, \\\"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\\\"),\"},{\"line\":14,\"text\":\"CID = \\\"NEWFI-260907-PORTAL200-APPLY3\\\"\\nBRANCH = \\\"implementation/newfi-closed-mvp-20260906\\\"\\nOLD_HEAD = \\\"06bab5acd124e768f70305b3d7eca2f595871890\\\"\\nWT = pathlib.Path(\\\"/root/newfi-portal/.worktrees/implementation-newfi-closed-mvp-20260906\\\")\\nCAND_ROOT = pathlib.Path(\\\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\\\")\\nPORTAL_REL = pathlib.Path(\\\"src/addons/Newfi/Portal\\\")\\nGIT_PORTAL = WT / PORTAL_REL\\nEXPECTED_NEW = {\\n \\\"Pub/Controller/Portal.php\\\": (363, \\\"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\\\"),\\n \\\"Service/PortalData.php\\\": (5213, \\\"4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d\\\"),\\n \\\"_data/templates.xml\\\": (10977, \\\"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\\\"),\\n}\\nEXPECTED_OLD = {\"},{\"line\":20,\"text\":\"GIT_PORTAL = WT / PORTAL_REL\\nEXPECTED_NEW = {\\n \\\"Pub/Controller/Portal.php\\\": (363, \\\"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\\\"),\\n \\\"Service/PortalData.php\\\": (5213, \\\"4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d\\\"),\\n \\\"_data/templates.xml\\\": (10977, \\\"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\\\"),\\n}\\nEXPECTED_OLD = {\\n \\\"Pub/Controller/Portal.php\\\": (247, \\\"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\\\"),\\n \\\"_data/templates.xml\\\": (1461, \\\"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\\\"),\\n}\\nEXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)]\\nBACKUP = f\\\"/root/newfi-portal200-backup-{CID}\\\"\\n\"},{\"line\":24,\"text\":\" \\\"_data/templates.xml\\\": (10977, \\\"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\\\"),\\n}\\nEXPECTED_OLD = {\\n \\\"Pub/Controller/Portal.php\\\": (247, \\\"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\\\"),\\n \\\"_data/templates.xml\\\": (1461, \\\"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\\\"),\\n}\\nEXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)]\\nBACKUP = f\\\"/root/newfi-portal200-backup-{CID}\\\"\\n\\n\\nclass PortalError(RuntimeError):\\n pass\\n\"},{\"line\":27,\"text\":\" \\\"Pub/Controller/Portal.php\\\": (247, \\\"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\\\"),\\n \\\"_data/templates.xml\\\": (1461, \\\"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\\\"),\\n}\\nEXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)]\\nBACKUP = f\\\"/root/newfi-portal200-backup-{CID}\\\"\\n\\n\\nclass PortalError(RuntimeError):\\n pass\\n\\n\\ndef emit(key, value=None):\\n if value is None:\"},{\"line\":160,\"text\":\"}\\n$portal = $root . '/src/addons/Newfi/Portal';\\n$templates = $db->fetchAll(\\n \\\"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \\\" .\\n \\\"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \\\" .\\n \\\"LOCATE('--newfi-portal200', template) > 0 AS css_marker \\\" .\\n \\\"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\\\"\\n);\\n$result = [\\n 'hostname' => gethostname(),\\n 'application' => [\\n 'boardActive' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\\\"),\\n 'boardUrl' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\\\"),\"},{\"line\":161,\"text\":\"$portal = $root . '/src/addons/Newfi/Portal';\\n$templates = $db->fetchAll(\\n \\\"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \\\" .\\n \\\"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \\\" .\\n \\\"LOCATE('--newfi-portal200', template) > 0 AS css_marker \\\" .\\n \\\"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\\\"\\n);\\n$result = [\\n 'hostname' => gethostname(),\\n 'application' => [\\n 'boardActive' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\\\"),\\n 'boardUrl' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\\\"),\\n 'indexRoute' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\\\"),\"},{\"line\":175,\"text\":\" 'error_count' => (int)$db->fetchOne(\\\"SELECT COUNT(*) FROM xf_error_log\\\"),\\n 'error_max_id' => (int)$db->fetchOne(\\\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\\\")\\n ],\\n 'addon' => $db->fetchRow(\\n \\\"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \\\" .\\n \\\"FROM xf_addon WHERE addon_id='Newfi/Portal'\\\"\\n ),\\n 'source' => [\\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\\n ],\\n 'templates' => $templates\"},{\"line\":181,\"text\":\" ),\\n 'source' => [\\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\\n ],\\n 'templates' => $templates\\n];\\necho \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\";\\necho json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\"},{\"line\":185,\"text\":\" '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\\n ],\\n 'templates' => $templates\\n];\\necho \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\";\\necho json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\\necho \\\"\\\\nPORTAL200_INSPECT_JSON_END\\\\n\\\";\\n?>'''\\n\\n\"},{\"line\":190,\"text\":\"echo json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\\necho \\\"\\\\nPORTAL200_INSPECT_JSON_END\\\\n\\\";\\n?>'''\\n\\n\\nIMPORT_PHP = r'''postDataImport();\\n$repo = $app->repository(\\\\XF\\\\Repository\\\\StyleRepository::class);\\n$repo->updateAllStylesLastModifiedDateLater();\\n$addOn->postRebuild();\\necho \\\"PORTAL200_IMPORT_JSON=\\\" . json_encode(\\n ['result'=>'PASS','containers'=>$imported],\\n JSON_UNESCAPED_SLASHES\\n) . \\\"\\\\n\\\";\\n?>'''\\n\\n\\nERRORS_PHP_TEMPLATE = r'''(bool)($section['hasItems'] ?? false),\\n 'status'=>(string)($section['status'] ?? '')\\n ];\\n }\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\\\"1\\\"') !== false;\\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\\\"resources\\\"') !== false;\\n $result['locked_media'] = strpos($html, 'data-portal-locked=\\\"media\\\"') !== false;\\n $result['html_bytes'] = strlen($html);\\n if (!$result['html_marker']) { fwrite(STDERR, \\\"HTML_MARKER_MISSING\\\\n\\\"); exit(61); }\\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\\n fwrite(STDERR, \\\"GUEST_PERMISSION_CONTRACT_CHANGED\\\\n\\\"); exit(62);\\n }\\n if (!$result['locked_resources'] || !$result['locked_media']) {\"},{\"line\":288,\"text\":\" if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\\n fwrite(STDERR, \\\"GUEST_PERMISSION_CONTRACT_CHANGED\\\\n\\\"); exit(62);\\n }\\n if (!$result['locked_resources'] || !$result['locked_media']) {\\n fwrite(STDERR, \\\"LOCKED_COPY_MISSING\\\\n\\\"); exit(63);\\n }\\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\\n fwrite(STDERR, \\\"PUBLIC_SECTION_UNAVAILABLE\\\\n\\\"); exit(64);\\n }\\n} else if ($state === 'old') {\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\"},{\"line\":295,\"text\":\" fwrite(STDERR, \\\"PUBLIC_SECTION_UNAVAILABLE\\\\n\\\"); exit(64);\\n }\\n} else if ($state === 'old') {\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\\n $result['html_bytes'] = strlen($html);\\n if ($result['html_marker'] || !$result['old_copy']) {\\n fwrite(STDERR, \\\"OLD_RENDER_CONTRACT\\\\n\\\"); exit(65);\\n }\\n} else {\\n exit(66);\\n}\"},{\"line\":296,\"text\":\" }\\n} else if ($state === 'old') {\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\\n $result['html_bytes'] = strlen($html);\\n if ($result['html_marker'] || !$result['old_copy']) {\\n fwrite(STDERR, \\\"OLD_RENDER_CONTRACT\\\\n\\\"); exit(65);\\n }\\n} else {\\n exit(66);\\n}\\necho \\\"PORTAL200_RUNTIME_JSON=\\\" . json_encode(\"},{\"line\":298,\"text\":\" $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\\n $result['html_bytes'] = strlen($html);\\n if ($result['html_marker'] || !$result['old_copy']) {\\n fwrite(STDERR, \\\"OLD_RENDER_CONTRACT\\\\n\\\"); exit(65);\\n }\\n} else {\\n exit(66);\\n}\\necho \\\"PORTAL200_RUNTIME_JSON=\\\" . json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\"},{\"line\":304,\"text\":\" }\\n} else {\\n exit(66);\\n}\\necho \\\"PORTAL200_RUNTIME_JSON=\\\" . json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n) . \\\"\\\\n\\\";\\n?>'''\\n\\n\\nHTTP_VERIFY_PY = r'''import html,json,re,subprocess,sys,urllib.parse\\nmode=sys.argv[1]\"},{\"line\":367,\"text\":\" out.append((k,v))\\n if found:\\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\\n forced_css_status,forced_body=fetch(forced)\\n forced_css_marker='--newfi-portal200' in forced_body\\nresult={\\n 'status':status,\\n 'bytes':len(body.encode()),\\n 'html_marker':'data-newfi-portal200=\\\"1\\\"' in body,\\n 'css_marker':'--newfi-portal200' in normal_css_text,\\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\\n 'css_count':len(hrefs),\\n 'css_status':normal_status,\"},{\"line\":371,\"text\":\" forced_css_marker='--newfi-portal200' in forced_body\\nresult={\\n 'status':status,\\n 'bytes':len(body.encode()),\\n 'html_marker':'data-newfi-portal200=\\\"1\\\"' in body,\\n 'css_marker':'--newfi-portal200' in normal_css_text,\\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\\n 'css_count':len(hrefs),\\n 'css_status':normal_status,\\n 'forced_css_status':forced_css_status,\\n 'forced_css_marker':forced_css_marker,\\n}\\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\"},{\"line\":372,\"text\":\"result={\\n 'status':status,\\n 'bytes':len(body.encode()),\\n 'html_marker':'data-newfi-portal200=\\\"1\\\"' in body,\\n 'css_marker':'--newfi-portal200' in normal_css_text,\\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\\n 'css_count':len(hrefs),\\n 'css_status':normal_status,\\n 'forced_css_status':forced_css_status,\\n 'forced_css_marker':forced_css_marker,\\n}\\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\\nif status!=expected_status: raise SystemExit(45)\"},{\"line\":373,\"text\":\" 'status':status,\\n 'bytes':len(body.encode()),\\n 'html_marker':'data-newfi-portal200=\\\"1\\\"' in body,\\n 'css_marker':'--newfi-portal200' in normal_css_text,\\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\\n 'css_count':len(hrefs),\\n 'css_status':normal_status,\\n 'forced_css_status':forced_css_status,\\n 'forced_css_marker':forced_css_marker,\\n}\\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\\nif status!=expected_status: raise SystemExit(45)\\nif mode=='prod':\"},{\"line\":379,\"text\":\" 'css_status':normal_status,\\n 'forced_css_status':forced_css_status,\\n 'forced_css_marker':forced_css_marker,\\n}\\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\\nif status!=expected_status: raise SystemExit(45)\\nif mode=='prod':\\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\\nelif mode=='staging':\\n if any(x!=200 for x in normal_status): raise SystemExit(46)\\nelse:\\n raise SystemExit(50)\\nif mode=='prod' and state=='new':\"},{\"line\":390,\"text\":\" raise SystemExit(50)\\nif mode=='prod' and state=='new':\\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\\nelif mode=='prod' and state=='old':\\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\\nelif mode=='staging' and state=='new':\\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\\nelif mode=='staging' and state=='old':\\n pass\\nelse:\\n raise SystemExit(50)\\n'''\\n\"},{\"line\":401,\"text\":\"'''\\n\\n\\ndef parse_inspect(text):\\n b = \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\"\\n e = \\\"\\\\nPORTAL200_INSPECT_JSON_END\\\"\\n if b not in text or e not in text:\\n fail(\\\"INSPECT_MARKERS\\\")\\n try:\\n return json.loads(text.split(b, 1)[1].split(e, 1)[0])\\n except Exception:\\n fail(\\\"INSPECT_JSON\\\")\\n\"},{\"line\":402,\"text\":\"\\n\\ndef parse_inspect(text):\\n b = \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\"\\n e = \\\"\\\\nPORTAL200_INSPECT_JSON_END\\\"\\n if b not in text or e not in text:\\n fail(\\\"INSPECT_MARKERS\\\")\\n try:\\n return json.loads(text.split(b, 1)[1].split(e, 1)[0])\\n except Exception:\\n fail(\\\"INSPECT_JSON\\\")\\n\\n\"},{\"line\":460,\"text\":\" and a.get(\\\"boardUrl\\\") == expected[1]\\n and a.get(\\\"indexRoute\\\") == \\\"portal/\\\"\\n and a.get(\\\"defaultStyleId\\\") == \\\"4\\\"\\n and addon.get(\\\"addon_id\\\") == \\\"Newfi/Portal\\\"\\n and int(addon.get(\\\"version_id\\\", 0)) == 1000010\\n and addon.get(\\\"version_string\\\") == \\\"1.0.0 Alpha 1\\\"\\n and int(addon.get(\\\"active\\\", 0)) == 1\\n and int(addon.get(\\\"is_processing\\\", 1)) == 0\\n )\\n\\n\\ndef new_errors(target, baseline):\\n raw = qga(\"},{\"line\":461,\"text\":\" and a.get(\\\"indexRoute\\\") == \\\"portal/\\\"\\n and a.get(\\\"defaultStyleId\\\") == \\\"4\\\"\\n and addon.get(\\\"addon_id\\\") == \\\"Newfi/Portal\\\"\\n and int(addon.get(\\\"version_id\\\", 0)) == 1000010\\n and addon.get(\\\"version_string\\\") == \\\"1.0.0 Alpha 1\\\"\\n and int(addon.get(\\\"active\\\", 0)) == 1\\n and int(addon.get(\\\"is_processing\\\", 1)) == 0\\n )\\n\\n\\ndef new_errors(target, baseline):\\n raw = qga(\\n target,\"},{\"line\":484,\"text\":\"def assert_no_new_errors(target, baseline, phase):\\n rows = new_errors(target, baseline)\\n if rows:\\n emit(\\n \\\"PORTAL200_NEW_XF_ERRORS\\\",\\n {\\n \\\"target\\\": target,\\n \\\"phase\\\": phase,\\n \\\"count\\\": len(rows),\\n \\\"ids\\\": [r.get(\\\"error_id\\\") for r in rows],\\n },\\n )\\n fail(\\\"NEW_XF_ERRORS\\\")\"},{\"line\":502,\"text\":\"P=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest ! -e \\\"$B\\\"\\ninstall -d -m 700 \\\"$B/Pub/Controller\\\" \\\"$B/_data\\\"\\ncp -p -- \\\"$P/Pub/Controller/Portal.php\\\" \\\"$B/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$P/_data/templates.xml\\\" \\\"$B/_data/templates.xml\\\"\\ntest ! -e \\\"$P/Service/PortalData.php\\\"\\nprintf 'PORTAL200_BACKUP=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_BACKUP=PASS\\\" not in out:\\n fail(\\\"BACKUP_FAILED\\\")\\n\\n\"},{\"line\":504,\"text\":\"install -d -m 700 \\\"$B/Pub/Controller\\\" \\\"$B/_data\\\"\\ncp -p -- \\\"$P/Pub/Controller/Portal.php\\\" \\\"$B/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$P/_data/templates.xml\\\" \\\"$B/_data/templates.xml\\\"\\ntest ! -e \\\"$P/Service/PortalData.php\\\"\\nprintf 'PORTAL200_BACKUP=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_BACKUP=PASS\\\" not in out:\\n fail(\\\"BACKUP_FAILED\\\")\\n\\n\\ndef guest_write(target, rel, data):\\n dest = f\\\"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}\\\"\"},{\"line\":507,\"text\":\"test ! -e \\\"$P/Service/PortalData.php\\\"\\nprintf 'PORTAL200_BACKUP=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_BACKUP=PASS\\\" not in out:\\n fail(\\\"BACKUP_FAILED\\\")\\n\\n\\ndef guest_write(target, rel, data):\\n dest = f\\\"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}\\\"\\n parent = str(pathlib.PurePosixPath(dest).parent)\\n script = (\\n \\\"set -eu; umask 022; \\\"\"},{\"line\":519,\"text\":\" \\\"set -eu; umask 022; \\\"\\n + \\\"install -d -m 755 \\\"\\n + shlex.quote(parent)\\n + \\\"; T=$(mktemp \\\"\\n + shlex.quote(parent + \\\"/.portal200.XXXXXX\\\")\\n + \\\"); cat >\\\\\\\"$T\\\\\\\"; chmod 0644 \\\\\\\"$T\\\\\\\"; chown root:root \\\\\\\"$T\\\\\\\"; \\\"\\n + \\\"mv -f -- \\\\\\\"$T\\\\\\\" \\\"\\n + shlex.quote(dest)\\n + \\\"; printf 'PORTAL200_WRITE=PASS\\\\\\\\n'\\\"\\n )\\n out = qga(\\n target,\\n [\\\"/bin/bash\\\", \\\"-lc\\\", script],\"},{\"line\":523,\"text\":\" + shlex.quote(parent + \\\"/.portal200.XXXXXX\\\")\\n + \\\"); cat >\\\\\\\"$T\\\\\\\"; chmod 0644 \\\\\\\"$T\\\\\\\"; chown root:root \\\\\\\"$T\\\\\\\"; \\\"\\n + \\\"mv -f -- \\\\\\\"$T\\\\\\\" \\\"\\n + shlex.quote(dest)\\n + \\\"; printf 'PORTAL200_WRITE=PASS\\\\\\\\n'\\\"\\n )\\n out = qga(\\n target,\\n [\\\"/bin/bash\\\", \\\"-lc\\\", script],\\n input_bytes=data,\\n timeout=90,\\n )\\n if \\\"PORTAL200_WRITE=PASS\\\" not in out:\"},{\"line\":531,\"text\":\" [\\\"/bin/bash\\\", \\\"-lc\\\", script],\\n input_bytes=data,\\n timeout=90,\\n )\\n if \\\"PORTAL200_WRITE=PASS\\\" not in out:\\n fail(\\\"WRITE_FAILED\\\")\\n\\n\\ndef install_candidate(target):\\n for rel in [\\n \\\"Pub/Controller/Portal.php\\\",\\n \\\"Service/PortalData.php\\\",\\n \\\"_data/templates.xml\\\",\"},{\"line\":532,\"text\":\" input_bytes=data,\\n timeout=90,\\n )\\n if \\\"PORTAL200_WRITE=PASS\\\" not in out:\\n fail(\\\"WRITE_FAILED\\\")\\n\\n\\ndef install_candidate(target):\\n for rel in [\\n \\\"Pub/Controller/Portal.php\\\",\\n \\\"Service/PortalData.php\\\",\\n \\\"_data/templates.xml\\\",\\n ]:\"},{\"line\":535,\"text\":\" if \\\"PORTAL200_WRITE=PASS\\\" not in out:\\n fail(\\\"WRITE_FAILED\\\")\\n\\n\\ndef install_candidate(target):\\n for rel in [\\n \\\"Pub/Controller/Portal.php\\\",\\n \\\"Service/PortalData.php\\\",\\n \\\"_data/templates.xml\\\",\\n ]:\\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\\n\\n\"},{\"line\":539,\"text\":\"def install_candidate(target):\\n for rel in [\\n \\\"Pub/Controller/Portal.php\\\",\\n \\\"Service/PortalData.php\\\",\\n \\\"_data/templates.xml\\\",\\n ]:\\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\\n\\n\\ndef restore_old(target):\\n script = f'''set -eu\\nB={shlex.quote(BACKUP)}\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\"},{\"line\":541,\"text\":\" \\\"Pub/Controller/Portal.php\\\",\\n \\\"Service/PortalData.php\\\",\\n \\\"_data/templates.xml\\\",\\n ]:\\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\\n\\n\\ndef restore_old(target):\\n script = f'''set -eu\\nB={shlex.quote(BACKUP)}\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest -f \\\"$B/Pub/Controller/Portal.php\\\"\\ntest -f \\\"$B/_data/templates.xml\\\"\"},{\"line\":549,\"text\":\" script = f'''set -eu\\nB={shlex.quote(BACKUP)}\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest -f \\\"$B/Pub/Controller/Portal.php\\\"\\ntest -f \\\"$B/_data/templates.xml\\\"\\ncp -p -- \\\"$B/Pub/Controller/Portal.php\\\" \\\"$P/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$B/_data/templates.xml\\\" \\\"$P/_data/templates.xml\\\"\\nrm -f -- \\\"$P/Service/PortalData.php\\\"\\nrmdir -- \\\"$P/Service\\\" 2>/dev/null || true\\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_SOURCE_RESTORE=PASS\\\" not in out:\"},{\"line\":551,\"text\":\"P=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest -f \\\"$B/Pub/Controller/Portal.php\\\"\\ntest -f \\\"$B/_data/templates.xml\\\"\\ncp -p -- \\\"$B/Pub/Controller/Portal.php\\\" \\\"$P/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$B/_data/templates.xml\\\" \\\"$P/_data/templates.xml\\\"\\nrm -f -- \\\"$P/Service/PortalData.php\\\"\\nrmdir -- \\\"$P/Service\\\" 2>/dev/null || true\\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_SOURCE_RESTORE=PASS\\\" not in out:\\n fail(\\\"SOURCE_RESTORE_FAILED\\\")\\n\"},{\"line\":554,\"text\":\"cp -p -- \\\"$B/Pub/Controller/Portal.php\\\" \\\"$P/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$B/_data/templates.xml\\\" \\\"$P/_data/templates.xml\\\"\\nrm -f -- \\\"$P/Service/PortalData.php\\\"\\nrmdir -- \\\"$P/Service\\\" 2>/dev/null || true\\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_SOURCE_RESTORE=PASS\\\" not in out:\\n fail(\\\"SOURCE_RESTORE_FAILED\\\")\\n\\n\\ndef import_addon(target):\\n out = qga(\"},{\"line\":557,\"text\":\"rmdir -- \\\"$P/Service\\\" 2>/dev/null || true\\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_SOURCE_RESTORE=PASS\\\" not in out:\\n fail(\\\"SOURCE_RESTORE_FAILED\\\")\\n\\n\\ndef import_addon(target):\\n out = qga(\\n target,\\n [\\\"/usr/bin/php8.3\\\"],\\n input_bytes=IMPORT_PHP.encode(),\"},{\"line\":568,\"text\":\" [\\\"/usr/bin/php8.3\\\"],\\n input_bytes=IMPORT_PHP.encode(),\\n timeout=240,\\n )\\n if \\\"PORTAL200_IMPORT_JSON=\\\" not in out:\\n fail(\\\"IMPORT_FAILED\\\")\\n\\n\\ndef runtime_verify(target, state):\\n src = RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", state)\\n out = qga(target, [\\\"/usr/bin/php8.3\\\"], input_bytes=src.encode(), timeout=120)\\n line = next(\\n (x for x in out.splitlines() if x.startswith(\\\"PORTAL200_RUNTIME_JSON=\\\")),\"},{\"line\":576,\"text\":\"def runtime_verify(target, state):\\n src = RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", state)\\n out = qga(target, [\\\"/usr/bin/php8.3\\\"], input_bytes=src.encode(), timeout=120)\\n line = next(\\n (x for x in out.splitlines() if x.startswith(\\\"PORTAL200_RUNTIME_JSON=\\\")),\\n None,\\n )\\n if not line:\\n fail(\\\"RUNTIME_MARKER_MISSING\\\")\\n try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n except Exception:\\n fail(\\\"RUNTIME_JSON\\\")\"},{\"line\":585,\"text\":\" try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n except Exception:\\n fail(\\\"RUNTIME_JSON\\\")\\n emit(\\\"PORTAL200_RUNTIME\\\", {\\\"target\\\": target, **data})\\n return data\\n\\n\\ndef http_verify(target, state):\\n argv = [\\\"/usr/bin/python3\\\", \\\"-\\\", target, state]\\n if target == \\\"prod\\\":\\n try:\\n p = subprocess.run(\"},{\"line\":606,\"text\":\" fail(\\\"HTTP_PROD_TIMEOUT\\\")\\n out = p.stdout.decode(\\\"utf-8\\\", \\\"replace\\\")\\n err = p.stderr.decode(\\\"utf-8\\\", \\\"replace\\\")\\n line = next(\\n (x for x in out.splitlines() if x.startswith(\\\"PORTAL200_HTTP_JSON=\\\")),\\n None,\\n )\\n data = None\\n if line:\\n try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n emit(\\\"PORTAL200_HTTP\\\", {\\\"target\\\": target, \\\"state\\\": state, **data})\\n except Exception:\"},{\"line\":613,\"text\":\" data = None\\n if line:\\n try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n emit(\\\"PORTAL200_HTTP\\\", {\\\"target\\\": target, \\\"state\\\": state, **data})\\n except Exception:\\n fail(\\\"HTTP_JSON\\\")\\n if p.returncode != 0:\\n emit(\\n \\\"PORTAL200_HTTP_EXEC\\\",\\n {\\n \\\"target\\\": target,\\n \\\"state\\\": state,\"},{\"line\":618,\"text\":\" except Exception:\\n fail(\\\"HTTP_JSON\\\")\\n if p.returncode != 0:\\n emit(\\n \\\"PORTAL200_HTTP_EXEC\\\",\\n {\\n \\\"target\\\": target,\\n \\\"state\\\": state,\\n \\\"rc\\\": p.returncode,\\n \\\"stderr\\\": err[:800],\\n },\\n )\\n fail(\\\"HTTP_PROD_RC\\\")\"},{\"line\":638,\"text\":\" input_bytes=HTTP_VERIFY_PY.encode(),\\n timeout=180,\\n )\\n line = next(\\n (x for x in out.splitlines() if x.startswith(\\\"PORTAL200_HTTP_JSON=\\\")),\\n None,\\n )\\n if not line:\\n fail(\\\"HTTP_MARKER_MISSING\\\")\\n try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n except Exception:\\n fail(\\\"HTTP_JSON\\\")\"},{\"line\":647,\"text\":\" try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n except Exception:\\n fail(\\\"HTTP_JSON\\\")\\n emit(\\\"PORTAL200_HTTP\\\", {\\\"target\\\": target, \\\"state\\\": state, **data})\\n return data\\n\\n\\ndef verify_new(target, baseline):\\n data = inspect(target)\\n if not app_contract(data, target):\\n fail(\\\"APP_CONTRACT_NEW\\\")\\n if not source_is_new(data):\"},{\"line\":715,\"text\":\" timeout=40,\\n )\\n if \\\"status: running\\\" not in p.stdout.decode():\\n fail(\\\"VM211_NOT_RUNNING\\\")\\n emit(\\\"PORTAL200_CLUSTER\\\", \\\"PASS\\\")\\n\\n\\ndef result_contract(status, changes, rollback_started, rollback_restored):\\n emit(\\n \\\"HOMELAB_RESULT_CONTRACT\\\",\\n {\\n \\\"version\\\": 1,\\n \\\"command_id\\\": CID,\"},{\"line\":734,\"text\":\"\\n\\nemit(\\\"NEWFI200_BEGIN\\\")\\nemit(\\\"COMMAND_ID\\\", CID)\\nemit(\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\\\", 22)\\nemit(\\n \\\"PORTAL200_FLOW\\\",\\n \\\"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\\\",\\n)\\n\\nstaging_backup = False\\nstaging_mutated = False\\nstaging_final_new = False\"},{\"line\":736,\"text\":\"emit(\\\"NEWFI200_BEGIN\\\")\\nemit(\\\"COMMAND_ID\\\", CID)\\nemit(\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\\\", 22)\\nemit(\\n \\\"PORTAL200_FLOW\\\",\\n \\\"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\\\",\\n)\\n\\nstaging_backup = False\\nstaging_mutated = False\\nstaging_final_new = False\\nprod_backup = False\\nprod_mutated = False\"},{\"line\":737,\"text\":\"emit(\\\"COMMAND_ID\\\", CID)\\nemit(\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\\\", 22)\\nemit(\\n \\\"PORTAL200_FLOW\\\",\\n \\\"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\\\",\\n)\\n\\nstaging_backup = False\\nstaging_mutated = False\\nstaging_final_new = False\\nprod_backup = False\\nprod_mutated = False\\ngit_files_changed = False\"},{\"line\":758,\"text\":\"s_old_templates = None\\np_old_templates = None\\n\\ntry:\\n compile(HTTP_VERIFY_PY, \\\"\\\", \\\"exec\\\")\\n for rel, (size, sha) in EXPECTED_NEW.items():\\n p = CAND_ROOT / rel\\n if not p.is_file() or file_meta(p) != (size, sha):\\n fail(\\\"CANDIDATE_IDENTITY\\\")\\n tree = ET.parse(CAND_ROOT / \\\"_data/templates.xml\\\")\\n titles = sorted(x.attrib.get(\\\"title\\\") for x in tree.getroot().findall(\\\"template\\\"))\\n if titles != [\\\"newfi_portal\\\", \\\"newfi_portal.less\\\"]:\\n fail(\\\"CANDIDATE_TEMPLATE_SET\\\")\"},{\"line\":760,\"text\":\"\\ntry:\\n compile(HTTP_VERIFY_PY, \\\"\\\", \\\"exec\\\")\\n for rel, (size, sha) in EXPECTED_NEW.items():\\n p = CAND_ROOT / rel\\n if not p.is_file() or file_meta(p) != (size, sha):\\n fail(\\\"CANDIDATE_IDENTITY\\\")\\n tree = ET.parse(CAND_ROOT / \\\"_data/templates.xml\\\")\\n titles = sorted(x.attrib.get(\\\"title\\\") for x in tree.getroot().findall(\\\"template\\\"))\\n if titles != [\\\"newfi_portal\\\", \\\"newfi_portal.less\\\"]:\\n fail(\\\"CANDIDATE_TEMPLATE_SET\\\")\\n service = (CAND_ROOT / \\\"Service/PortalData.php\\\").read_text(\\\"utf-8\\\")\\n if any(\"},{\"line\":762,\"text\":\" compile(HTTP_VERIFY_PY, \\\"\\\", \\\"exec\\\")\\n for rel, (size, sha) in EXPECTED_NEW.items():\\n p = CAND_ROOT / rel\\n if not p.is_file() or file_meta(p) != (size, sha):\\n fail(\\\"CANDIDATE_IDENTITY\\\")\\n tree = ET.parse(CAND_ROOT / \\\"_data/templates.xml\\\")\\n titles = sorted(x.attrib.get(\\\"title\\\") for x in tree.getroot().findall(\\\"template\\\"))\\n if titles != [\\\"newfi_portal\\\", \\\"newfi_portal.less\\\"]:\\n fail(\\\"CANDIDATE_TEMPLATE_SET\\\")\\n service = (CAND_ROOT / \\\"Service/PortalData.php\\\").read_text(\\\"utf-8\\\")\\n if any(\\n token in service\\n for token in [\"},{\"line\":763,\"text\":\" for rel, (size, sha) in EXPECTED_NEW.items():\\n p = CAND_ROOT / rel\\n if not p.is_file() or file_meta(p) != (size, sha):\\n fail(\\\"CANDIDATE_IDENTITY\\\")\\n tree = ET.parse(CAND_ROOT / \\\"_data/templates.xml\\\")\\n titles = sorted(x.attrib.get(\\\"title\\\") for x in tree.getroot().findall(\\\"template\\\"))\\n if titles != [\\\"newfi_portal\\\", \\\"newfi_portal.less\\\"]:\\n fail(\\\"CANDIDATE_TEMPLATE_SET\\\")\\n service = (CAND_ROOT / \\\"Service/PortalData.php\\\").read_text(\\\"utf-8\\\")\\n if any(\\n token in service\\n for token in [\\n \\\"\\\\\\\\XF::db(\\\",\"},{\"line\":766,\"text\":\" fail(\\\"CANDIDATE_IDENTITY\\\")\\n tree = ET.parse(CAND_ROOT / \\\"_data/templates.xml\\\")\\n titles = sorted(x.attrib.get(\\\"title\\\") for x in tree.getroot().findall(\\\"template\\\"))\\n if titles != [\\\"newfi_portal\\\", \\\"newfi_portal.less\\\"]:\\n fail(\\\"CANDIDATE_TEMPLATE_SET\\\")\\n service = (CAND_ROOT / \\\"Service/PortalData.php\\\").read_text(\\\"utf-8\\\")\\n if any(\\n token in service\\n for token in [\\n \\\"\\\\\\\\XF::db(\\\",\\n \\\"->save(\\\",\\n \\\"->delete(\\\",\\n \\\"INSERT \\\",\"},{\"line\":767,\"text\":\" tree = ET.parse(CAND_ROOT / \\\"_data/templates.xml\\\")\\n titles = sorted(x.attrib.get(\\\"title\\\") for x in tree.getroot().findall(\\\"template\\\"))\\n if titles != [\\\"newfi_portal\\\", \\\"newfi_portal.less\\\"]:\\n fail(\\\"CANDIDATE_TEMPLATE_SET\\\")\\n service = (CAND_ROOT / \\\"Service/PortalData.php\\\").read_text(\\\"utf-8\\\")\\n if any(\\n token in service\\n for token in [\\n \\\"\\\\\\\\XF::db(\\\",\\n \\\"->save(\\\",\\n \\\"->delete(\\\",\\n \\\"INSERT \\\",\\n \\\"UPDATE \\\",\"},{\"line\":780,\"text\":\" \\\"DELETE \\\",\\n \\\"REPLACE \\\",\\n ]\\n ):\\n fail(\\\"CANDIDATE_MUTATION_SCAN\\\")\\n emit(\\\"PORTAL200_CANDIDATE_IDENTITY\\\", \\\"PASS\\\")\\n emit(\\n \\\"PORTAL200_ORCHESTRATION_FIXES\\\",\\n {\\n \\\"staging_503_without_css_links\\\": True,\\n \\\"failure_status\\\": \\\"FAIL\\\",\\n \\\"prod_http_via_pve01_public_route\\\": True,\\n \\\"css_href_regex_fixed\\\": True,\"},{\"line\":781,\"text\":\" \\\"REPLACE \\\",\\n ]\\n ):\\n fail(\\\"CANDIDATE_MUTATION_SCAN\\\")\\n emit(\\\"PORTAL200_CANDIDATE_IDENTITY\\\", \\\"PASS\\\")\\n emit(\\n \\\"PORTAL200_ORCHESTRATION_FIXES\\\",\\n {\\n \\\"staging_503_without_css_links\\\": True,\\n \\\"failure_status\\\": \\\"FAIL\\\",\\n \\\"prod_http_via_pve01_public_route\\\": True,\\n \\\"css_href_regex_fixed\\\": True,\\n },\"},{\"line\":783,\"text\":\" ):\\n fail(\\\"CANDIDATE_MUTATION_SCAN\\\")\\n emit(\\\"PORTAL200_CANDIDATE_IDENTITY\\\", \\\"PASS\\\")\\n emit(\\n \\\"PORTAL200_ORCHESTRATION_FIXES\\\",\\n {\\n \\\"staging_503_without_css_links\\\": True,\\n \\\"failure_status\\\": \\\"FAIL\\\",\\n \\\"prod_http_via_pve01_public_route\\\": True,\\n \\\"css_href_regex_fixed\\\": True,\\n },\\n )\\n\"},{\"line\":800,\"text\":\" remote = remote_out.split()[0] if remote_out else \\\"\\\"\\n if branch != BRANCH or head != OLD_HEAD or remote != OLD_HEAD or status:\\n fail(\\\"GIT_PRESTATE\\\")\\n emit(\\n \\\"PORTAL200_GIT_PRESTATE\\\",\\n {\\\"branch\\\": branch, \\\"head\\\": head, \\\"remote\\\": remote, \\\"clean\\\": True},\\n )\\n cluster_guard()\\n\\n lint_php(\\\"staging\\\", INSPECT_PHP, \\\"INSPECT_STAGING\\\")\\n lint_php(\\\"staging\\\", IMPORT_PHP, \\\"IMPORT_STAGING\\\")\\n lint_php(\\\"staging\\\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", \\\"new\\\"), \\\"RUNTIME_STAGING\\\")\\n lint_php(\\\"prod\\\", INSPECT_PHP, \\\"INSPECT_PROD\\\")\"},{\"line\":811,\"text\":\" lint_php(\\\"staging\\\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", \\\"new\\\"), \\\"RUNTIME_STAGING\\\")\\n lint_php(\\\"prod\\\", INSPECT_PHP, \\\"INSPECT_PROD\\\")\\n lint_php(\\\"prod\\\", IMPORT_PHP, \\\"IMPORT_PROD\\\")\\n lint_php(\\\"prod\\\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", \\\"new\\\"), \\\"RUNTIME_PROD\\\")\\n emit(\\\"PORTAL200_RUNTIME_HELPER_LINT\\\", \\\"PASS\\\")\\n\\n s0 = inspect(\\\"staging\\\")\\n p0 = inspect(\\\"prod\\\")\\n if not app_contract(s0, \\\"staging\\\") or not app_contract(p0, \\\"prod\\\"):\\n fail(\\\"APP_PRESTATE\\\")\\n if not source_is_old(s0) or not source_is_old(p0):\\n fail(\\\"SOURCE_PRESTATE\\\")\\n if s0.get(\\\"templates\\\") != p0.get(\\\"templates\\\"):\"},{\"line\":825,\"text\":\" s_error = int(s0[\\\"application\\\"][\\\"error_max_id\\\"])\\n p_error = int(p0[\\\"application\\\"][\\\"error_max_id\\\"])\\n s_old_templates = s0[\\\"templates\\\"]\\n p_old_templates = p0[\\\"templates\\\"]\\n emit(\\\"PORTAL200_ERROR_BASELINE\\\", {\\\"staging\\\": s_error, \\\"prod\\\": p_error})\\n http_verify(\\\"staging\\\", \\\"old\\\")\\n runtime_verify(\\\"prod\\\", \\\"old\\\")\\n http_verify(\\\"prod\\\", \\\"old\\\")\\n assert_no_new_errors(\\\"staging\\\", s_error, \\\"preflight\\\")\\n assert_no_new_errors(\\\"prod\\\", p_error, \\\"preflight\\\")\\n emit(\\\"PORTAL200_PREFLIGHT\\\", \\\"PASS\\\")\\n\\n guest_backup(\\\"staging\\\")\"},{\"line\":831,\"text\":\" runtime_verify(\\\"prod\\\", \\\"old\\\")\\n http_verify(\\\"prod\\\", \\\"old\\\")\\n assert_no_new_errors(\\\"staging\\\", s_error, \\\"preflight\\\")\\n assert_no_new_errors(\\\"prod\\\", p_error, \\\"preflight\\\")\\n emit(\\\"PORTAL200_PREFLIGHT\\\", \\\"PASS\\\")\\n\\n guest_backup(\\\"staging\\\")\\n staging_backup = True\\n staging_mutated = True\\n install_candidate(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_new(\\\"staging\\\", s_error)\\n emit(\\\"PORTAL200_STAGING_FIRST_APPLY\\\", \\\"PASS\\\")\"},{\"line\":836,\"text\":\"\\n guest_backup(\\\"staging\\\")\\n staging_backup = True\\n staging_mutated = True\\n install_candidate(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_new(\\\"staging\\\", s_error)\\n emit(\\\"PORTAL200_STAGING_FIRST_APPLY\\\", \\\"PASS\\\")\\n\\n restore_old(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_old(\\\"staging\\\", s_error, s_old_templates)\\n staging_mutated = False\"},{\"line\":839,\"text\":\" staging_mutated = True\\n install_candidate(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_new(\\\"staging\\\", s_error)\\n emit(\\\"PORTAL200_STAGING_FIRST_APPLY\\\", \\\"PASS\\\")\\n\\n restore_old(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_old(\\\"staging\\\", s_error, s_old_templates)\\n staging_mutated = False\\n emit(\\\"PORTAL200_STAGING_ROLLBACK_PROOF\\\", \\\"PASS\\\")\\n\\n staging_mutated = True\"},{\"line\":845,\"text\":\" restore_old(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_old(\\\"staging\\\", s_error, s_old_templates)\\n staging_mutated = False\\n emit(\\\"PORTAL200_STAGING_ROLLBACK_PROOF\\\", \\\"PASS\\\")\\n\\n staging_mutated = True\\n install_candidate(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_new(\\\"staging\\\", s_error)\\n staging_final_new = True\\n emit(\\\"PORTAL200_STAGING_FINAL\\\", \\\"PASS\\\")\\n\"},{\"line\":848,\"text\":\" staging_mutated = False\\n emit(\\\"PORTAL200_STAGING_ROLLBACK_PROOF\\\", \\\"PASS\\\")\\n\\n staging_mutated = True\\n install_candidate(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_new(\\\"staging\\\", s_error)\\n staging_final_new = True\\n emit(\\\"PORTAL200_STAGING_FINAL\\\", \\\"PASS\\\")\\n\\n for rel in EXPECTED_NEW:\\n dest = GIT_PORTAL / rel\\n old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None\"},{\"line\":852,\"text\":\" install_candidate(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_new(\\\"staging\\\", s_error)\\n staging_final_new = True\\n emit(\\\"PORTAL200_STAGING_FINAL\\\", \\\"PASS\\\")\\n\\n for rel in EXPECTED_NEW:\\n dest = GIT_PORTAL / rel\\n old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None\\n dest.parent.mkdir(parents=True, exist_ok=True)\\n shutil.copyfile(CAND_ROOT / rel, dest)\\n git_files_changed = True\\n\"},{\"line\":858,\"text\":\" for rel in EXPECTED_NEW:\\n dest = GIT_PORTAL / rel\\n old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None\\n dest.parent.mkdir(parents=True, exist_ok=True)\\n shutil.copyfile(CAND_ROOT / rel, dest)\\n git_files_changed = True\\n\\n if git(\\\"diff\\\", \\\"--check\\\"):\\n fail(\\\"GIT_DIFF_CHECK\\\")\\n status_lines = git(\\\"status\\\", \\\"--porcelain=v1\\\", \\\"--untracked-files=all\\\").splitlines()\\n actual_paths = sorted(line[3:] for line in status_lines if len(line) >= 4)\\n if actual_paths != sorted(EXPECTED_GIT_PATHS):\\n fail(\\\"GIT_SCOPE\\\")\"},{\"line\":885,\"text\":\" if remote_head != new_head:\\n fail(\\\"GIT_REMOTE_HEAD\\\")\\n push_confirmed = True\\n emit(\\n \\\"PORTAL200_GIT_PROMOTION\\\",\\n {\\n \\\"old_head\\\": OLD_HEAD,\\n \\\"new_head\\\": new_head,\\n \\\"remote_head\\\": remote_head,\\n \\\"result\\\": \\\"PASS\\\",\\n },\\n )\\n\"},{\"line\":902,\"text\":\" fail(\\\"PROD_ERROR_DRIFT\\\")\\n guest_backup(\\\"prod\\\")\\n prod_backup = True\\n prod_mutated = True\\n install_candidate(\\\"prod\\\")\\n import_addon(\\\"prod\\\")\\n verify_new(\\\"prod\\\", p_error)\\n emit(\\\"PORTAL200_PRODUCTION\\\", \\\"PASS\\\")\\n\\n sf = verify_new(\\\"staging\\\", s_error)\\n pf = verify_new(\\\"prod\\\", p_error)\\n if sf[\\\"source\\\"] != pf[\\\"source\\\"]:\\n fail(\\\"FINAL_SOURCE_PARITY\\\")\"},{\"line\":905,\"text\":\" prod_mutated = True\\n install_candidate(\\\"prod\\\")\\n import_addon(\\\"prod\\\")\\n verify_new(\\\"prod\\\", p_error)\\n emit(\\\"PORTAL200_PRODUCTION\\\", \\\"PASS\\\")\\n\\n sf = verify_new(\\\"staging\\\", s_error)\\n pf = verify_new(\\\"prod\\\", p_error)\\n if sf[\\\"source\\\"] != pf[\\\"source\\\"]:\\n fail(\\\"FINAL_SOURCE_PARITY\\\")\\n if sf[\\\"templates\\\"] != pf[\\\"templates\\\"]:\\n fail(\\\"FINAL_TEMPLATE_PARITY\\\")\\n if git(\\\"rev-parse\\\", \\\"HEAD\\\") != new_head:\"},{\"line\":922,\"text\":\" if git(\\\"status\\\", \\\"--porcelain=v1\\\", \\\"--untracked-files=all\\\"):\\n fail(\\\"FINAL_GIT_DIRTY\\\")\\n cluster_guard()\\n emit(\\n \\\"PORTAL200_FINAL_EVIDENCE\\\",\\n {\\n \\\"commit\\\": new_head,\\n \\\"source_files\\\": 3,\\n \\\"templates\\\": 2,\\n \\\"staging_error_baseline\\\": s_error,\\n \\\"prod_error_baseline\\\": p_error,\\n },\\n )\"},{\"line\":931,\"text\":\" \\\"staging_error_baseline\\\": s_error,\\n \\\"prod_error_baseline\\\": p_error,\\n },\\n )\\n emit(\\\"PORTAL200_PORTAL\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_STAGING\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_PRODUCTION\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\\\", 21)\\n result_contract(\\\"OK\\\", True, False, None)\\n emit(\\\"NEWFI200_END\\\")\\n raise SystemExit(0)\\n\\nexcept PortalError as exc:\"},{\"line\":932,\"text\":\" \\\"prod_error_baseline\\\": p_error,\\n },\\n )\\n emit(\\\"PORTAL200_PORTAL\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_STAGING\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_PRODUCTION\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\\\", 21)\\n result_contract(\\\"OK\\\", True, False, None)\\n emit(\\\"NEWFI200_END\\\")\\n raise SystemExit(0)\\n\\nexcept PortalError as exc:\\n code = str(exc)\"},{\"line\":933,\"text\":\" },\\n )\\n emit(\\\"PORTAL200_PORTAL\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_STAGING\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_PRODUCTION\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\\\", 21)\\n result_contract(\\\"OK\\\", True, False, None)\\n emit(\\\"NEWFI200_END\\\")\\n raise SystemExit(0)\\n\\nexcept PortalError as exc:\\n code = str(exc)\\n emit(\\\"PORTAL200_FAILURE\\\", code)\"},{\"line\":934,\"text\":\" )\\n emit(\\\"PORTAL200_PORTAL\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_STAGING\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_PRODUCTION\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\\\", 21)\\n result_contract(\\\"OK\\\", True, False, None)\\n emit(\\\"NEWFI200_END\\\")\\n raise SystemExit(0)\\n\\nexcept PortalError as exc:\\n code = str(exc)\\n emit(\\\"PORTAL200_FAILURE\\\", code)\\n\"},{\"line\":941,\"text\":\" raise SystemExit(0)\\n\\nexcept PortalError as exc:\\n code = str(exc)\\n emit(\\\"PORTAL200_FAILURE\\\", code)\\n\\n if push_confirmed:\\n if prod_mutated and prod_backup:\\n failure_rollback_started = True\\n try:\\n restore_old(\\\"prod\\\")\\n import_addon(\\\"prod\\\")\\n verify_old(\\\"prod\\\", p_error, p_old_templates, strict_errors=False)\"},{\"line\":952,\"text\":\" import_addon(\\\"prod\\\")\\n verify_old(\\\"prod\\\", p_error, p_old_templates, strict_errors=False)\\n prod_mutated = False\\n failure_rollback_restored = False\\n emit(\\\"PORTAL200_FAILURE_PROD_ROLLBACK\\\", \\\"PASS_PARTIAL_OVERALL_STATE_CHANGED\\\")\\n except Exception:\\n failure_rollback_restored = False\\n emit(\\\"PORTAL200_FAILURE_PROD_ROLLBACK\\\", \\\"FAIL\\\")\\n else:\\n if staging_mutated and staging_backup:\\n failure_rollback_started = True\\n try:\\n restore_old(\\\"staging\\\")\"},{\"line\":955,\"text\":\" failure_rollback_restored = False\\n emit(\\\"PORTAL200_FAILURE_PROD_ROLLBACK\\\", \\\"PASS_PARTIAL_OVERALL_STATE_CHANGED\\\")\\n except Exception:\\n failure_rollback_restored = False\\n emit(\\\"PORTAL200_FAILURE_PROD_ROLLBACK\\\", \\\"FAIL\\\")\\n else:\\n if staging_mutated and staging_backup:\\n failure_rollback_started = True\\n try:\\n restore_old(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_old(\\\"staging\\\", s_error, s_old_templates, strict_errors=False)\\n staging_mutated = False\"},{\"line\":966,\"text\":\" verify_old(\\\"staging\\\", s_error, s_old_templates, strict_errors=False)\\n staging_mutated = False\\n staging_final_new = False\\n failure_rollback_restored = True\\n emit(\\\"PORTAL200_FAILURE_STAGING_ROLLBACK\\\", \\\"PASS\\\")\\n except Exception:\\n failure_rollback_restored = False\\n emit(\\\"PORTAL200_FAILURE_STAGING_ROLLBACK\\\", \\\"FAIL\\\")\\n\\n if git_files_changed and not commit_created:\\n try:\\n for rel, old in old_git_bytes.items():\\n dest = GIT_PORTAL / rel\"},{\"line\":969,\"text\":\" failure_rollback_restored = True\\n emit(\\\"PORTAL200_FAILURE_STAGING_ROLLBACK\\\", \\\"PASS\\\")\\n except Exception:\\n failure_rollback_restored = False\\n emit(\\\"PORTAL200_FAILURE_STAGING_ROLLBACK\\\", \\\"FAIL\\\")\\n\\n if git_files_changed and not commit_created:\\n try:\\n for rel, old in old_git_bytes.items():\\n dest = GIT_PORTAL / rel\\n if old is None:\\n if dest.exists():\\n dest.unlink()\"},{\"line\":979,\"text\":\" if old is None:\\n if dest.exists():\\n dest.unlink()\\n else:\\n dest.write_bytes(old)\\n service_dir = GIT_PORTAL / \\\"Service\\\"\\n try:\\n service_dir.rmdir()\\n except OSError:\\n pass\\n emit(\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\", \\\"PASS\\\")\\n except Exception:\\n emit(\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\", \\\"FAIL\\\")\"},{\"line\":985,\"text\":\" try:\\n service_dir.rmdir()\\n except OSError:\\n pass\\n emit(\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\", \\\"PASS\\\")\\n except Exception:\\n emit(\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\", \\\"FAIL\\\")\\n\\n if commit_created:\\n emit(\\n \\\"PORTAL200_FAILURE_GIT_HISTORY\\\",\\n \\\"COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED\\\",\\n )\"},{\"line\":987,\"text\":\" except OSError:\\n pass\\n emit(\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\", \\\"PASS\\\")\\n except Exception:\\n emit(\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\", \\\"FAIL\\\")\\n\\n if commit_created:\\n emit(\\n \\\"PORTAL200_FAILURE_GIT_HISTORY\\\",\\n \\\"COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED\\\",\\n )\\n\\n persisted = bool(push_confirmed or commit_created or staging_final_new or prod_mutated)\"},{\"line\":991,\"text\":\" emit(\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\", \\\"FAIL\\\")\\n\\n if commit_created:\\n emit(\\n \\\"PORTAL200_FAILURE_GIT_HISTORY\\\",\\n \\\"COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED\\\",\\n )\\n\\n persisted = bool(push_confirmed or commit_created or staging_final_new or prod_mutated)\\n if failure_rollback_started and failure_rollback_restored is None:\\n failure_rollback_restored = False\\n result_contract(\\n \\\"FAIL\\\",\"},{\"line\":1004,\"text\":\" persisted,\\n failure_rollback_started,\\n failure_rollback_restored,\\n )\\n emit(\\\"PORTAL200_ROADMAP_REMAINS\\\", 22)\\n emit(\\\"NEWFI200_END\\\")\\n raise SystemExit(1)\\n\\nexcept Exception:\\n emit(\\\"PORTAL200_FAILURE\\\", \\\"UNEXPECTED_EXCEPTION\\\")\\n result_contract(\\n \\\"FAIL\\\",\\n bool(push_confirmed or commit_created or staging_final_new or prod_mutated),\"},{\"line\":1009,\"text\":\" emit(\\\"NEWFI200_END\\\")\\n raise SystemExit(1)\\n\\nexcept Exception:\\n emit(\\\"PORTAL200_FAILURE\\\", \\\"UNEXPECTED_EXCEPTION\\\")\\n result_contract(\\n \\\"FAIL\\\",\\n bool(push_confirmed or commit_created or staging_final_new or prod_mutated),\\n False,\\n None,\\n )\\n emit(\\\"PORTAL200_ROADMAP_REMAINS\\\", 22)\\n emit(\\\"NEWFI200_END\\\")\"},{\"line\":1016,\"text\":\" bool(push_confirmed or commit_created or staging_final_new or prod_mutated),\\n False,\\n None,\\n )\\n emit(\\\"PORTAL200_ROADMAP_REMAINS\\\", 22)\\n emit(\\\"NEWFI200_END\\\")\\n raise SystemExit(1)\"}]\nRELEVANT_FUNCTIONS=[{\"lineno\":400,\"name\":\"parse_inspect\",\"sha256\":\"7b72b62a0ff4c617b7175627d8630294160a6c129f3acfb0d109b4c72c2e8fe2\",\"source\":\"def parse_inspect(text):\\n b = \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\"\\n e = \\\"\\\\nPORTAL200_INSPECT_JSON_END\\\"\\n if b not in text or e not in text:\\n fail(\\\"INSPECT_MARKERS\\\")\\n try:\\n return json.loads(text.split(b, 1)[1].split(e, 1)[0])\\n except Exception:\\n fail(\\\"INSPECT_JSON\\\")\"},{\"lineno\":447,\"name\":\"app_contract\",\"sha256\":\"a3a06560de8f2db42e0c3bafc27ad29ee985fd5bf13b01c527a283731ed7904c\",\"source\":\"def app_contract(data, target):\\n a = data.get(\\\"application\\\", {})\\n addon = data.get(\\\"addon\\\") or {}\\n expected = {\\n \\\"staging\\\": (\\\"0\\\", \\\"http://newfi-staging.gram1.ru\\\"),\\n \\\"prod\\\": (\\\"1\\\", \\\"https://newfi.ru\\\"),\\n }[target]\\n return (\\n a.get(\\\"boardActive\\\") == expected[0]\\n and a.get(\\\"boardUrl\\\") == expected[1]\\n and a.get(\\\"indexRoute\\\") == \\\"portal/\\\"\\n and a.get(\\\"defaultStyleId\\\") == \\\"4\\\"\\n and addon.get(\\\"addon_id\\\") == \\\"Newfi/Portal\\\"\\n and int(addon.get(\\\"version_id\\\", 0)) == 1000010\\n and addon.get(\\\"version_string\\\") == \\\"1.0.0 Alpha 1\\\"\\n and int(addon.get(\\\"active\\\", 0)) == 1\\n and int(addon.get(\\\"is_processing\\\", 1)) == 0\\n )\"},{\"lineno\":480,\"name\":\"assert_no_new_errors\",\"sha256\":\"801839c1705804fed1a507709263d2882a332f0276b42a7c5439204b99428dfb\",\"source\":\"def assert_no_new_errors(target, baseline, phase):\\n rows = new_errors(target, baseline)\\n if rows:\\n emit(\\n \\\"PORTAL200_NEW_XF_ERRORS\\\",\\n {\\n \\\"target\\\": target,\\n \\\"phase\\\": phase,\\n \\\"count\\\": len(rows),\\n \\\"ids\\\": [r.get(\\\"error_id\\\") for r in rows],\\n },\\n )\\n fail(\\\"NEW_XF_ERRORS\\\")\"},{\"lineno\":495,\"name\":\"guest_backup\",\"sha256\":\"3e0d88922bf3d3420512f69cd0e50232e0325391cebb99845ed1c8c206ec08a1\",\"source\":\"def guest_backup(target):\\n script = f'''set -eu\\nB={shlex.quote(BACKUP)}\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest ! -e \\\"$B\\\"\\ninstall -d -m 700 \\\"$B/Pub/Controller\\\" \\\"$B/_data\\\"\\ncp -p -- \\\"$P/Pub/Controller/Portal.php\\\" \\\"$B/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$P/_data/templates.xml\\\" \\\"$B/_data/templates.xml\\\"\\ntest ! -e \\\"$P/Service/PortalData.php\\\"\\nprintf 'PORTAL200_BACKUP=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_BACKUP=PASS\\\" not in out:\\n fail(\\\"BACKUP_FAILED\\\")\"},{\"lineno\":511,\"name\":\"guest_write\",\"sha256\":\"f5f8ed7e6553eadf19e219ed9cab5dd6d7b9ad72dca3926e5368ad663f9b2881\",\"source\":\"def guest_write(target, rel, data):\\n dest = f\\\"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}\\\"\\n parent = str(pathlib.PurePosixPath(dest).parent)\\n script = (\\n \\\"set -eu; umask 022; \\\"\\n + \\\"install -d -m 755 \\\"\\n + shlex.quote(parent)\\n + \\\"; T=$(mktemp \\\"\\n + shlex.quote(parent + \\\"/.portal200.XXXXXX\\\")\\n + \\\"); cat >\\\\\\\"$T\\\\\\\"; chmod 0644 \\\\\\\"$T\\\\\\\"; chown root:root \\\\\\\"$T\\\\\\\"; \\\"\\n + \\\"mv -f -- \\\\\\\"$T\\\\\\\" \\\"\\n + shlex.quote(dest)\\n + \\\"; printf 'PORTAL200_WRITE=PASS\\\\\\\\n'\\\"\\n )\\n out = qga(\\n target,\\n [\\\"/bin/bash\\\", \\\"-lc\\\", script],\\n input_bytes=data,\\n timeout=90,\\n )\\n if \\\"PORTAL200_WRITE=PASS\\\" not in out:\\n fail(\\\"WRITE_FAILED\\\")\"},{\"lineno\":535,\"name\":\"install_candidate\",\"sha256\":\"b46ecffec3c0972b1306ffb057164c1223e08f26bd3121bfde7f624ffad66903\",\"source\":\"def install_candidate(target):\\n for rel in [\\n \\\"Pub/Controller/Portal.php\\\",\\n \\\"Service/PortalData.php\\\",\\n \\\"_data/templates.xml\\\",\\n ]:\\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\"},{\"lineno\":544,\"name\":\"restore_old\",\"sha256\":\"eafa8284ff101e7e3c189922361201ce5684ce970e90e2c933c39b65e25e21da\",\"source\":\"def restore_old(target):\\n script = f'''set -eu\\nB={shlex.quote(BACKUP)}\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest -f \\\"$B/Pub/Controller/Portal.php\\\"\\ntest -f \\\"$B/_data/templates.xml\\\"\\ncp -p -- \\\"$B/Pub/Controller/Portal.php\\\" \\\"$P/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$B/_data/templates.xml\\\" \\\"$P/_data/templates.xml\\\"\\nrm -f -- \\\"$P/Service/PortalData.php\\\"\\nrmdir -- \\\"$P/Service\\\" 2>/dev/null || true\\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_SOURCE_RESTORE=PASS\\\" not in out:\\n fail(\\\"SOURCE_RESTORE_FAILED\\\")\"},{\"lineno\":561,\"name\":\"import_addon\",\"sha256\":\"8af6f854e1917ee8e02cfe345f730da0b4b6fbe1b44fdb73ced52c8a3eeebb50\",\"source\":\"def import_addon(target):\\n out = qga(\\n target,\\n [\\\"/usr/bin/php8.3\\\"],\\n input_bytes=IMPORT_PHP.encode(),\\n timeout=240,\\n )\\n if \\\"PORTAL200_IMPORT_JSON=\\\" not in out:\\n fail(\\\"IMPORT_FAILED\\\")\"},{\"lineno\":572,\"name\":\"runtime_verify\",\"sha256\":\"cbe3a884afd8bb310ce9d055ddaf74a9923ac55e975ad5a9c3fbb0b910776497\",\"source\":\"def runtime_verify(target, state):\\n src = RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", state)\\n out = qga(target, [\\\"/usr/bin/php8.3\\\"], input_bytes=src.encode(), timeout=120)\\n line = next(\\n (x for x in out.splitlines() if x.startswith(\\\"PORTAL200_RUNTIME_JSON=\\\")),\\n None,\\n )\\n if not line:\\n fail(\\\"RUNTIME_MARKER_MISSING\\\")\\n try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n except Exception:\\n fail(\\\"RUNTIME_JSON\\\")\\n emit(\\\"PORTAL200_RUNTIME\\\", {\\\"target\\\": target, **data})\\n return data\"},{\"lineno\":589,\"name\":\"http_verify\",\"sha256\":\"75eb5fbec1862b5d926cbc91519c284218a4bb05db9edf616422fa187b8348a6\",\"source\":\"def http_verify(target, state):\\n argv = [\\\"/usr/bin/python3\\\", \\\"-\\\", target, state]\\n if target == \\\"prod\\\":\\n try:\\n p = subprocess.run(\\n argv,\\n input=HTTP_VERIFY_PY.encode(),\\n stdout=subprocess.PIPE,\\n stderr=subprocess.PIPE,\\n timeout=180,\\n check=False,\\n )\\n except subprocess.TimeoutExpired:\\n fail(\\\"HTTP_PROD_TIMEOUT\\\")\\n out = p.stdout.decode(\\\"utf-8\\\", \\\"replace\\\")\\n err = p.stderr.decode(\\\"utf-8\\\", \\\"replace\\\")\\n line = next(\\n (x for x in out.splitlines() if x.startswith(\\\"PORTAL200_HTTP_JSON=\\\")),\\n None,\\n )\\n data = None\\n if line:\\n try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n emit(\\\"PORTAL200_HTTP\\\", {\\\"target\\\": target, \\\"state\\\": state, **data})\\n except Exception:\\n fail(\\\"HTTP_JSON\\\")\\n if p.returncode != 0:\\n emit(\\n \\\"PORTAL200_HTTP_EXEC\\\",\\n {\\n \\\"target\\\": target,\\n \\\"state\\\": state,\\n \\\"rc\\\": p.returncode,\\n \\\"stderr\\\": err[:800],\\n },\\n )\\n fail(\\\"HTTP_PROD_RC\\\")\\n if data is None:\\n fail(\\\"HTTP_MARKER_MISSING\\\")\\n return data\\n\\n out = qga(\\n target,\\n argv,\\n input_bytes=HTTP_VERIFY_PY.encode(),\\n timeout=180,\\n )\\n line = next(\\n (x for x in out.splitlines() if x.startswith(\\\"PORTAL200_HTTP_JSON=\\\")),\\n None,\\n )\\n if not line:\\n fail(\\\"HTTP_MARKER_MISSING\\\")\\n try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n except Exception:\\n fail(\\\"HTTP_JSON\\\")\\n emit(\\\"PORTAL200_HTTP\\\", {\\\"target\\\": target, \\\"state\\\": state, **data})\\n return data\"},{\"lineno\":692,\"name\":\"cluster_guard\",\"sha256\":\"bd6547918004a2f74ae4ac8cc7d7caf80b4ced2bcf57cc98ddc454561121dcac\",\"source\":\"def cluster_guard():\\n p = run([\\\"/usr/sbin/qm\\\", \\\"status\\\", \\\"210\\\"], timeout=30)\\n if \\\"status: running\\\" not in p.stdout.decode():\\n fail(\\\"VM210_NOT_RUNNING\\\")\\n p = run(\\n [\\n \\\"/usr/bin/ssh\\\",\\n \\\"-T\\\",\\n \\\"-o\\\",\\n \\\"BatchMode=yes\\\",\\n \\\"-o\\\",\\n \\\"StrictHostKeyChecking=yes\\\",\\n \\\"-o\\\",\\n \\\"UpdateHostKeys=no\\\",\\n \\\"-o\\\",\\n \\\"ConnectTimeout=10\\\",\\n \\\"root@pve03\\\",\\n \\\"/usr/sbin/qm status 211\\\",\\n ],\\n timeout=40,\\n )\\n if \\\"status: running\\\" not in p.stdout.decode():\\n fail(\\\"VM211_NOT_RUNNING\\\")\\n emit(\\\"PORTAL200_CLUSTER\\\", \\\"PASS\\\")\"}]\nLITERAL_PAYLOADS=[{\"bytes\":2008,\"lineno\":147,\"names\":[\"INSPECT_PHP\"],\"sha256\":\"8de3d66a7adfca4c179db55bb5c4e956f6750fb41e9e058b449efeb214cbd7f1\",\"value\":\"db();\\nfunction srcMeta(string $path): ?array {\\n if (!is_file($path)) { return null; }\\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\\n}\\n$portal = $root . '/src/addons/Newfi/Portal';\\n$templates = $db->fetchAll(\\n \\\"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \\\" .\\n \\\"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \\\" .\\n \\\"LOCATE('--newfi-portal200', template) > 0 AS css_marker \\\" .\\n \\\"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\\\"\\n);\\n$result = [\\n 'hostname' => gethostname(),\\n 'application' => [\\n 'boardActive' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\\\"),\\n 'boardUrl' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\\\"),\\n 'indexRoute' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\\\"),\\n 'defaultStyleId' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\\\"),\\n 'error_count' => (int)$db->fetchOne(\\\"SELECT COUNT(*) FROM xf_error_log\\\"),\\n 'error_max_id' => (int)$db->fetchOne(\\\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\\\")\\n ],\\n 'addon' => $db->fetchRow(\\n \\\"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \\\" .\\n \\\"FROM xf_addon WHERE addon_id='Newfi/Portal'\\\"\\n ),\\n 'source' => [\\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\\n ],\\n 'templates' => $templates\\n];\\necho \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\";\\necho json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\\necho \\\"\\\\nPORTAL200_INSPECT_JSON_END\\\\n\\\";\\n?>\"},{\"bytes\":1429,\"lineno\":194,\"names\":[\"IMPORT_PHP\"],\"sha256\":\"8f4109ed896f29035cfa5534751507efff2786d4d6d18840adf30b84771a18e8\",\"value\":\"addOnManager()->getById('Newfi/Portal');\\nif (!$addOn || !$addOn->isInstalled()) {\\n fwrite(STDERR, \\\"ADDON_NOT_INSTALLED\\\\n\\\");\\n exit(30);\\n}\\n$addOn->preRebuild();\\n$dataManager = $app->addOnDataManager();\\n$imported = [];\\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\\n $handler = $dataManager->getDataTypeHandler($dataType);\\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\\n if (!$xml || !$xml->count()) {\\n $handler->deleteAddOnData($addOn->addon_id);\\n continue;\\n }\\n $start = 0;\\n $guard = 0;\\n while (true) {\\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\\n if (!$last) { break; }\\n if ($last <= $start || ++$guard > 100000) {\\n fwrite(STDERR, \\\"IMPORT_PROGRESS_INVALID\\\\n\\\");\\n exit(31);\\n }\\n $start = $last;\\n }\\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\\n \\\\XF::triggerRunOnce();\\n $imported[] = $handler->getContainerTag();\\n}\\n$addOn->postDataImport();\\n$repo = $app->repository(\\\\XF\\\\Repository\\\\StyleRepository::class);\\n$repo->updateAllStylesLastModifiedDateLater();\\n$addOn->postRebuild();\\necho \\\"PORTAL200_IMPORT_JSON=\\\" . json_encode(\\n ['result'=>'PASS','containers'=>$imported],\\n JSON_UNESCAPED_SLASHES\\n) . \\\"\\\\n\\\";\\n?>\"},{\"bytes\":410,\"lineno\":240,\"names\":[\"ERRORS_PHP_TEMPLATE\"],\"sha256\":\"ee412cafa99d7d809c8b682db8ebb0bc2958ab0cbc9e37537e22a78f10489635\",\"value\":\"db();\\n$b=%d;\\n$rows=$db->fetchAll(\\n \\\"SELECT error_id,exception_type,message,filename,line FROM xf_error_log WHERE error_id>? ORDER BY error_id\\\",\\n [$b]\\n);\\necho json_encode(\\n $rows,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\\n?>\"},{\"bytes\":2501,\"lineno\":258,\"names\":[\"RUNTIME_VERIFY_PHP_TEMPLATE\"],\"sha256\":\"884c03648203992cf6d5b96be8342243739b0412fdff55f4a6b64cf96aad3313\",\"value\":\"$state];\\nif ($state === 'new') {\\n $service = new \\\\Newfi\\\\Portal\\\\Service\\\\PortalData($app);\\n $portal = $service->build();\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\\n $result['sections'] = [];\\n foreach (['forums','resources','media','events','dogs'] as $key) {\\n $section = $portal[$key] ?? null;\\n if (!is_array($section)) { fwrite(STDERR, \\\"SECTION_MISSING\\\\n\\\"); exit(60); }\\n $result['sections'][$key] = [\\n 'available'=>(bool)($section['available'] ?? false),\\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\\n 'status'=>(string)($section['status'] ?? '')\\n ];\\n }\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\\\"1\\\"') !== false;\\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\\\"resources\\\"') !== false;\\n $result['locked_media'] = strpos($html, 'data-portal-locked=\\\"media\\\"') !== false;\\n $result['html_bytes'] = strlen($html);\\n if (!$result['html_marker']) { fwrite(STDERR, \\\"HTML_MARKER_MISSING\\\\n\\\"); exit(61); }\\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\\n fwrite(STDERR, \\\"GUEST_PERMISSION_CONTRACT_CHANGED\\\\n\\\"); exit(62);\\n }\\n if (!$result['locked_resources'] || !$result['locked_media']) {\\n fwrite(STDERR, \\\"LOCKED_COPY_MISSING\\\\n\\\"); exit(63);\\n }\\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\\n fwrite(STDERR, \\\"PUBLIC_SECTION_UNAVAILABLE\\\\n\\\"); exit(64);\\n }\\n} else if ($state === 'old') {\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\\n $result['html_bytes'] = strlen($html);\\n if ($result['html_marker'] || !$result['old_copy']) {\\n fwrite(STDERR, \\\"OLD_RENDER_CONTRACT\\\\n\\\"); exit(65);\\n }\\n} else {\\n exit(66);\\n}\\necho \\\"PORTAL200_RUNTIME_JSON=\\\" . json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n) . \\\"\\\\n\\\";\\n?>\"},{\"bytes\":3277,\"lineno\":311,\"names\":[\"HTTP_VERIFY_PY\"],\"sha256\":\"15b9839d95798833aef597b59787d301155d32cfb404b14ca7e0bf7d83a1a71e\",\"value\":\"import html,json,re,subprocess,sys,urllib.parse\\nmode=sys.argv[1]\\nstate=sys.argv[2]\\nif mode=='staging':\\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\\nelif mode=='prod':\\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\\nelse:\\n raise SystemExit(40)\\ndef fetch(url):\\n cmd=[\\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\\n ]\\n if mode=='staging':\\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\\n cmd += [\\n '-o','-','-w','\\\\n__PORTAL_STATUS__%{http_code}',url\\n ]\\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\\n if p.returncode!=0: raise SystemExit(41)\\n text=p.stdout.decode('utf-8','replace')\\n marker='\\\\n__PORTAL_STATUS__'\\n if marker not in text: raise SystemExit(42)\\n body,status=text.rsplit(marker,1)\\n return int(status.strip()),body\\nroot=f'{scheme}://{host}/'\\nstatus,body=fetch(root)\\nhrefs=[]\\nfor raw in re.findall(r\\\"href=[\\\\\\\"']([^\\\\\\\"']*css\\\\.php[^\\\\\\\"']*)[\\\\\\\"']\\\",body,re.I):\\n raw=html.unescape(raw)\\n hrefs.append(urllib.parse.urljoin(root,raw))\\nnormal_css=[]\\nnormal_status=[]\\nfor u in hrefs:\\n s,b=fetch(u)\\n normal_status.append(s)\\n normal_css.append(b)\\nnormal_css_text='\\\\n'.join(normal_css)\\nforced_css_marker=None\\nforced_css_status=None\\nif mode=='staging' and state=='new' and hrefs:\\n u=urllib.parse.urlsplit(hrefs[0])\\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\\n out=[]\\n found=False\\n for k,v in pairs:\\n if k=='css':\\n found=True\\n parts=[x for x in v.split(',') if x]\\n if 'public:newfi_portal.less' not in parts:\\n parts.append('public:newfi_portal.less')\\n v=','.join(parts)\\n out.append((k,v))\\n if found:\\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\\n forced_css_status,forced_body=fetch(forced)\\n forced_css_marker='--newfi-portal200' in forced_body\\nresult={\\n 'status':status,\\n 'bytes':len(body.encode()),\\n 'html_marker':'data-newfi-portal200=\\\"1\\\"' in body,\\n 'css_marker':'--newfi-portal200' in normal_css_text,\\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\\n 'css_count':len(hrefs),\\n 'css_status':normal_status,\\n 'forced_css_status':forced_css_status,\\n 'forced_css_marker':forced_css_marker,\\n}\\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\\nif status!=expected_status: raise SystemExit(45)\\nif mode=='prod':\\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\\nelif mode=='staging':\\n if any(x!=200 for x in normal_status): raise SystemExit(46)\\nelse:\\n raise SystemExit(50)\\nif mode=='prod' and state=='new':\\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\\nelif mode=='prod' and state=='old':\\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\\nelif mode=='staging' and state=='new':\\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\\nelif mode=='staging' and state=='old':\\n pass\\nelse:\\n raise SystemExit(50)\\n\"}]\nSOURCE_VERSION_IDS=[]\nSOURCE_VERSION_STRINGS=[]\nAST_RECOVERY_END=true\n" +} diff --git a/runtime/history/NEWFI-260907-A-PORTAL200-APPLY4DESIGN7.txt b/runtime/history/NEWFI-260907-A-PORTAL200-APPLY4DESIGN7.txt new file mode 100644 index 00000000..1ace0fd1 --- /dev/null +++ b/runtime/history/NEWFI-260907-A-PORTAL200-APPLY4DESIGN7.txt @@ -0,0 +1,50 @@ +CHAT_OUTPUT_BEGIN +COMMAND_ID=NEWFI-260907-A-PORTAL200-APPLY4DESIGN7 +STATUS=OK +RC=0 +HOST=pve01 +MODE=read-only +COMPONENT=newfi-portal200-apply4-static-ast-recovery +REFERENCE_REGISTER_CHECK=OK +REFERENCE_SHA256=5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66 +ERROR_REGISTER_CHECK=OK +ERROR_REGISTER_SHA256=3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0 +COMMAND_SHA256=f496660f98e2d5bd57395b705f69565e9b9b4fa3823630155bbe079f561a2876 +DUPLICATE_FAILED_COMMAND_BLOCKED=false +EXECUTION_STARTED=true +CHANGE_DECLARED=false +RESULT_CONTRACT_VALID=true +RESULT_CONTRACT_STATUS=NOT_APPLICABLE +RESULT_CONTRACT_ERROR=NONE +COMMAND_RC=0 +CHANGES_MADE=false +ROLLBACK_STARTED=false +ROLLBACK_RESTORED=null +MUTATION_OUTCOME=NO_MUTATION +SANITIZED=yes +SECRETS_INCLUDED=no +PRIVATE_ADDRESSES_INCLUDED=no +RAW_EVIDENCE_SHA256=b0e750afcc8c28e33530e823fa9ca879ddd22ce66e758296aeb27ffd61aa5d1a +SANITIZED_OUTPUT_SHA256=b0e750afcc8c28e33530e823fa9ca879ddd22ce66e758296aeb27ffd61aa5d1a +OUTPUT_BEGIN +AST_RECOVERY_BEGIN=true +OLD_CARRIER_SHA256=573c5c94fe59ed072e094281668ce9847dc4a9882019a2b89d8ba5f190d92176 +OLD_INNER_SHA256=97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184 +OLD_INNER_BYTES=34155 +TOP_LEVEL_LITERAL_ASSIGNMENTS={"BRANCH":"implementation/newfi-closed-mvp-20260906","CID":"NEWFI-260907-PORTAL200-APPLY3","ERRORS_PHP_TEMPLATE":"db();\n$b=%d;\n$rows=$db->fetchAll(\n \"SELECT error_id,exception_type,message,filename,line FROM xf_error_log WHERE error_id>? ORDER BY error_id\",\n [$b]\n);\necho json_encode(\n $rows,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\n?>","EXPECTED_NEW":{"Pub/Controller/Portal.php":[363,"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d"],"Service/PortalData.php":[5213,"4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d"],"_data/templates.xml":[10977,"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861"]},"EXPECTED_OLD":{"Pub/Controller/Portal.php":[247,"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3"],"_data/templates.xml":[1461,"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf"]},"HTTP_VERIFY_PY":"import html,json,re,subprocess,sys,urllib.parse\nmode=sys.argv[1]\nstate=sys.argv[2]\nif mode=='staging':\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\nelif mode=='prod':\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\nelse:\n raise SystemExit(40)\ndef fetch(url):\n cmd=[\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\n ]\n if mode=='staging':\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\n cmd += [\n '-o','-','-w','\\n__PORTAL_STATUS__%{http_code}',url\n ]\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\n if p.returncode!=0: raise SystemExit(41)\n text=p.stdout.decode('utf-8','replace')\n marker='\\n__PORTAL_STATUS__'\n if marker not in text: raise SystemExit(42)\n body,status=text.rsplit(marker,1)\n return int(status.strip()),body\nroot=f'{scheme}://{host}/'\nstatus,body=fetch(root)\nhrefs=[]\nfor raw in re.findall(r\"href=[\\\"']([^\\\"']*css\\.php[^\\\"']*)[\\\"']\",body,re.I):\n raw=html.unescape(raw)\n hrefs.append(urllib.parse.urljoin(root,raw))\nnormal_css=[]\nnormal_status=[]\nfor u in hrefs:\n s,b=fetch(u)\n normal_status.append(s)\n normal_css.append(b)\nnormal_css_text='\\n'.join(normal_css)\nforced_css_marker=None\nforced_css_status=None\nif mode=='staging' and state=='new' and hrefs:\n u=urllib.parse.urlsplit(hrefs[0])\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\n out=[]\n found=False\n for k,v in pairs:\n if k=='css':\n found=True\n parts=[x for x in v.split(',') if x]\n if 'public:newfi_portal.less' not in parts:\n parts.append('public:newfi_portal.less')\n v=','.join(parts)\n out.append((k,v))\n if found:\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\n forced_css_status,forced_body=fetch(forced)\n forced_css_marker='--newfi-portal200' in forced_body\nresult={\n 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\n}\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\nif status!=expected_status: raise SystemExit(45)\nif mode=='prod':\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\nelif mode=='staging':\n if any(x!=200 for x in normal_status): raise SystemExit(46)\nelse:\n raise SystemExit(50)\nif mode=='prod' and state=='new':\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\nelif mode=='prod' and state=='old':\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\nelif mode=='staging' and state=='new':\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\nelif mode=='staging' and state=='old':\n pass\nelse:\n raise SystemExit(50)\n","IMPORT_PHP":"addOnManager()->getById('Newfi/Portal');\nif (!$addOn || !$addOn->isInstalled()) {\n fwrite(STDERR, \"ADDON_NOT_INSTALLED\\n\");\n exit(30);\n}\n$addOn->preRebuild();\n$dataManager = $app->addOnDataManager();\n$imported = [];\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\n $handler = $dataManager->getDataTypeHandler($dataType);\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\n if (!$xml || !$xml->count()) {\n $handler->deleteAddOnData($addOn->addon_id);\n continue;\n }\n $start = 0;\n $guard = 0;\n while (true) {\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\n if (!$last) { break; }\n if ($last <= $start || ++$guard > 100000) {\n fwrite(STDERR, \"IMPORT_PROGRESS_INVALID\\n\");\n exit(31);\n }\n $start = $last;\n }\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\n \\XF::triggerRunOnce();\n $imported[] = $handler->getContainerTag();\n}\n$addOn->postDataImport();\n$repo = $app->repository(\\XF\\Repository\\StyleRepository::class);\n$repo->updateAllStylesLastModifiedDateLater();\n$addOn->postRebuild();\necho \"PORTAL200_IMPORT_JSON=\" . json_encode(\n ['result'=>'PASS','containers'=>$imported],\n JSON_UNESCAPED_SLASHES\n) . \"\\n\";\n?>","INSPECT_PHP":"db();\nfunction srcMeta(string $path): ?array {\n if (!is_file($path)) { return null; }\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\n}\n$portal = $root . '/src/addons/Newfi/Portal';\n$templates = $db->fetchAll(\n \"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \" .\n \"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \" .\n \"LOCATE('--newfi-portal200', template) > 0 AS css_marker \" .\n \"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\"\n);\n$result = [\n 'hostname' => gethostname(),\n 'application' => [\n 'boardActive' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\"),\n 'boardUrl' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\"),\n 'indexRoute' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\"),\n 'defaultStyleId' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\"),\n 'error_count' => (int)$db->fetchOne(\"SELECT COUNT(*) FROM xf_error_log\"),\n 'error_max_id' => (int)$db->fetchOne(\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\")\n ],\n 'addon' => $db->fetchRow(\n \"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \" .\n \"FROM xf_addon WHERE addon_id='Newfi/Portal'\"\n ),\n 'source' => [\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n ],\n 'templates' => $templates\n];\necho \"PORTAL200_INSPECT_JSON_BEGIN\\n\";\necho json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\necho \"\\nPORTAL200_INSPECT_JSON_END\\n\";\n?>","OLD_HEAD":"06bab5acd124e768f70305b3d7eca2f595871890","RUNTIME_VERIFY_PHP_TEMPLATE":"$state];\nif ($state === 'new') {\n $service = new \\Newfi\\Portal\\Service\\PortalData($app);\n $portal = $service->build();\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\n $result['sections'] = [];\n foreach (['forums','resources','media','events','dogs'] as $key) {\n $section = $portal[$key] ?? null;\n if (!is_array($section)) { fwrite(STDERR, \"SECTION_MISSING\\n\"); exit(60); }\n $result['sections'][$key] = [\n 'available'=>(bool)($section['available'] ?? false),\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\n 'status'=>(string)($section['status'] ?? '')\n ];\n }\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\"1\"') !== false;\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\"resources\"') !== false;\n $result['locked_media'] = strpos($html, 'data-portal-locked=\"media\"') !== false;\n $result['html_bytes'] = strlen($html);\n if (!$result['html_marker']) { fwrite(STDERR, \"HTML_MARKER_MISSING\\n\"); exit(61); }\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\n fwrite(STDERR, \"GUEST_PERMISSION_CONTRACT_CHANGED\\n\"); exit(62);\n }\n if (!$result['locked_resources'] || !$result['locked_media']) {\n fwrite(STDERR, \"LOCKED_COPY_MISSING\\n\"); exit(63);\n }\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\n fwrite(STDERR, \"PUBLIC_SECTION_UNAVAILABLE\\n\"); exit(64);\n }\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\n $result['html_bytes'] = strlen($html);\n if ($result['html_marker'] || !$result['old_copy']) {\n fwrite(STDERR, \"OLD_RENDER_CONTRACT\\n\"); exit(65);\n }\n} else {\n exit(66);\n}\necho \"PORTAL200_RUNTIME_JSON=\" . json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n) . \"\\n\";\n?>","commit_created":false,"failure_rollback_started":false,"git_files_changed":false,"old_git_bytes":{},"prod_backup":false,"prod_mutated":false,"push_confirmed":false,"staging_backup":false,"staging_final_new":false,"staging_mutated":false} +CONST_CID=NEWFI-260907-PORTAL200-APPLY3 +CONST_BRANCH=implementation/newfi-closed-mvp-20260906 +CONST_OLD_HEAD=06bab5acd124e768f70305b3d7eca2f595871890 +CONST_WT=UNPROVEN +CONST_CAND_ROOT=UNPROVEN +RELEVANT_CONSTANTS=[{"lineno":10,"segment":"\"NEWFI-260907-PORTAL200-APPLY3\"","value":"NEWFI-260907-PORTAL200-APPLY3"},{"lineno":147,"segment":"r'''db();\nfunction srcMeta(string $path): ?array {\n if (!is_file($path)) { return null; }\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\n}\n$portal = $root . '/src/addons/Newfi/Portal';\n$templates = $db->fetchAll(\n \"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \" .\n \"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \" .\n \"LOCATE('--newfi-portal200', template) > 0 AS css_marker \" .\n \"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\"\n);\n$result = [\n 'hostname' => gethostname(),\n 'application' => [\n 'boardActive' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\"),\n 'boardUrl' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\"),\n 'indexRoute' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\"),\n 'defaultStyleId' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\"),\n 'error_count' => (int)$db->fetchOne(\"SELECT COUNT(*) FROM xf_error_log\"),\n 'error_max_id' => (int)$db->fetchOne(\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\")\n ],\n 'addon' => $db->fetchRow(\n \"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \" .\n \"FROM xf_addon WHERE addon_id='Newfi/Portal'\"\n ),\n 'source' => [\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n ],\n 'templates' => $templates\n];\necho \"PORTAL200_INSPECT_JSON_BEGIN\\n\";\necho json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\necho \"\\nPORTAL200_INSPECT_JSO","value":"db();\nfunction srcMeta(string $path): ?array {\n if (!is_file($path)) { return null; }\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\n}\n$portal = $root . '/src/addons/Newfi/Portal';\n$templates = $db->fetchAll(\n \"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \" .\n \"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \" .\n \"LOCATE('--newfi-portal200', template) > 0 AS css_marker \" .\n \"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\"\n);\n$result = [\n 'hostname' => gethostname(),\n 'application' => [\n 'boardActive' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\"),\n 'boardUrl' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\"),\n 'indexRoute' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\"),\n 'defaultStyleId' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\"),\n 'error_count' => (int)$db->fetchOne(\"SELECT COUNT(*) FROM xf_error_log\"),\n 'error_max_id' => (int)$db->fetchOne(\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\")\n ],\n 'addon' => $db->fetchRow(\n \"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \" .\n \"FROM xf_addon WHERE addon_id='Newfi/Portal'\"\n ),\n 'source' => [\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n ],\n 'templates' => $templates\n];\necho \"PORTAL200_INSPECT_JSON_BEGIN\\n\";\necho json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\necho \"\\nPORTAL200_INSPECT_JSON_END\\n\";\n?>"},{"lineno":194,"segment":"r'''addOnManager()->getById('Newfi/Portal');\nif (!$addOn || !$addOn->isInstalled()) {\n fwrite(STDERR, \"ADDON_NOT_INSTALLED\\n\");\n exit(30);\n}\n$addOn->preRebuild();\n$dataManager = $app->addOnDataManager();\n$imported = [];\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\n $handler = $dataManager->getDataTypeHandler($dataType);\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\n if (!$xml || !$xml->count()) {\n $handler->deleteAddOnData($addOn->addon_id);\n continue;\n }\n $start = 0;\n $guard = 0;\n while (true) {\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\n if (!$last) { break; }\n if ($last <= $start || ++$guard > 100000) {\n fwrite(STDERR, \"IMPORT_PROGRESS_INVALID\\n\");\n exit(31);\n }\n $start = $last;\n }\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\n \\XF::triggerRunOnce();\n $imported[] = $handler->getContainerTag();\n}\n$addOn->postDataImport();\n$repo = $app->repository(\\XF\\Repository\\StyleRepository::class);\n$repo->updateAllStylesLastModifiedDateLater();\n$addOn->postRebuild();\necho \"PORTAL200_IMPORT_JSON=\" . json_encode(\n ['result'=>'PASS','containers'=>$imported],\n JSON_UNESCAPED_SLASHES\n) . \"\\n\";\n?>'''","value":"addOnManager()->getById('Newfi/Portal');\nif (!$addOn || !$addOn->isInstalled()) {\n fwrite(STDERR, \"ADDON_NOT_INSTALLED\\n\");\n exit(30);\n}\n$addOn->preRebuild();\n$dataManager = $app->addOnDataManager();\n$imported = [];\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\n $handler = $dataManager->getDataTypeHandler($dataType);\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\n if (!$xml || !$xml->count()) {\n $handler->deleteAddOnData($addOn->addon_id);\n continue;\n }\n $start = 0;\n $guard = 0;\n while (true) {\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\n if (!$last) { break; }\n if ($last <= $start || ++$guard > 100000) {\n fwrite(STDERR, \"IMPORT_PROGRESS_INVALID\\n\");\n exit(31);\n }\n $start = $last;\n }\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\n \\XF::triggerRunOnce();\n $imported[] = $handler->getContainerTag();\n}\n$addOn->postDataImport();\n$repo = $app->repository(\\XF\\Repository\\StyleRepository::class);\n$repo->updateAllStylesLastModifiedDateLater();\n$addOn->postRebuild();\necho \"PORTAL200_IMPORT_JSON=\" . json_encode(\n ['result'=>'PASS','containers'=>$imported],\n JSON_UNESCAPED_SLASHES\n) . \"\\n\";\n?>"},{"lineno":258,"segment":"r'''$state];\nif ($state === 'new') {\n $service = new \\Newfi\\Portal\\Service\\PortalData($app);\n $portal = $service->build();\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\n $result['sections'] = [];\n foreach (['forums','resources','media','events','dogs'] as $key) {\n $section = $portal[$key] ?? null;\n if (!is_array($section)) { fwrite(STDERR, \"SECTION_MISSING\\n\"); exit(60); }\n $result['sections'][$key] = [\n 'available'=>(bool)($section['available'] ?? false),\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\n 'status'=>(string)($section['status'] ?? '')\n ];\n }\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\"1\"') !== false;\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\"resources\"') !== false;\n $result['locked_media'] = strpos($html, 'data-portal-locked=\"media\"') !== false;\n $result['html_bytes'] = strlen($html);\n if (!$result['html_marker']) { fwrite(STDERR, \"HTML_MARKER_MISSING\\n\"); exit(61); }\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\n fwrite(STDERR, \"GUEST_PERMISSION_CONTRACT_CHANGED\\n\"); exit(62);\n }\n if (!$result['locked_resources'] || !$result['locked_media']) {\n fwrite(STDERR, \"LOCKED_COPY_MISSING\\n\"); exit(63);\n }\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\n fwrite(STDERR, \"PUBLIC_SECTION_UNAVAILABLE\\n\"); exit(64);\n }\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['o","value":"$state];\nif ($state === 'new') {\n $service = new \\Newfi\\Portal\\Service\\PortalData($app);\n $portal = $service->build();\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\n $result['sections'] = [];\n foreach (['forums','resources','media','events','dogs'] as $key) {\n $section = $portal[$key] ?? null;\n if (!is_array($section)) { fwrite(STDERR, \"SECTION_MISSING\\n\"); exit(60); }\n $result['sections'][$key] = [\n 'available'=>(bool)($section['available'] ?? false),\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\n 'status'=>(string)($section['status'] ?? '')\n ];\n }\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\"1\"') !== false;\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\"resources\"') !== false;\n $result['locked_media'] = strpos($html, 'data-portal-locked=\"media\"') !== false;\n $result['html_bytes'] = strlen($html);\n if (!$result['html_marker']) { fwrite(STDERR, \"HTML_MARKER_MISSING\\n\"); exit(61); }\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\n fwrite(STDERR, \"GUEST_PERMISSION_CONTRACT_CHANGED\\n\"); exit(62);\n }\n if (!$result['locked_resources'] || !$result['locked_media']) {\n fwrite(STDERR, \"LOCKED_COPY_MISSING\\n\"); exit(63);\n }\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\n fwrite(STDERR, \"PUBLIC_SECTION_UNAVAILABLE\\n\"); exit(64);\n }\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\n $result['html_bytes'] = strlen($html);\n if ($result['html_marker'] || !$result['old_copy']) {\n fwrite(STDERR, \"OLD_RENDER_CONTRACT\\n\"); exit(65);\n }\n} else {\n exit(66);\n}\necho \"PORTAL200_RUNTIME_JSON=\" . json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n) . \"\\n\";\n?>"},{"lineno":311,"segment":"r'''import html,json,re,subprocess,sys,urllib.parse\nmode=sys.argv[1]\nstate=sys.argv[2]\nif mode=='staging':\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\nelif mode=='prod':\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\nelse:\n raise SystemExit(40)\ndef fetch(url):\n cmd=[\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\n ]\n if mode=='staging':\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\n cmd += [\n '-o','-','-w','\\n__PORTAL_STATUS__%{http_code}',url\n ]\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\n if p.returncode!=0: raise SystemExit(41)\n text=p.stdout.decode('utf-8','replace')\n marker='\\n__PORTAL_STATUS__'\n if marker not in text: raise SystemExit(42)\n body,status=text.rsplit(marker,1)\n return int(status.strip()),body\nroot=f'{scheme}://{host}/'\nstatus,body=fetch(root)\nhrefs=[]\nfor raw in re.findall(r\"href=[\\\"']([^\\\"']*css\\.php[^\\\"']*)[\\\"']\",body,re.I):\n raw=html.unescape(raw)\n hrefs.append(urllib.parse.urljoin(root,raw))\nnormal_css=[]\nnormal_status=[]\nfor u in hrefs:\n s,b=fetch(u)\n normal_status.append(s)\n normal_css.append(b)\nnormal_css_text='\\n'.join(normal_css)\nforced_css_marker=None\nforced_css_status=None\nif mode=='staging' and state=='new' and hrefs:\n u=urllib.parse.urlsplit(hrefs[0])\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\n out=[]\n found=False\n for k,v in pairs:\n if k=='css':\n found=True\n parts=[x for x in v.split(',') if x]\n if 'public:newfi_portal.less' not in parts:\n parts.append('public:newfi_portal.less')\n v=','.join(parts)\n out.append((k,v))\n if found:\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\n forced_css_status,forced_body=fetch(forced)\n forced_css_marker='--newfi-portal200' in ","value":"import html,json,re,subprocess,sys,urllib.parse\nmode=sys.argv[1]\nstate=sys.argv[2]\nif mode=='staging':\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\nelif mode=='prod':\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\nelse:\n raise SystemExit(40)\ndef fetch(url):\n cmd=[\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\n ]\n if mode=='staging':\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\n cmd += [\n '-o','-','-w','\\n__PORTAL_STATUS__%{http_code}',url\n ]\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\n if p.returncode!=0: raise SystemExit(41)\n text=p.stdout.decode('utf-8','replace')\n marker='\\n__PORTAL_STATUS__'\n if marker not in text: raise SystemExit(42)\n body,status=text.rsplit(marker,1)\n return int(status.strip()),body\nroot=f'{scheme}://{host}/'\nstatus,body=fetch(root)\nhrefs=[]\nfor raw in re.findall(r\"href=[\\\"']([^\\\"']*css\\.php[^\\\"']*)[\\\"']\",body,re.I):\n raw=html.unescape(raw)\n hrefs.append(urllib.parse.urljoin(root,raw))\nnormal_css=[]\nnormal_status=[]\nfor u in hrefs:\n s,b=fetch(u)\n normal_status.append(s)\n normal_css.append(b)\nnormal_css_text='\\n'.join(normal_css)\nforced_css_marker=None\nforced_css_status=None\nif mode=='staging' and state=='new' and hrefs:\n u=urllib.parse.urlsplit(hrefs[0])\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\n out=[]\n found=False\n for k,v in pairs:\n if k=='css':\n found=True\n parts=[x for x in v.split(',') if x]\n if 'public:newfi_portal.less' not in parts:\n parts.append('public:newfi_portal.less')\n v=','.join(parts)\n out.append((k,v))\n if found:\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\n forced_css_status,forced_body=fetch(forced)\n forced_css_marker='--newfi-portal200' in forced_body\nresult={\n 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\n}\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\nif status!=expected_status: raise SystemExit(45)\nif mode=='prod':\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\nelif mode=='staging':\n if any(x!=200 for x in normal_status): raise SystemExit(46)\nelse:\n raise SystemExit(50)\nif mode=='prod' and state=='new':\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\nelif mode=='prod' and state=='old':\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\nelif mode=='staging' and state=='new':\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\nelif mode=='staging' and state=='old':\n pass\nelse:\n raise SystemExit(50)\n"},{"lineno":14,"segment":"\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\"","value":"/root/newfi-portal200-candidate/src/addons/Newfi/Portal"},{"lineno":20,"segment":"\"_data/templates.xml\"","value":"_data/templates.xml"},{"lineno":24,"segment":"\"_data/templates.xml\"","value":"_data/templates.xml"},{"lineno":27,"segment":"/root/newfi-portal200-backup-","value":"/root/newfi-portal200-backup-"},{"lineno":401,"segment":"\"PORTAL200_INSPECT_JSON_BEGIN\\n\"","value":"PORTAL200_INSPECT_JSON_BEGIN\n"},{"lineno":402,"segment":"\"\\nPORTAL200_INSPECT_JSON_END\"","value":"\nPORTAL200_INSPECT_JSON_END"},{"lineno":734,"segment":"\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\"","value":"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE"},{"lineno":736,"segment":"\"PORTAL200_FLOW\"","value":"PORTAL200_FLOW"},{"lineno":737,"segment":"\"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\"","value":"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production"},{"lineno":497,"segment":"\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest ! -e \"$B\"\ninstall -d -m 700 \"$B/Pub/Controller\" \"$B/_data\"\ncp -p -- \"$P/Pub/Controller/Portal.php\" \"$B/Pub/Controller/Portal.php\"\ncp -p -- \"$P/_data/templates.xml\" \"$B/_data/templates.xml\"\ntest ! -e \"$P/Service/PortalData.php\"\nprintf 'PORTAL200_BACKUP=PASS\\n'\n","value":"\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest ! -e \"$B\"\ninstall -d -m 700 \"$B/Pub/Controller\" \"$B/_data\"\ncp -p -- \"$P/Pub/Controller/Portal.php\" \"$B/Pub/Controller/Portal.php\"\ncp -p -- \"$P/_data/templates.xml\" \"$B/_data/templates.xml\"\ntest ! -e \"$P/Service/PortalData.php\"\nprintf 'PORTAL200_BACKUP=PASS\n'\n"},{"lineno":507,"segment":"\"PORTAL200_BACKUP=PASS\"","value":"PORTAL200_BACKUP=PASS"},{"lineno":523,"segment":"\"; printf 'PORTAL200_WRITE=PASS\\\\n'\"","value":"; printf 'PORTAL200_WRITE=PASS\\n'"},{"lineno":531,"segment":"\"PORTAL200_WRITE=PASS\"","value":"PORTAL200_WRITE=PASS"},{"lineno":539,"segment":"\"_data/templates.xml\"","value":"_data/templates.xml"},{"lineno":546,"segment":"\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest -f \"$B/Pub/Controller/Portal.php\"\ntest -f \"$B/_data/templates.xml\"\ncp -p -- \"$B/Pub/Controller/Portal.php\" \"$P/Pub/Controller/Portal.php\"\ncp -p -- \"$B/_data/templates.xml\" \"$P/_data/templates.xml\"\nrm -f -- \"$P/Service/PortalData.php\"\nrmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\n","value":"\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest -f \"$B/Pub/Controller/Portal.php\"\ntest -f \"$B/_data/templates.xml\"\ncp -p -- \"$B/Pub/Controller/Portal.php\" \"$P/Pub/Controller/Portal.php\"\ncp -p -- \"$B/_data/templates.xml\" \"$P/_data/templates.xml\"\nrm -f -- \"$P/Service/PortalData.php\"\nrmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\n'\n"},{"lineno":557,"segment":"\"PORTAL200_SOURCE_RESTORE=PASS\"","value":"PORTAL200_SOURCE_RESTORE=PASS"},{"lineno":568,"segment":"\"PORTAL200_IMPORT_JSON=\"","value":"PORTAL200_IMPORT_JSON="},{"lineno":585,"segment":"\"PORTAL200_RUNTIME\"","value":"PORTAL200_RUNTIME"},{"lineno":647,"segment":"\"PORTAL200_HTTP\"","value":"PORTAL200_HTTP"},{"lineno":715,"segment":"\"PORTAL200_CLUSTER\"","value":"PORTAL200_CLUSTER"},{"lineno":758,"segment":"\"\"","value":""},{"lineno":781,"segment":"\"PORTAL200_CANDIDATE_IDENTITY\"","value":"PORTAL200_CANDIDATE_IDENTITY"},{"lineno":783,"segment":"\"PORTAL200_ORCHESTRATION_FIXES\"","value":"PORTAL200_ORCHESTRATION_FIXES"},{"lineno":800,"segment":"\"PORTAL200_GIT_PRESTATE\"","value":"PORTAL200_GIT_PRESTATE"},{"lineno":811,"segment":"\"PORTAL200_RUNTIME_HELPER_LINT\"","value":"PORTAL200_RUNTIME_HELPER_LINT"},{"lineno":825,"segment":"\"PORTAL200_ERROR_BASELINE\"","value":"PORTAL200_ERROR_BASELINE"},{"lineno":831,"segment":"\"PORTAL200_PREFLIGHT\"","value":"PORTAL200_PREFLIGHT"},{"lineno":839,"segment":"\"PORTAL200_STAGING_FIRST_APPLY\"","value":"PORTAL200_STAGING_FIRST_APPLY"},{"lineno":845,"segment":"\"PORTAL200_STAGING_ROLLBACK_PROOF\"","value":"PORTAL200_STAGING_ROLLBACK_PROOF"},{"lineno":852,"segment":"\"PORTAL200_STAGING_FINAL\"","value":"PORTAL200_STAGING_FINAL"},{"lineno":885,"segment":"\"PORTAL200_GIT_PROMOTION\"","value":"PORTAL200_GIT_PROMOTION"},{"lineno":905,"segment":"\"PORTAL200_PRODUCTION\"","value":"PORTAL200_PRODUCTION"},{"lineno":922,"segment":"\"PORTAL200_FINAL_EVIDENCE\"","value":"PORTAL200_FINAL_EVIDENCE"},{"lineno":931,"segment":"\"PORTAL200_PORTAL\"","value":"PORTAL200_PORTAL"},{"lineno":932,"segment":"\"PORTAL200_STAGING\"","value":"PORTAL200_STAGING"},{"lineno":933,"segment":"\"PORTAL200_PRODUCTION\"","value":"PORTAL200_PRODUCTION"},{"lineno":934,"segment":"\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\"","value":"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS"},{"lineno":460,"segment":"1000010","value":1000010},{"lineno":484,"segment":"\"PORTAL200_NEW_XF_ERRORS\"","value":"PORTAL200_NEW_XF_ERRORS"},{"lineno":763,"segment":"\"_data/templates.xml\"","value":"_data/templates.xml"},{"lineno":766,"segment":"\"CANDIDATE_TEMPLATE_SET\"","value":"CANDIDATE_TEMPLATE_SET"},{"lineno":780,"segment":"\"CANDIDATE_MUTATION_SCAN\"","value":"CANDIDATE_MUTATION_SCAN"},{"lineno":941,"segment":"\"PORTAL200_FAILURE\"","value":"PORTAL200_FAILURE"},{"lineno":1004,"segment":"\"PORTAL200_ROADMAP_REMAINS\"","value":"PORTAL200_ROADMAP_REMAINS"},{"lineno":1009,"segment":"\"PORTAL200_FAILURE\"","value":"PORTAL200_FAILURE"},{"lineno":1016,"segment":"\"PORTAL200_ROADMAP_REMAINS\"","value":"PORTAL200_ROADMAP_REMAINS"},{"lineno":461,"segment":"\"version_string\"","value":"version_string"},{"lineno":618,"segment":"\"PORTAL200_HTTP_EXEC\"","value":"PORTAL200_HTTP_EXEC"},{"lineno":762,"segment":"\"CANDIDATE_IDENTITY\"","value":"CANDIDATE_IDENTITY"},{"lineno":460,"segment":"\"version_id\"","value":"version_id"},{"lineno":576,"segment":"\"PORTAL200_RUNTIME_JSON=\"","value":"PORTAL200_RUNTIME_JSON="},{"lineno":613,"segment":"\"PORTAL200_HTTP\"","value":"PORTAL200_HTTP"},{"lineno":638,"segment":"\"PORTAL200_HTTP_JSON=\"","value":"PORTAL200_HTTP_JSON="},{"lineno":991,"segment":"\"PORTAL200_FAILURE_GIT_HISTORY\"","value":"PORTAL200_FAILURE_GIT_HISTORY"},{"lineno":606,"segment":"\"PORTAL200_HTTP_JSON=\"","value":"PORTAL200_HTTP_JSON="},{"lineno":952,"segment":"\"PORTAL200_FAILURE_PROD_ROLLBACK\"","value":"PORTAL200_FAILURE_PROD_ROLLBACK"},{"lineno":966,"segment":"\"PORTAL200_FAILURE_STAGING_ROLLBACK\"","value":"PORTAL200_FAILURE_STAGING_ROLLBACK"},{"lineno":985,"segment":"\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\"","value":"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE"},{"lineno":955,"segment":"\"PORTAL200_FAILURE_PROD_ROLLBACK\"","value":"PORTAL200_FAILURE_PROD_ROLLBACK"},{"lineno":969,"segment":"\"PORTAL200_FAILURE_STAGING_ROLLBACK\"","value":"PORTAL200_FAILURE_STAGING_ROLLBACK"},{"lineno":987,"segment":"\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\"","value":"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE"},{"lineno":519,"segment":"\"/.portal200.XXXXXX\"","value":"/.portal200.XXXXXX"}] +RELEVANT_CALLS=[{"args":["/root/newfi-portal200-candidate/src/addons/Newfi/Portal"],"kwargs":{},"lineno":14,"name":"pathlib.Path","source":"pathlib.Path(\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\")"},{"args":["src/addons/Newfi/Portal"],"kwargs":{},"lineno":15,"name":"pathlib.Path","source":"pathlib.Path(\"src/addons/Newfi/Portal\")"},{"args":["PORTAL200_FLOW","candidate->staging-apply->rollback-proof->staging-reapply->git-push->production"],"kwargs":{},"lineno":735,"name":"emit","source":"emit(\n \"PORTAL200_FLOW\",\n \"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\",\n)"},{"args":["[\"/usr/bin/git\", \"-C\", str(WT), *args]"],"kwargs":{"ok":"ok","timeout":"timeout"},"lineno":77,"name":"run","source":"run([\"/usr/bin/git\", \"-C\", str(WT), *args], timeout=timeout, ok=ok)"},{"args":["cmd"],"kwargs":{"input_bytes":"input_bytes","timeout":"timeout + 30"},"lineno":135,"name":"run","source":"run(cmd, input_bytes=input_bytes, timeout=timeout + 30)"},{"args":["__STATE__","state"],"kwargs":{},"lineno":573,"name":"RUNTIME_VERIFY_PHP_TEMPLATE.replace","source":"RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", state)"},{"args":["target","new"],"kwargs":{},"lineno":659,"name":"runtime_verify","source":"runtime_verify(target, \"new\")"},{"args":[["/usr/sbin/qm","status","210"]],"kwargs":{"timeout":30},"lineno":693,"name":"run","source":"run([\"/usr/sbin/qm\", \"status\", \"210\"], timeout=30)"},{"args":[["/usr/bin/ssh","-T","-o","BatchMode=yes","-o","StrictHostKeyChecking=yes","-o","UpdateHostKeys=no","-o","ConnectTimeout=10","root@pve03","/usr/sbin/qm status 211"]],"kwargs":{"timeout":40},"lineno":696,"name":"run","source":"run(\n [\n \"/usr/bin/ssh\",\n \"-T\",\n \"-o\",\n \"BatchMode=yes\",\n \"-o\",\n \"StrictHostKeyChecking=yes\",\n \"-o\",\n \"UpdateHostKeys=no\",\n \"-o\",\n \"ConnectTimeout=10\",\n \"root@pve03\",\n \"/usr/sbin/qm status 211\",\n ],\n timeout=40,\n )"},{"args":["CAND_ROOT / \"_data/templates.xml\""],"kwargs":{},"lineno":763,"name":"ET.parse","source":"ET.parse(CAND_ROOT / \"_data/templates.xml\")"},{"args":["utf-8"],"kwargs":{},"lineno":767,"name":"read_text","source":"(CAND_ROOT / \"Service/PortalData.php\").read_text(\"utf-8\")"},{"args":["PORTAL200_CANDIDATE_IDENTITY","PASS"],"kwargs":{},"lineno":781,"name":"emit","source":"emit(\"PORTAL200_CANDIDATE_IDENTITY\", \"PASS\")"},{"args":["prod","old"],"kwargs":{},"lineno":827,"name":"runtime_verify","source":"runtime_verify(\"prod\", \"old\")"},{"args":["staging"],"kwargs":{},"lineno":836,"name":"install_candidate","source":"install_candidate(\"staging\")"},{"args":["staging"],"kwargs":{},"lineno":848,"name":"install_candidate","source":"install_candidate(\"staging\")"},{"args":["prod"],"kwargs":{},"lineno":902,"name":"install_candidate","source":"install_candidate(\"prod\")"},{"args":["cmd"],"kwargs":{"check":false,"input":"input_bytes","stderr":"subprocess.PIPE","stdout":"subprocess.PIPE","timeout":"timeout"},"lineno":61,"name":"subprocess.run","source":"subprocess.run(\n cmd,\n input=input_bytes,\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n timeout=timeout,\n check=False,\n )"},{"args":["target","rel","(CAND_ROOT / rel).read_bytes()"],"kwargs":{},"lineno":541,"name":"guest_write","source":"guest_write(target, rel, (CAND_ROOT / rel).read_bytes())"},{"args":["target","old"],"kwargs":{},"lineno":674,"name":"runtime_verify","source":"runtime_verify(target, \"old\")"},{"args":["CANDIDATE_TEMPLATE_SET"],"kwargs":{},"lineno":766,"name":"fail","source":"fail(\"CANDIDATE_TEMPLATE_SET\")"},{"args":["CANDIDATE_MUTATION_SCAN"],"kwargs":{},"lineno":780,"name":"fail","source":"fail(\"CANDIDATE_MUTATION_SCAN\")"},{"args":["__STATE__","new"],"kwargs":{},"lineno":807,"name":"RUNTIME_VERIFY_PHP_TEMPLATE.replace","source":"RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\")"},{"args":["__STATE__","new"],"kwargs":{},"lineno":810,"name":"RUNTIME_VERIFY_PHP_TEMPLATE.replace","source":"RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\")"},{"args":["CAND_ROOT / rel","dest"],"kwargs":{},"lineno":858,"name":"shutil.copyfile","source":"shutil.copyfile(CAND_ROOT / rel, dest)"},{"args":["addon.get(\"version_id\", 0)"],"kwargs":{},"lineno":460,"name":"int","source":"int(addon.get(\"version_id\", 0))"},{"args":["version_string"],"kwargs":{},"lineno":461,"name":"addon.get","source":"addon.get(\"version_string\")"},{"args":[],"kwargs":{},"lineno":541,"name":"read_bytes","source":"(CAND_ROOT / rel).read_bytes()"},{"args":["argv"],"kwargs":{"check":false,"input":"HTTP_VERIFY_PY.encode()","stderr":"subprocess.PIPE","stdout":"subprocess.PIPE","timeout":180},"lineno":593,"name":"subprocess.run","source":"subprocess.run(\n argv,\n input=HTTP_VERIFY_PY.encode(),\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n timeout=180,\n check=False,\n )"},{"args":["CANDIDATE_IDENTITY"],"kwargs":{},"lineno":762,"name":"fail","source":"fail(\"CANDIDATE_IDENTITY\")"},{"args":["version_id",0],"kwargs":{},"lineno":460,"name":"addon.get","source":"addon.get(\"version_id\", 0)"},{"args":["old"],"kwargs":{},"lineno":979,"name":"dest.write_bytes","source":"dest.write_bytes(old)"}] +RELEVANT_SOURCE_CONTEXTS=[{"line":10,"text":"import subprocess\nimport sys\nimport xml.etree.ElementTree as ET\n\nCID = \"NEWFI-260907-PORTAL200-APPLY3\"\nBRANCH = \"implementation/newfi-closed-mvp-20260906\"\nOLD_HEAD = \"06bab5acd124e768f70305b3d7eca2f595871890\"\nWT = pathlib.Path(\"/root/newfi-portal/.worktrees/implementation-newfi-closed-mvp-20260906\")\nCAND_ROOT = pathlib.Path(\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\")\nPORTAL_REL = pathlib.Path(\"src/addons/Newfi/Portal\")\nGIT_PORTAL = WT / PORTAL_REL\nEXPECTED_NEW = {\n \"Pub/Controller/Portal.php\": (363, \"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\"),"},{"line":14,"text":"CID = \"NEWFI-260907-PORTAL200-APPLY3\"\nBRANCH = \"implementation/newfi-closed-mvp-20260906\"\nOLD_HEAD = \"06bab5acd124e768f70305b3d7eca2f595871890\"\nWT = pathlib.Path(\"/root/newfi-portal/.worktrees/implementation-newfi-closed-mvp-20260906\")\nCAND_ROOT = pathlib.Path(\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\")\nPORTAL_REL = pathlib.Path(\"src/addons/Newfi/Portal\")\nGIT_PORTAL = WT / PORTAL_REL\nEXPECTED_NEW = {\n \"Pub/Controller/Portal.php\": (363, \"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\"),\n \"Service/PortalData.php\": (5213, \"4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d\"),\n \"_data/templates.xml\": (10977, \"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\"),\n}\nEXPECTED_OLD = {"},{"line":20,"text":"GIT_PORTAL = WT / PORTAL_REL\nEXPECTED_NEW = {\n \"Pub/Controller/Portal.php\": (363, \"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\"),\n \"Service/PortalData.php\": (5213, \"4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d\"),\n \"_data/templates.xml\": (10977, \"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\"),\n}\nEXPECTED_OLD = {\n \"Pub/Controller/Portal.php\": (247, \"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\"),\n \"_data/templates.xml\": (1461, \"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\"),\n}\nEXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)]\nBACKUP = f\"/root/newfi-portal200-backup-{CID}\"\n"},{"line":24,"text":" \"_data/templates.xml\": (10977, \"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\"),\n}\nEXPECTED_OLD = {\n \"Pub/Controller/Portal.php\": (247, \"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\"),\n \"_data/templates.xml\": (1461, \"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\"),\n}\nEXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)]\nBACKUP = f\"/root/newfi-portal200-backup-{CID}\"\n\n\nclass PortalError(RuntimeError):\n pass\n"},{"line":27,"text":" \"Pub/Controller/Portal.php\": (247, \"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\"),\n \"_data/templates.xml\": (1461, \"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\"),\n}\nEXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)]\nBACKUP = f\"/root/newfi-portal200-backup-{CID}\"\n\n\nclass PortalError(RuntimeError):\n pass\n\n\ndef emit(key, value=None):\n if value is None:"},{"line":160,"text":"}\n$portal = $root . '/src/addons/Newfi/Portal';\n$templates = $db->fetchAll(\n \"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \" .\n \"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \" .\n \"LOCATE('--newfi-portal200', template) > 0 AS css_marker \" .\n \"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\"\n);\n$result = [\n 'hostname' => gethostname(),\n 'application' => [\n 'boardActive' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\"),\n 'boardUrl' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\"),"},{"line":161,"text":"$portal = $root . '/src/addons/Newfi/Portal';\n$templates = $db->fetchAll(\n \"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \" .\n \"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \" .\n \"LOCATE('--newfi-portal200', template) > 0 AS css_marker \" .\n \"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\"\n);\n$result = [\n 'hostname' => gethostname(),\n 'application' => [\n 'boardActive' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\"),\n 'boardUrl' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\"),\n 'indexRoute' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\"),"},{"line":175,"text":" 'error_count' => (int)$db->fetchOne(\"SELECT COUNT(*) FROM xf_error_log\"),\n 'error_max_id' => (int)$db->fetchOne(\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\")\n ],\n 'addon' => $db->fetchRow(\n \"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \" .\n \"FROM xf_addon WHERE addon_id='Newfi/Portal'\"\n ),\n 'source' => [\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n ],\n 'templates' => $templates"},{"line":181,"text":" ),\n 'source' => [\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n ],\n 'templates' => $templates\n];\necho \"PORTAL200_INSPECT_JSON_BEGIN\\n\";\necho json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);"},{"line":185,"text":" '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n ],\n 'templates' => $templates\n];\necho \"PORTAL200_INSPECT_JSON_BEGIN\\n\";\necho json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\necho \"\\nPORTAL200_INSPECT_JSON_END\\n\";\n?>'''\n\n"},{"line":190,"text":"echo json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\necho \"\\nPORTAL200_INSPECT_JSON_END\\n\";\n?>'''\n\n\nIMPORT_PHP = r'''postDataImport();\n$repo = $app->repository(\\XF\\Repository\\StyleRepository::class);\n$repo->updateAllStylesLastModifiedDateLater();\n$addOn->postRebuild();\necho \"PORTAL200_IMPORT_JSON=\" . json_encode(\n ['result'=>'PASS','containers'=>$imported],\n JSON_UNESCAPED_SLASHES\n) . \"\\n\";\n?>'''\n\n\nERRORS_PHP_TEMPLATE = r'''(bool)($section['hasItems'] ?? false),\n 'status'=>(string)($section['status'] ?? '')\n ];\n }\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\"1\"') !== false;\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\"resources\"') !== false;\n $result['locked_media'] = strpos($html, 'data-portal-locked=\"media\"') !== false;\n $result['html_bytes'] = strlen($html);\n if (!$result['html_marker']) { fwrite(STDERR, \"HTML_MARKER_MISSING\\n\"); exit(61); }\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\n fwrite(STDERR, \"GUEST_PERMISSION_CONTRACT_CHANGED\\n\"); exit(62);\n }\n if (!$result['locked_resources'] || !$result['locked_media']) {"},{"line":288,"text":" if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\n fwrite(STDERR, \"GUEST_PERMISSION_CONTRACT_CHANGED\\n\"); exit(62);\n }\n if (!$result['locked_resources'] || !$result['locked_media']) {\n fwrite(STDERR, \"LOCKED_COPY_MISSING\\n\"); exit(63);\n }\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\n fwrite(STDERR, \"PUBLIC_SECTION_UNAVAILABLE\\n\"); exit(64);\n }\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;"},{"line":295,"text":" fwrite(STDERR, \"PUBLIC_SECTION_UNAVAILABLE\\n\"); exit(64);\n }\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\n $result['html_bytes'] = strlen($html);\n if ($result['html_marker'] || !$result['old_copy']) {\n fwrite(STDERR, \"OLD_RENDER_CONTRACT\\n\"); exit(65);\n }\n} else {\n exit(66);\n}"},{"line":296,"text":" }\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\n $result['html_bytes'] = strlen($html);\n if ($result['html_marker'] || !$result['old_copy']) {\n fwrite(STDERR, \"OLD_RENDER_CONTRACT\\n\"); exit(65);\n }\n} else {\n exit(66);\n}\necho \"PORTAL200_RUNTIME_JSON=\" . json_encode("},{"line":298,"text":" $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\n $result['html_bytes'] = strlen($html);\n if ($result['html_marker'] || !$result['old_copy']) {\n fwrite(STDERR, \"OLD_RENDER_CONTRACT\\n\"); exit(65);\n }\n} else {\n exit(66);\n}\necho \"PORTAL200_RUNTIME_JSON=\" . json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE"},{"line":304,"text":" }\n} else {\n exit(66);\n}\necho \"PORTAL200_RUNTIME_JSON=\" . json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n) . \"\\n\";\n?>'''\n\n\nHTTP_VERIFY_PY = r'''import html,json,re,subprocess,sys,urllib.parse\nmode=sys.argv[1]"},{"line":367,"text":" out.append((k,v))\n if found:\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\n forced_css_status,forced_body=fetch(forced)\n forced_css_marker='--newfi-portal200' in forced_body\nresult={\n 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,"},{"line":371,"text":" forced_css_marker='--newfi-portal200' in forced_body\nresult={\n 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\n}\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))"},{"line":372,"text":"result={\n 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\n}\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\nif status!=expected_status: raise SystemExit(45)"},{"line":373,"text":" 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\n}\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\nif status!=expected_status: raise SystemExit(45)\nif mode=='prod':"},{"line":379,"text":" 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\n}\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\nif status!=expected_status: raise SystemExit(45)\nif mode=='prod':\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\nelif mode=='staging':\n if any(x!=200 for x in normal_status): raise SystemExit(46)\nelse:\n raise SystemExit(50)\nif mode=='prod' and state=='new':"},{"line":390,"text":" raise SystemExit(50)\nif mode=='prod' and state=='new':\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\nelif mode=='prod' and state=='old':\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\nelif mode=='staging' and state=='new':\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\nelif mode=='staging' and state=='old':\n pass\nelse:\n raise SystemExit(50)\n'''\n"},{"line":401,"text":"'''\n\n\ndef parse_inspect(text):\n b = \"PORTAL200_INSPECT_JSON_BEGIN\\n\"\n e = \"\\nPORTAL200_INSPECT_JSON_END\"\n if b not in text or e not in text:\n fail(\"INSPECT_MARKERS\")\n try:\n return json.loads(text.split(b, 1)[1].split(e, 1)[0])\n except Exception:\n fail(\"INSPECT_JSON\")\n"},{"line":402,"text":"\n\ndef parse_inspect(text):\n b = \"PORTAL200_INSPECT_JSON_BEGIN\\n\"\n e = \"\\nPORTAL200_INSPECT_JSON_END\"\n if b not in text or e not in text:\n fail(\"INSPECT_MARKERS\")\n try:\n return json.loads(text.split(b, 1)[1].split(e, 1)[0])\n except Exception:\n fail(\"INSPECT_JSON\")\n\n"},{"line":460,"text":" and a.get(\"boardUrl\") == expected[1]\n and a.get(\"indexRoute\") == \"portal/\"\n and a.get(\"defaultStyleId\") == \"4\"\n and addon.get(\"addon_id\") == \"Newfi/Portal\"\n and int(addon.get(\"version_id\", 0)) == 1000010\n and addon.get(\"version_string\") == \"1.0.0 Alpha 1\"\n and int(addon.get(\"active\", 0)) == 1\n and int(addon.get(\"is_processing\", 1)) == 0\n )\n\n\ndef new_errors(target, baseline):\n raw = qga("},{"line":461,"text":" and a.get(\"indexRoute\") == \"portal/\"\n and a.get(\"defaultStyleId\") == \"4\"\n and addon.get(\"addon_id\") == \"Newfi/Portal\"\n and int(addon.get(\"version_id\", 0)) == 1000010\n and addon.get(\"version_string\") == \"1.0.0 Alpha 1\"\n and int(addon.get(\"active\", 0)) == 1\n and int(addon.get(\"is_processing\", 1)) == 0\n )\n\n\ndef new_errors(target, baseline):\n raw = qga(\n target,"},{"line":484,"text":"def assert_no_new_errors(target, baseline, phase):\n rows = new_errors(target, baseline)\n if rows:\n emit(\n \"PORTAL200_NEW_XF_ERRORS\",\n {\n \"target\": target,\n \"phase\": phase,\n \"count\": len(rows),\n \"ids\": [r.get(\"error_id\") for r in rows],\n },\n )\n fail(\"NEW_XF_ERRORS\")"},{"line":502,"text":"P=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest ! -e \"$B\"\ninstall -d -m 700 \"$B/Pub/Controller\" \"$B/_data\"\ncp -p -- \"$P/Pub/Controller/Portal.php\" \"$B/Pub/Controller/Portal.php\"\ncp -p -- \"$P/_data/templates.xml\" \"$B/_data/templates.xml\"\ntest ! -e \"$P/Service/PortalData.php\"\nprintf 'PORTAL200_BACKUP=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_BACKUP=PASS\" not in out:\n fail(\"BACKUP_FAILED\")\n\n"},{"line":504,"text":"install -d -m 700 \"$B/Pub/Controller\" \"$B/_data\"\ncp -p -- \"$P/Pub/Controller/Portal.php\" \"$B/Pub/Controller/Portal.php\"\ncp -p -- \"$P/_data/templates.xml\" \"$B/_data/templates.xml\"\ntest ! -e \"$P/Service/PortalData.php\"\nprintf 'PORTAL200_BACKUP=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_BACKUP=PASS\" not in out:\n fail(\"BACKUP_FAILED\")\n\n\ndef guest_write(target, rel, data):\n dest = f\"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}\""},{"line":507,"text":"test ! -e \"$P/Service/PortalData.php\"\nprintf 'PORTAL200_BACKUP=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_BACKUP=PASS\" not in out:\n fail(\"BACKUP_FAILED\")\n\n\ndef guest_write(target, rel, data):\n dest = f\"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}\"\n parent = str(pathlib.PurePosixPath(dest).parent)\n script = (\n \"set -eu; umask 022; \""},{"line":519,"text":" \"set -eu; umask 022; \"\n + \"install -d -m 755 \"\n + shlex.quote(parent)\n + \"; T=$(mktemp \"\n + shlex.quote(parent + \"/.portal200.XXXXXX\")\n + \"); cat >\\\"$T\\\"; chmod 0644 \\\"$T\\\"; chown root:root \\\"$T\\\"; \"\n + \"mv -f -- \\\"$T\\\" \"\n + shlex.quote(dest)\n + \"; printf 'PORTAL200_WRITE=PASS\\\\n'\"\n )\n out = qga(\n target,\n [\"/bin/bash\", \"-lc\", script],"},{"line":523,"text":" + shlex.quote(parent + \"/.portal200.XXXXXX\")\n + \"); cat >\\\"$T\\\"; chmod 0644 \\\"$T\\\"; chown root:root \\\"$T\\\"; \"\n + \"mv -f -- \\\"$T\\\" \"\n + shlex.quote(dest)\n + \"; printf 'PORTAL200_WRITE=PASS\\\\n'\"\n )\n out = qga(\n target,\n [\"/bin/bash\", \"-lc\", script],\n input_bytes=data,\n timeout=90,\n )\n if \"PORTAL200_WRITE=PASS\" not in out:"},{"line":531,"text":" [\"/bin/bash\", \"-lc\", script],\n input_bytes=data,\n timeout=90,\n )\n if \"PORTAL200_WRITE=PASS\" not in out:\n fail(\"WRITE_FAILED\")\n\n\ndef install_candidate(target):\n for rel in [\n \"Pub/Controller/Portal.php\",\n \"Service/PortalData.php\",\n \"_data/templates.xml\","},{"line":532,"text":" input_bytes=data,\n timeout=90,\n )\n if \"PORTAL200_WRITE=PASS\" not in out:\n fail(\"WRITE_FAILED\")\n\n\ndef install_candidate(target):\n for rel in [\n \"Pub/Controller/Portal.php\",\n \"Service/PortalData.php\",\n \"_data/templates.xml\",\n ]:"},{"line":535,"text":" if \"PORTAL200_WRITE=PASS\" not in out:\n fail(\"WRITE_FAILED\")\n\n\ndef install_candidate(target):\n for rel in [\n \"Pub/Controller/Portal.php\",\n \"Service/PortalData.php\",\n \"_data/templates.xml\",\n ]:\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\n\n"},{"line":539,"text":"def install_candidate(target):\n for rel in [\n \"Pub/Controller/Portal.php\",\n \"Service/PortalData.php\",\n \"_data/templates.xml\",\n ]:\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\n\n\ndef restore_old(target):\n script = f'''set -eu\nB={shlex.quote(BACKUP)}\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal"},{"line":541,"text":" \"Pub/Controller/Portal.php\",\n \"Service/PortalData.php\",\n \"_data/templates.xml\",\n ]:\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\n\n\ndef restore_old(target):\n script = f'''set -eu\nB={shlex.quote(BACKUP)}\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest -f \"$B/Pub/Controller/Portal.php\"\ntest -f \"$B/_data/templates.xml\""},{"line":549,"text":" script = f'''set -eu\nB={shlex.quote(BACKUP)}\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest -f \"$B/Pub/Controller/Portal.php\"\ntest -f \"$B/_data/templates.xml\"\ncp -p -- \"$B/Pub/Controller/Portal.php\" \"$P/Pub/Controller/Portal.php\"\ncp -p -- \"$B/_data/templates.xml\" \"$P/_data/templates.xml\"\nrm -f -- \"$P/Service/PortalData.php\"\nrmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_SOURCE_RESTORE=PASS\" not in out:"},{"line":551,"text":"P=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest -f \"$B/Pub/Controller/Portal.php\"\ntest -f \"$B/_data/templates.xml\"\ncp -p -- \"$B/Pub/Controller/Portal.php\" \"$P/Pub/Controller/Portal.php\"\ncp -p -- \"$B/_data/templates.xml\" \"$P/_data/templates.xml\"\nrm -f -- \"$P/Service/PortalData.php\"\nrmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_SOURCE_RESTORE=PASS\" not in out:\n fail(\"SOURCE_RESTORE_FAILED\")\n"},{"line":554,"text":"cp -p -- \"$B/Pub/Controller/Portal.php\" \"$P/Pub/Controller/Portal.php\"\ncp -p -- \"$B/_data/templates.xml\" \"$P/_data/templates.xml\"\nrm -f -- \"$P/Service/PortalData.php\"\nrmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_SOURCE_RESTORE=PASS\" not in out:\n fail(\"SOURCE_RESTORE_FAILED\")\n\n\ndef import_addon(target):\n out = qga("},{"line":557,"text":"rmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_SOURCE_RESTORE=PASS\" not in out:\n fail(\"SOURCE_RESTORE_FAILED\")\n\n\ndef import_addon(target):\n out = qga(\n target,\n [\"/usr/bin/php8.3\"],\n input_bytes=IMPORT_PHP.encode(),"},{"line":568,"text":" [\"/usr/bin/php8.3\"],\n input_bytes=IMPORT_PHP.encode(),\n timeout=240,\n )\n if \"PORTAL200_IMPORT_JSON=\" not in out:\n fail(\"IMPORT_FAILED\")\n\n\ndef runtime_verify(target, state):\n src = RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", state)\n out = qga(target, [\"/usr/bin/php8.3\"], input_bytes=src.encode(), timeout=120)\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_RUNTIME_JSON=\")),"},{"line":576,"text":"def runtime_verify(target, state):\n src = RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", state)\n out = qga(target, [\"/usr/bin/php8.3\"], input_bytes=src.encode(), timeout=120)\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_RUNTIME_JSON=\")),\n None,\n )\n if not line:\n fail(\"RUNTIME_MARKER_MISSING\")\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n except Exception:\n fail(\"RUNTIME_JSON\")"},{"line":585,"text":" try:\n data = json.loads(line.split(\"=\", 1)[1])\n except Exception:\n fail(\"RUNTIME_JSON\")\n emit(\"PORTAL200_RUNTIME\", {\"target\": target, **data})\n return data\n\n\ndef http_verify(target, state):\n argv = [\"/usr/bin/python3\", \"-\", target, state]\n if target == \"prod\":\n try:\n p = subprocess.run("},{"line":606,"text":" fail(\"HTTP_PROD_TIMEOUT\")\n out = p.stdout.decode(\"utf-8\", \"replace\")\n err = p.stderr.decode(\"utf-8\", \"replace\")\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_HTTP_JSON=\")),\n None,\n )\n data = None\n if line:\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\n except Exception:"},{"line":613,"text":" data = None\n if line:\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\n except Exception:\n fail(\"HTTP_JSON\")\n if p.returncode != 0:\n emit(\n \"PORTAL200_HTTP_EXEC\",\n {\n \"target\": target,\n \"state\": state,"},{"line":618,"text":" except Exception:\n fail(\"HTTP_JSON\")\n if p.returncode != 0:\n emit(\n \"PORTAL200_HTTP_EXEC\",\n {\n \"target\": target,\n \"state\": state,\n \"rc\": p.returncode,\n \"stderr\": err[:800],\n },\n )\n fail(\"HTTP_PROD_RC\")"},{"line":638,"text":" input_bytes=HTTP_VERIFY_PY.encode(),\n timeout=180,\n )\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_HTTP_JSON=\")),\n None,\n )\n if not line:\n fail(\"HTTP_MARKER_MISSING\")\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n except Exception:\n fail(\"HTTP_JSON\")"},{"line":647,"text":" try:\n data = json.loads(line.split(\"=\", 1)[1])\n except Exception:\n fail(\"HTTP_JSON\")\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\n return data\n\n\ndef verify_new(target, baseline):\n data = inspect(target)\n if not app_contract(data, target):\n fail(\"APP_CONTRACT_NEW\")\n if not source_is_new(data):"},{"line":715,"text":" timeout=40,\n )\n if \"status: running\" not in p.stdout.decode():\n fail(\"VM211_NOT_RUNNING\")\n emit(\"PORTAL200_CLUSTER\", \"PASS\")\n\n\ndef result_contract(status, changes, rollback_started, rollback_restored):\n emit(\n \"HOMELAB_RESULT_CONTRACT\",\n {\n \"version\": 1,\n \"command_id\": CID,"},{"line":734,"text":"\n\nemit(\"NEWFI200_BEGIN\")\nemit(\"COMMAND_ID\", CID)\nemit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\", 22)\nemit(\n \"PORTAL200_FLOW\",\n \"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\",\n)\n\nstaging_backup = False\nstaging_mutated = False\nstaging_final_new = False"},{"line":736,"text":"emit(\"NEWFI200_BEGIN\")\nemit(\"COMMAND_ID\", CID)\nemit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\", 22)\nemit(\n \"PORTAL200_FLOW\",\n \"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\",\n)\n\nstaging_backup = False\nstaging_mutated = False\nstaging_final_new = False\nprod_backup = False\nprod_mutated = False"},{"line":737,"text":"emit(\"COMMAND_ID\", CID)\nemit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\", 22)\nemit(\n \"PORTAL200_FLOW\",\n \"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\",\n)\n\nstaging_backup = False\nstaging_mutated = False\nstaging_final_new = False\nprod_backup = False\nprod_mutated = False\ngit_files_changed = False"},{"line":758,"text":"s_old_templates = None\np_old_templates = None\n\ntry:\n compile(HTTP_VERIFY_PY, \"\", \"exec\")\n for rel, (size, sha) in EXPECTED_NEW.items():\n p = CAND_ROOT / rel\n if not p.is_file() or file_meta(p) != (size, sha):\n fail(\"CANDIDATE_IDENTITY\")\n tree = ET.parse(CAND_ROOT / \"_data/templates.xml\")\n titles = sorted(x.attrib.get(\"title\") for x in tree.getroot().findall(\"template\"))\n if titles != [\"newfi_portal\", \"newfi_portal.less\"]:\n fail(\"CANDIDATE_TEMPLATE_SET\")"},{"line":760,"text":"\ntry:\n compile(HTTP_VERIFY_PY, \"\", \"exec\")\n for rel, (size, sha) in EXPECTED_NEW.items():\n p = CAND_ROOT / rel\n if not p.is_file() or file_meta(p) != (size, sha):\n fail(\"CANDIDATE_IDENTITY\")\n tree = ET.parse(CAND_ROOT / \"_data/templates.xml\")\n titles = sorted(x.attrib.get(\"title\") for x in tree.getroot().findall(\"template\"))\n if titles != [\"newfi_portal\", \"newfi_portal.less\"]:\n fail(\"CANDIDATE_TEMPLATE_SET\")\n service = (CAND_ROOT / \"Service/PortalData.php\").read_text(\"utf-8\")\n if any("},{"line":762,"text":" compile(HTTP_VERIFY_PY, \"\", \"exec\")\n for rel, (size, sha) in EXPECTED_NEW.items():\n p = CAND_ROOT / rel\n if not p.is_file() or file_meta(p) != (size, sha):\n fail(\"CANDIDATE_IDENTITY\")\n tree = ET.parse(CAND_ROOT / \"_data/templates.xml\")\n titles = sorted(x.attrib.get(\"title\") for x in tree.getroot().findall(\"template\"))\n if titles != [\"newfi_portal\", \"newfi_portal.less\"]:\n fail(\"CANDIDATE_TEMPLATE_SET\")\n service = (CAND_ROOT / \"Service/PortalData.php\").read_text(\"utf-8\")\n if any(\n token in service\n for token in ["},{"line":763,"text":" for rel, (size, sha) in EXPECTED_NEW.items():\n p = CAND_ROOT / rel\n if not p.is_file() or file_meta(p) != (size, sha):\n fail(\"CANDIDATE_IDENTITY\")\n tree = ET.parse(CAND_ROOT / \"_data/templates.xml\")\n titles = sorted(x.attrib.get(\"title\") for x in tree.getroot().findall(\"template\"))\n if titles != [\"newfi_portal\", \"newfi_portal.less\"]:\n fail(\"CANDIDATE_TEMPLATE_SET\")\n service = (CAND_ROOT / \"Service/PortalData.php\").read_text(\"utf-8\")\n if any(\n token in service\n for token in [\n \"\\\\XF::db(\","},{"line":766,"text":" fail(\"CANDIDATE_IDENTITY\")\n tree = ET.parse(CAND_ROOT / \"_data/templates.xml\")\n titles = sorted(x.attrib.get(\"title\") for x in tree.getroot().findall(\"template\"))\n if titles != [\"newfi_portal\", \"newfi_portal.less\"]:\n fail(\"CANDIDATE_TEMPLATE_SET\")\n service = (CAND_ROOT / \"Service/PortalData.php\").read_text(\"utf-8\")\n if any(\n token in service\n for token in [\n \"\\\\XF::db(\",\n \"->save(\",\n \"->delete(\",\n \"INSERT \","},{"line":767,"text":" tree = ET.parse(CAND_ROOT / \"_data/templates.xml\")\n titles = sorted(x.attrib.get(\"title\") for x in tree.getroot().findall(\"template\"))\n if titles != [\"newfi_portal\", \"newfi_portal.less\"]:\n fail(\"CANDIDATE_TEMPLATE_SET\")\n service = (CAND_ROOT / \"Service/PortalData.php\").read_text(\"utf-8\")\n if any(\n token in service\n for token in [\n \"\\\\XF::db(\",\n \"->save(\",\n \"->delete(\",\n \"INSERT \",\n \"UPDATE \","},{"line":780,"text":" \"DELETE \",\n \"REPLACE \",\n ]\n ):\n fail(\"CANDIDATE_MUTATION_SCAN\")\n emit(\"PORTAL200_CANDIDATE_IDENTITY\", \"PASS\")\n emit(\n \"PORTAL200_ORCHESTRATION_FIXES\",\n {\n \"staging_503_without_css_links\": True,\n \"failure_status\": \"FAIL\",\n \"prod_http_via_pve01_public_route\": True,\n \"css_href_regex_fixed\": True,"},{"line":781,"text":" \"REPLACE \",\n ]\n ):\n fail(\"CANDIDATE_MUTATION_SCAN\")\n emit(\"PORTAL200_CANDIDATE_IDENTITY\", \"PASS\")\n emit(\n \"PORTAL200_ORCHESTRATION_FIXES\",\n {\n \"staging_503_without_css_links\": True,\n \"failure_status\": \"FAIL\",\n \"prod_http_via_pve01_public_route\": True,\n \"css_href_regex_fixed\": True,\n },"},{"line":783,"text":" ):\n fail(\"CANDIDATE_MUTATION_SCAN\")\n emit(\"PORTAL200_CANDIDATE_IDENTITY\", \"PASS\")\n emit(\n \"PORTAL200_ORCHESTRATION_FIXES\",\n {\n \"staging_503_without_css_links\": True,\n \"failure_status\": \"FAIL\",\n \"prod_http_via_pve01_public_route\": True,\n \"css_href_regex_fixed\": True,\n },\n )\n"},{"line":800,"text":" remote = remote_out.split()[0] if remote_out else \"\"\n if branch != BRANCH or head != OLD_HEAD or remote != OLD_HEAD or status:\n fail(\"GIT_PRESTATE\")\n emit(\n \"PORTAL200_GIT_PRESTATE\",\n {\"branch\": branch, \"head\": head, \"remote\": remote, \"clean\": True},\n )\n cluster_guard()\n\n lint_php(\"staging\", INSPECT_PHP, \"INSPECT_STAGING\")\n lint_php(\"staging\", IMPORT_PHP, \"IMPORT_STAGING\")\n lint_php(\"staging\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\"), \"RUNTIME_STAGING\")\n lint_php(\"prod\", INSPECT_PHP, \"INSPECT_PROD\")"},{"line":811,"text":" lint_php(\"staging\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\"), \"RUNTIME_STAGING\")\n lint_php(\"prod\", INSPECT_PHP, \"INSPECT_PROD\")\n lint_php(\"prod\", IMPORT_PHP, \"IMPORT_PROD\")\n lint_php(\"prod\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\"), \"RUNTIME_PROD\")\n emit(\"PORTAL200_RUNTIME_HELPER_LINT\", \"PASS\")\n\n s0 = inspect(\"staging\")\n p0 = inspect(\"prod\")\n if not app_contract(s0, \"staging\") or not app_contract(p0, \"prod\"):\n fail(\"APP_PRESTATE\")\n if not source_is_old(s0) or not source_is_old(p0):\n fail(\"SOURCE_PRESTATE\")\n if s0.get(\"templates\") != p0.get(\"templates\"):"},{"line":825,"text":" s_error = int(s0[\"application\"][\"error_max_id\"])\n p_error = int(p0[\"application\"][\"error_max_id\"])\n s_old_templates = s0[\"templates\"]\n p_old_templates = p0[\"templates\"]\n emit(\"PORTAL200_ERROR_BASELINE\", {\"staging\": s_error, \"prod\": p_error})\n http_verify(\"staging\", \"old\")\n runtime_verify(\"prod\", \"old\")\n http_verify(\"prod\", \"old\")\n assert_no_new_errors(\"staging\", s_error, \"preflight\")\n assert_no_new_errors(\"prod\", p_error, \"preflight\")\n emit(\"PORTAL200_PREFLIGHT\", \"PASS\")\n\n guest_backup(\"staging\")"},{"line":831,"text":" runtime_verify(\"prod\", \"old\")\n http_verify(\"prod\", \"old\")\n assert_no_new_errors(\"staging\", s_error, \"preflight\")\n assert_no_new_errors(\"prod\", p_error, \"preflight\")\n emit(\"PORTAL200_PREFLIGHT\", \"PASS\")\n\n guest_backup(\"staging\")\n staging_backup = True\n staging_mutated = True\n install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n emit(\"PORTAL200_STAGING_FIRST_APPLY\", \"PASS\")"},{"line":836,"text":"\n guest_backup(\"staging\")\n staging_backup = True\n staging_mutated = True\n install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n emit(\"PORTAL200_STAGING_FIRST_APPLY\", \"PASS\")\n\n restore_old(\"staging\")\n import_addon(\"staging\")\n verify_old(\"staging\", s_error, s_old_templates)\n staging_mutated = False"},{"line":839,"text":" staging_mutated = True\n install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n emit(\"PORTAL200_STAGING_FIRST_APPLY\", \"PASS\")\n\n restore_old(\"staging\")\n import_addon(\"staging\")\n verify_old(\"staging\", s_error, s_old_templates)\n staging_mutated = False\n emit(\"PORTAL200_STAGING_ROLLBACK_PROOF\", \"PASS\")\n\n staging_mutated = True"},{"line":845,"text":" restore_old(\"staging\")\n import_addon(\"staging\")\n verify_old(\"staging\", s_error, s_old_templates)\n staging_mutated = False\n emit(\"PORTAL200_STAGING_ROLLBACK_PROOF\", \"PASS\")\n\n staging_mutated = True\n install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n staging_final_new = True\n emit(\"PORTAL200_STAGING_FINAL\", \"PASS\")\n"},{"line":848,"text":" staging_mutated = False\n emit(\"PORTAL200_STAGING_ROLLBACK_PROOF\", \"PASS\")\n\n staging_mutated = True\n install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n staging_final_new = True\n emit(\"PORTAL200_STAGING_FINAL\", \"PASS\")\n\n for rel in EXPECTED_NEW:\n dest = GIT_PORTAL / rel\n old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None"},{"line":852,"text":" install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n staging_final_new = True\n emit(\"PORTAL200_STAGING_FINAL\", \"PASS\")\n\n for rel in EXPECTED_NEW:\n dest = GIT_PORTAL / rel\n old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None\n dest.parent.mkdir(parents=True, exist_ok=True)\n shutil.copyfile(CAND_ROOT / rel, dest)\n git_files_changed = True\n"},{"line":858,"text":" for rel in EXPECTED_NEW:\n dest = GIT_PORTAL / rel\n old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None\n dest.parent.mkdir(parents=True, exist_ok=True)\n shutil.copyfile(CAND_ROOT / rel, dest)\n git_files_changed = True\n\n if git(\"diff\", \"--check\"):\n fail(\"GIT_DIFF_CHECK\")\n status_lines = git(\"status\", \"--porcelain=v1\", \"--untracked-files=all\").splitlines()\n actual_paths = sorted(line[3:] for line in status_lines if len(line) >= 4)\n if actual_paths != sorted(EXPECTED_GIT_PATHS):\n fail(\"GIT_SCOPE\")"},{"line":885,"text":" if remote_head != new_head:\n fail(\"GIT_REMOTE_HEAD\")\n push_confirmed = True\n emit(\n \"PORTAL200_GIT_PROMOTION\",\n {\n \"old_head\": OLD_HEAD,\n \"new_head\": new_head,\n \"remote_head\": remote_head,\n \"result\": \"PASS\",\n },\n )\n"},{"line":902,"text":" fail(\"PROD_ERROR_DRIFT\")\n guest_backup(\"prod\")\n prod_backup = True\n prod_mutated = True\n install_candidate(\"prod\")\n import_addon(\"prod\")\n verify_new(\"prod\", p_error)\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n\n sf = verify_new(\"staging\", s_error)\n pf = verify_new(\"prod\", p_error)\n if sf[\"source\"] != pf[\"source\"]:\n fail(\"FINAL_SOURCE_PARITY\")"},{"line":905,"text":" prod_mutated = True\n install_candidate(\"prod\")\n import_addon(\"prod\")\n verify_new(\"prod\", p_error)\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n\n sf = verify_new(\"staging\", s_error)\n pf = verify_new(\"prod\", p_error)\n if sf[\"source\"] != pf[\"source\"]:\n fail(\"FINAL_SOURCE_PARITY\")\n if sf[\"templates\"] != pf[\"templates\"]:\n fail(\"FINAL_TEMPLATE_PARITY\")\n if git(\"rev-parse\", \"HEAD\") != new_head:"},{"line":922,"text":" if git(\"status\", \"--porcelain=v1\", \"--untracked-files=all\"):\n fail(\"FINAL_GIT_DIRTY\")\n cluster_guard()\n emit(\n \"PORTAL200_FINAL_EVIDENCE\",\n {\n \"commit\": new_head,\n \"source_files\": 3,\n \"templates\": 2,\n \"staging_error_baseline\": s_error,\n \"prod_error_baseline\": p_error,\n },\n )"},{"line":931,"text":" \"staging_error_baseline\": s_error,\n \"prod_error_baseline\": p_error,\n },\n )\n emit(\"PORTAL200_PORTAL\", \"PASS\")\n emit(\"PORTAL200_STAGING\", \"PASS\")\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n emit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\", 21)\n result_contract(\"OK\", True, False, None)\n emit(\"NEWFI200_END\")\n raise SystemExit(0)\n\nexcept PortalError as exc:"},{"line":932,"text":" \"prod_error_baseline\": p_error,\n },\n )\n emit(\"PORTAL200_PORTAL\", \"PASS\")\n emit(\"PORTAL200_STAGING\", \"PASS\")\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n emit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\", 21)\n result_contract(\"OK\", True, False, None)\n emit(\"NEWFI200_END\")\n raise SystemExit(0)\n\nexcept PortalError as exc:\n code = str(exc)"},{"line":933,"text":" },\n )\n emit(\"PORTAL200_PORTAL\", \"PASS\")\n emit(\"PORTAL200_STAGING\", \"PASS\")\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n emit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\", 21)\n result_contract(\"OK\", True, False, None)\n emit(\"NEWFI200_END\")\n raise SystemExit(0)\n\nexcept PortalError as exc:\n code = str(exc)\n emit(\"PORTAL200_FAILURE\", code)"},{"line":934,"text":" )\n emit(\"PORTAL200_PORTAL\", \"PASS\")\n emit(\"PORTAL200_STAGING\", \"PASS\")\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n emit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\", 21)\n result_contract(\"OK\", True, False, None)\n emit(\"NEWFI200_END\")\n raise SystemExit(0)\n\nexcept PortalError as exc:\n code = str(exc)\n emit(\"PORTAL200_FAILURE\", code)\n"},{"line":941,"text":" raise SystemExit(0)\n\nexcept PortalError as exc:\n code = str(exc)\n emit(\"PORTAL200_FAILURE\", code)\n\n if push_confirmed:\n if prod_mutated and prod_backup:\n failure_rollback_started = True\n try:\n restore_old(\"prod\")\n import_addon(\"prod\")\n verify_old(\"prod\", p_error, p_old_templates, strict_errors=False)"},{"line":952,"text":" import_addon(\"prod\")\n verify_old(\"prod\", p_error, p_old_templates, strict_errors=False)\n prod_mutated = False\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_PROD_ROLLBACK\", \"PASS_PARTIAL_OVERALL_STATE_CHANGED\")\n except Exception:\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_PROD_ROLLBACK\", \"FAIL\")\n else:\n if staging_mutated and staging_backup:\n failure_rollback_started = True\n try:\n restore_old(\"staging\")"},{"line":955,"text":" failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_PROD_ROLLBACK\", \"PASS_PARTIAL_OVERALL_STATE_CHANGED\")\n except Exception:\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_PROD_ROLLBACK\", \"FAIL\")\n else:\n if staging_mutated and staging_backup:\n failure_rollback_started = True\n try:\n restore_old(\"staging\")\n import_addon(\"staging\")\n verify_old(\"staging\", s_error, s_old_templates, strict_errors=False)\n staging_mutated = False"},{"line":966,"text":" verify_old(\"staging\", s_error, s_old_templates, strict_errors=False)\n staging_mutated = False\n staging_final_new = False\n failure_rollback_restored = True\n emit(\"PORTAL200_FAILURE_STAGING_ROLLBACK\", \"PASS\")\n except Exception:\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_STAGING_ROLLBACK\", \"FAIL\")\n\n if git_files_changed and not commit_created:\n try:\n for rel, old in old_git_bytes.items():\n dest = GIT_PORTAL / rel"},{"line":969,"text":" failure_rollback_restored = True\n emit(\"PORTAL200_FAILURE_STAGING_ROLLBACK\", \"PASS\")\n except Exception:\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_STAGING_ROLLBACK\", \"FAIL\")\n\n if git_files_changed and not commit_created:\n try:\n for rel, old in old_git_bytes.items():\n dest = GIT_PORTAL / rel\n if old is None:\n if dest.exists():\n dest.unlink()"},{"line":979,"text":" if old is None:\n if dest.exists():\n dest.unlink()\n else:\n dest.write_bytes(old)\n service_dir = GIT_PORTAL / \"Service\"\n try:\n service_dir.rmdir()\n except OSError:\n pass\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"PASS\")\n except Exception:\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"FAIL\")"},{"line":985,"text":" try:\n service_dir.rmdir()\n except OSError:\n pass\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"PASS\")\n except Exception:\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"FAIL\")\n\n if commit_created:\n emit(\n \"PORTAL200_FAILURE_GIT_HISTORY\",\n \"COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED\",\n )"},{"line":987,"text":" except OSError:\n pass\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"PASS\")\n except Exception:\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"FAIL\")\n\n if commit_created:\n emit(\n \"PORTAL200_FAILURE_GIT_HISTORY\",\n \"COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED\",\n )\n\n persisted = bool(push_confirmed or commit_created or staging_final_new or prod_mutated)"},{"line":991,"text":" emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"FAIL\")\n\n if commit_created:\n emit(\n \"PORTAL200_FAILURE_GIT_HISTORY\",\n \"COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED\",\n )\n\n persisted = bool(push_confirmed or commit_created or staging_final_new or prod_mutated)\n if failure_rollback_started and failure_rollback_restored is None:\n failure_rollback_restored = False\n result_contract(\n \"FAIL\","},{"line":1004,"text":" persisted,\n failure_rollback_started,\n failure_rollback_restored,\n )\n emit(\"PORTAL200_ROADMAP_REMAINS\", 22)\n emit(\"NEWFI200_END\")\n raise SystemExit(1)\n\nexcept Exception:\n emit(\"PORTAL200_FAILURE\", \"UNEXPECTED_EXCEPTION\")\n result_contract(\n \"FAIL\",\n bool(push_confirmed or commit_created or staging_final_new or prod_mutated),"},{"line":1009,"text":" emit(\"NEWFI200_END\")\n raise SystemExit(1)\n\nexcept Exception:\n emit(\"PORTAL200_FAILURE\", \"UNEXPECTED_EXCEPTION\")\n result_contract(\n \"FAIL\",\n bool(push_confirmed or commit_created or staging_final_new or prod_mutated),\n False,\n None,\n )\n emit(\"PORTAL200_ROADMAP_REMAINS\", 22)\n emit(\"NEWFI200_END\")"},{"line":1016,"text":" bool(push_confirmed or commit_created or staging_final_new or prod_mutated),\n False,\n None,\n )\n emit(\"PORTAL200_ROADMAP_REMAINS\", 22)\n emit(\"NEWFI200_END\")\n raise SystemExit(1)"}] +RELEVANT_FUNCTIONS=[{"lineno":400,"name":"parse_inspect","sha256":"7b72b62a0ff4c617b7175627d8630294160a6c129f3acfb0d109b4c72c2e8fe2","source":"def parse_inspect(text):\n b = \"PORTAL200_INSPECT_JSON_BEGIN\\n\"\n e = \"\\nPORTAL200_INSPECT_JSON_END\"\n if b not in text or e not in text:\n fail(\"INSPECT_MARKERS\")\n try:\n return json.loads(text.split(b, 1)[1].split(e, 1)[0])\n except Exception:\n fail(\"INSPECT_JSON\")"},{"lineno":447,"name":"app_contract","sha256":"a3a06560de8f2db42e0c3bafc27ad29ee985fd5bf13b01c527a283731ed7904c","source":"def app_contract(data, target):\n a = data.get(\"application\", {})\n addon = data.get(\"addon\") or {}\n expected = {\n \"staging\": (\"0\", \"http://newfi-staging.gram1.ru\"),\n \"prod\": (\"1\", \"https://newfi.ru\"),\n }[target]\n return (\n a.get(\"boardActive\") == expected[0]\n and a.get(\"boardUrl\") == expected[1]\n and a.get(\"indexRoute\") == \"portal/\"\n and a.get(\"defaultStyleId\") == \"4\"\n and addon.get(\"addon_id\") == \"Newfi/Portal\"\n and int(addon.get(\"version_id\", 0)) == 1000010\n and addon.get(\"version_string\") == \"1.0.0 Alpha 1\"\n and int(addon.get(\"active\", 0)) == 1\n and int(addon.get(\"is_processing\", 1)) == 0\n )"},{"lineno":480,"name":"assert_no_new_errors","sha256":"801839c1705804fed1a507709263d2882a332f0276b42a7c5439204b99428dfb","source":"def assert_no_new_errors(target, baseline, phase):\n rows = new_errors(target, baseline)\n if rows:\n emit(\n \"PORTAL200_NEW_XF_ERRORS\",\n {\n \"target\": target,\n \"phase\": phase,\n \"count\": len(rows),\n \"ids\": [r.get(\"error_id\") for r in rows],\n },\n )\n fail(\"NEW_XF_ERRORS\")"},{"lineno":495,"name":"guest_backup","sha256":"3e0d88922bf3d3420512f69cd0e50232e0325391cebb99845ed1c8c206ec08a1","source":"def guest_backup(target):\n script = f'''set -eu\nB={shlex.quote(BACKUP)}\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest ! -e \"$B\"\ninstall -d -m 700 \"$B/Pub/Controller\" \"$B/_data\"\ncp -p -- \"$P/Pub/Controller/Portal.php\" \"$B/Pub/Controller/Portal.php\"\ncp -p -- \"$P/_data/templates.xml\" \"$B/_data/templates.xml\"\ntest ! -e \"$P/Service/PortalData.php\"\nprintf 'PORTAL200_BACKUP=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_BACKUP=PASS\" not in out:\n fail(\"BACKUP_FAILED\")"},{"lineno":511,"name":"guest_write","sha256":"f5f8ed7e6553eadf19e219ed9cab5dd6d7b9ad72dca3926e5368ad663f9b2881","source":"def guest_write(target, rel, data):\n dest = f\"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}\"\n parent = str(pathlib.PurePosixPath(dest).parent)\n script = (\n \"set -eu; umask 022; \"\n + \"install -d -m 755 \"\n + shlex.quote(parent)\n + \"; T=$(mktemp \"\n + shlex.quote(parent + \"/.portal200.XXXXXX\")\n + \"); cat >\\\"$T\\\"; chmod 0644 \\\"$T\\\"; chown root:root \\\"$T\\\"; \"\n + \"mv -f -- \\\"$T\\\" \"\n + shlex.quote(dest)\n + \"; printf 'PORTAL200_WRITE=PASS\\\\n'\"\n )\n out = qga(\n target,\n [\"/bin/bash\", \"-lc\", script],\n input_bytes=data,\n timeout=90,\n )\n if \"PORTAL200_WRITE=PASS\" not in out:\n fail(\"WRITE_FAILED\")"},{"lineno":535,"name":"install_candidate","sha256":"b46ecffec3c0972b1306ffb057164c1223e08f26bd3121bfde7f624ffad66903","source":"def install_candidate(target):\n for rel in [\n \"Pub/Controller/Portal.php\",\n \"Service/PortalData.php\",\n \"_data/templates.xml\",\n ]:\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())"},{"lineno":544,"name":"restore_old","sha256":"eafa8284ff101e7e3c189922361201ce5684ce970e90e2c933c39b65e25e21da","source":"def restore_old(target):\n script = f'''set -eu\nB={shlex.quote(BACKUP)}\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest -f \"$B/Pub/Controller/Portal.php\"\ntest -f \"$B/_data/templates.xml\"\ncp -p -- \"$B/Pub/Controller/Portal.php\" \"$P/Pub/Controller/Portal.php\"\ncp -p -- \"$B/_data/templates.xml\" \"$P/_data/templates.xml\"\nrm -f -- \"$P/Service/PortalData.php\"\nrmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_SOURCE_RESTORE=PASS\" not in out:\n fail(\"SOURCE_RESTORE_FAILED\")"},{"lineno":561,"name":"import_addon","sha256":"8af6f854e1917ee8e02cfe345f730da0b4b6fbe1b44fdb73ced52c8a3eeebb50","source":"def import_addon(target):\n out = qga(\n target,\n [\"/usr/bin/php8.3\"],\n input_bytes=IMPORT_PHP.encode(),\n timeout=240,\n )\n if \"PORTAL200_IMPORT_JSON=\" not in out:\n fail(\"IMPORT_FAILED\")"},{"lineno":572,"name":"runtime_verify","sha256":"cbe3a884afd8bb310ce9d055ddaf74a9923ac55e975ad5a9c3fbb0b910776497","source":"def runtime_verify(target, state):\n src = RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", state)\n out = qga(target, [\"/usr/bin/php8.3\"], input_bytes=src.encode(), timeout=120)\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_RUNTIME_JSON=\")),\n None,\n )\n if not line:\n fail(\"RUNTIME_MARKER_MISSING\")\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n except Exception:\n fail(\"RUNTIME_JSON\")\n emit(\"PORTAL200_RUNTIME\", {\"target\": target, **data})\n return data"},{"lineno":589,"name":"http_verify","sha256":"75eb5fbec1862b5d926cbc91519c284218a4bb05db9edf616422fa187b8348a6","source":"def http_verify(target, state):\n argv = [\"/usr/bin/python3\", \"-\", target, state]\n if target == \"prod\":\n try:\n p = subprocess.run(\n argv,\n input=HTTP_VERIFY_PY.encode(),\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n timeout=180,\n check=False,\n )\n except subprocess.TimeoutExpired:\n fail(\"HTTP_PROD_TIMEOUT\")\n out = p.stdout.decode(\"utf-8\", \"replace\")\n err = p.stderr.decode(\"utf-8\", \"replace\")\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_HTTP_JSON=\")),\n None,\n )\n data = None\n if line:\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\n except Exception:\n fail(\"HTTP_JSON\")\n if p.returncode != 0:\n emit(\n \"PORTAL200_HTTP_EXEC\",\n {\n \"target\": target,\n \"state\": state,\n \"rc\": p.returncode,\n \"stderr\": err[:800],\n },\n )\n fail(\"HTTP_PROD_RC\")\n if data is None:\n fail(\"HTTP_MARKER_MISSING\")\n return data\n\n out = qga(\n target,\n argv,\n input_bytes=HTTP_VERIFY_PY.encode(),\n timeout=180,\n )\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_HTTP_JSON=\")),\n None,\n )\n if not line:\n fail(\"HTTP_MARKER_MISSING\")\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n except Exception:\n fail(\"HTTP_JSON\")\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\n return data"},{"lineno":692,"name":"cluster_guard","sha256":"bd6547918004a2f74ae4ac8cc7d7caf80b4ced2bcf57cc98ddc454561121dcac","source":"def cluster_guard():\n p = run([\"/usr/sbin/qm\", \"status\", \"210\"], timeout=30)\n if \"status: running\" not in p.stdout.decode():\n fail(\"VM210_NOT_RUNNING\")\n p = run(\n [\n \"/usr/bin/ssh\",\n \"-T\",\n \"-o\",\n \"BatchMode=yes\",\n \"-o\",\n \"StrictHostKeyChecking=yes\",\n \"-o\",\n \"UpdateHostKeys=no\",\n \"-o\",\n \"ConnectTimeout=10\",\n \"root@pve03\",\n \"/usr/sbin/qm status 211\",\n ],\n timeout=40,\n )\n if \"status: running\" not in p.stdout.decode():\n fail(\"VM211_NOT_RUNNING\")\n emit(\"PORTAL200_CLUSTER\", \"PASS\")"}] +LITERAL_PAYLOADS=[{"bytes":2008,"lineno":147,"names":["INSPECT_PHP"],"sha256":"8de3d66a7adfca4c179db55bb5c4e956f6750fb41e9e058b449efeb214cbd7f1","value":"db();\nfunction srcMeta(string $path): ?array {\n if (!is_file($path)) { return null; }\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\n}\n$portal = $root . '/src/addons/Newfi/Portal';\n$templates = $db->fetchAll(\n \"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \" .\n \"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \" .\n \"LOCATE('--newfi-portal200', template) > 0 AS css_marker \" .\n \"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\"\n);\n$result = [\n 'hostname' => gethostname(),\n 'application' => [\n 'boardActive' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\"),\n 'boardUrl' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\"),\n 'indexRoute' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\"),\n 'defaultStyleId' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\"),\n 'error_count' => (int)$db->fetchOne(\"SELECT COUNT(*) FROM xf_error_log\"),\n 'error_max_id' => (int)$db->fetchOne(\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\")\n ],\n 'addon' => $db->fetchRow(\n \"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \" .\n \"FROM xf_addon WHERE addon_id='Newfi/Portal'\"\n ),\n 'source' => [\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n ],\n 'templates' => $templates\n];\necho \"PORTAL200_INSPECT_JSON_BEGIN\\n\";\necho json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\necho \"\\nPORTAL200_INSPECT_JSON_END\\n\";\n?>"},{"bytes":1429,"lineno":194,"names":["IMPORT_PHP"],"sha256":"8f4109ed896f29035cfa5534751507efff2786d4d6d18840adf30b84771a18e8","value":"addOnManager()->getById('Newfi/Portal');\nif (!$addOn || !$addOn->isInstalled()) {\n fwrite(STDERR, \"ADDON_NOT_INSTALLED\\n\");\n exit(30);\n}\n$addOn->preRebuild();\n$dataManager = $app->addOnDataManager();\n$imported = [];\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\n $handler = $dataManager->getDataTypeHandler($dataType);\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\n if (!$xml || !$xml->count()) {\n $handler->deleteAddOnData($addOn->addon_id);\n continue;\n }\n $start = 0;\n $guard = 0;\n while (true) {\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\n if (!$last) { break; }\n if ($last <= $start || ++$guard > 100000) {\n fwrite(STDERR, \"IMPORT_PROGRESS_INVALID\\n\");\n exit(31);\n }\n $start = $last;\n }\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\n \\XF::triggerRunOnce();\n $imported[] = $handler->getContainerTag();\n}\n$addOn->postDataImport();\n$repo = $app->repository(\\XF\\Repository\\StyleRepository::class);\n$repo->updateAllStylesLastModifiedDateLater();\n$addOn->postRebuild();\necho \"PORTAL200_IMPORT_JSON=\" . json_encode(\n ['result'=>'PASS','containers'=>$imported],\n JSON_UNESCAPED_SLASHES\n) . \"\\n\";\n?>"},{"bytes":410,"lineno":240,"names":["ERRORS_PHP_TEMPLATE"],"sha256":"ee412cafa99d7d809c8b682db8ebb0bc2958ab0cbc9e37537e22a78f10489635","value":"db();\n$b=%d;\n$rows=$db->fetchAll(\n \"SELECT error_id,exception_type,message,filename,line FROM xf_error_log WHERE error_id>? ORDER BY error_id\",\n [$b]\n);\necho json_encode(\n $rows,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\n?>"},{"bytes":2501,"lineno":258,"names":["RUNTIME_VERIFY_PHP_TEMPLATE"],"sha256":"884c03648203992cf6d5b96be8342243739b0412fdff55f4a6b64cf96aad3313","value":"$state];\nif ($state === 'new') {\n $service = new \\Newfi\\Portal\\Service\\PortalData($app);\n $portal = $service->build();\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\n $result['sections'] = [];\n foreach (['forums','resources','media','events','dogs'] as $key) {\n $section = $portal[$key] ?? null;\n if (!is_array($section)) { fwrite(STDERR, \"SECTION_MISSING\\n\"); exit(60); }\n $result['sections'][$key] = [\n 'available'=>(bool)($section['available'] ?? false),\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\n 'status'=>(string)($section['status'] ?? '')\n ];\n }\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\"1\"') !== false;\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\"resources\"') !== false;\n $result['locked_media'] = strpos($html, 'data-portal-locked=\"media\"') !== false;\n $result['html_bytes'] = strlen($html);\n if (!$result['html_marker']) { fwrite(STDERR, \"HTML_MARKER_MISSING\\n\"); exit(61); }\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\n fwrite(STDERR, \"GUEST_PERMISSION_CONTRACT_CHANGED\\n\"); exit(62);\n }\n if (!$result['locked_resources'] || !$result['locked_media']) {\n fwrite(STDERR, \"LOCKED_COPY_MISSING\\n\"); exit(63);\n }\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\n fwrite(STDERR, \"PUBLIC_SECTION_UNAVAILABLE\\n\"); exit(64);\n }\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\n $result['html_bytes'] = strlen($html);\n if ($result['html_marker'] || !$result['old_copy']) {\n fwrite(STDERR, \"OLD_RENDER_CONTRACT\\n\"); exit(65);\n }\n} else {\n exit(66);\n}\necho \"PORTAL200_RUNTIME_JSON=\" . json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n) . \"\\n\";\n?>"},{"bytes":3277,"lineno":311,"names":["HTTP_VERIFY_PY"],"sha256":"15b9839d95798833aef597b59787d301155d32cfb404b14ca7e0bf7d83a1a71e","value":"import html,json,re,subprocess,sys,urllib.parse\nmode=sys.argv[1]\nstate=sys.argv[2]\nif mode=='staging':\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\nelif mode=='prod':\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\nelse:\n raise SystemExit(40)\ndef fetch(url):\n cmd=[\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\n ]\n if mode=='staging':\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\n cmd += [\n '-o','-','-w','\\n__PORTAL_STATUS__%{http_code}',url\n ]\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\n if p.returncode!=0: raise SystemExit(41)\n text=p.stdout.decode('utf-8','replace')\n marker='\\n__PORTAL_STATUS__'\n if marker not in text: raise SystemExit(42)\n body,status=text.rsplit(marker,1)\n return int(status.strip()),body\nroot=f'{scheme}://{host}/'\nstatus,body=fetch(root)\nhrefs=[]\nfor raw in re.findall(r\"href=[\\\"']([^\\\"']*css\\.php[^\\\"']*)[\\\"']\",body,re.I):\n raw=html.unescape(raw)\n hrefs.append(urllib.parse.urljoin(root,raw))\nnormal_css=[]\nnormal_status=[]\nfor u in hrefs:\n s,b=fetch(u)\n normal_status.append(s)\n normal_css.append(b)\nnormal_css_text='\\n'.join(normal_css)\nforced_css_marker=None\nforced_css_status=None\nif mode=='staging' and state=='new' and hrefs:\n u=urllib.parse.urlsplit(hrefs[0])\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\n out=[]\n found=False\n for k,v in pairs:\n if k=='css':\n found=True\n parts=[x for x in v.split(',') if x]\n if 'public:newfi_portal.less' not in parts:\n parts.append('public:newfi_portal.less')\n v=','.join(parts)\n out.append((k,v))\n if found:\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\n forced_css_status,forced_body=fetch(forced)\n forced_css_marker='--newfi-portal200' in forced_body\nresult={\n 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\n}\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\nif status!=expected_status: raise SystemExit(45)\nif mode=='prod':\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\nelif mode=='staging':\n if any(x!=200 for x in normal_status): raise SystemExit(46)\nelse:\n raise SystemExit(50)\nif mode=='prod' and state=='new':\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\nelif mode=='prod' and state=='old':\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\nelif mode=='staging' and state=='new':\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\nelif mode=='staging' and state=='old':\n pass\nelse:\n raise SystemExit(50)\n"}] +SOURCE_VERSION_IDS=[] +SOURCE_VERSION_STRINGS=[] +AST_RECOVERY_END=true + +OUTPUT_END +CHAT_OUTPUT_END diff --git a/runtime/latest.json b/runtime/latest.json index 0b6b0a59..1b678f48 100644 --- a/runtime/latest.json +++ b/runtime/latest.json @@ -1,19 +1,19 @@ { "schema_version": 1, "channel": "homelab-runtime", - "command_id": "NEWFI-260907-A-PORTAL200-APPLY4DESIGN6", + "command_id": "NEWFI-260907-A-PORTAL200-APPLY4DESIGN7", "status": "OK", "rc": 0, "host": "pve01", "mode": "read-only", - "component": "newfi-portal200-apply4-exact-old-carrier-recovery", - "started_at_utc": "2026-09-07T20:30:30Z", - "finished_at_utc": "2026-09-07T20:30:30Z", + "component": "newfi-portal200-apply4-static-ast-recovery", + "started_at_utc": "2026-09-07T20:36:05Z", + "finished_at_utc": "2026-09-07T20:36:05Z", "reference_register_checked": true, "reference_sha256": "5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66", "error_register_checked": true, "error_register_sha256": "3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0", - "command_sha256": "743ae461d12f19d0da870df76bde76c8a943c1729a9a04890736219912177071", + "command_sha256": "f496660f98e2d5bd57395b705f69565e9b9b4fa3823630155bbe079f561a2876", "duplicate_failed_command_blocked": false, "block_reason": null, "execution_started": true, @@ -30,9 +30,9 @@ "secrets_included": false, "private_addresses_included": false, "raw_evidence_retained_locally": true, - "raw_evidence_sha256": "26388e4b0b4f7fd3220098a4be02fcc57b475838436c8374cae49aec73fea1ac", - "sanitized_output_sha256": "778936e6f9b7e0ce7697c2b9feb8203c185a24c19d7b76e2f58d05caa12ee143", + "raw_evidence_sha256": "b0e750afcc8c28e33530e823fa9ca879ddd22ce66e758296aeb27ffd61aa5d1a", + "sanitized_output_sha256": "b0e750afcc8c28e33530e823fa9ca879ddd22ce66e758296aeb27ffd61aa5d1a", "output_truncated_in_json": false, "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt", - "output": "LOCAL_APPLY3_EXACT_CARRIER_RECOVERY_BEGIN=true\nCOMMAND_ID=NEWFI-260907-A-PORTAL200-APPLY4DESIGN6\nMUTATION_SCOPE=NONE_READ_ONLY\nLOCAL_MUTATIONS_PERFORMED=NO\nTARGET_BASENAME=newfi-54-portal200-apply3.cmd\nTARGET_OLD_COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\nTARGET_MATCH_COUNT=1\nTARGET_FILE_SHA256=573c5c94fe59ed072e094281668ce9847dc4a9882019a2b89d8ba5f190d92176\nTARGET_FILE_BYTES=17209\nTARGET_FILE_CONTAINS_OLD_CID=True\nDECODED_LAYER_COUNT=2\nCANDIDATE_LAYER_COUNT=2\nCANDIDATE_LAYER_1_SOURCE=carrier::b64::zlib\nCANDIDATE_LAYER_1_SHA256=97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184\nCANDIDATE_LAYER_1_BYTES=34155\nCANDIDATE_LAYER_1_RELEVANT_BEGIN\nimport subprocess\nimport sys\nimport xml.etree.ElementTree as ET\n\nCID = \"NEWFI-260907-PORTAL200-APPLY3\"\nBRANCH = \"implementation/newfi-closed-mvp-20260906\"\nOLD_HEAD = \"06bab5acd124e768f70305b3d7eca2f595871890\"\nWT = pathlib.Path(\"/root/newfi-portal/.worktrees/implementation-newfi-closed-mvp-20260906\")\nCAND_ROOT = pathlib.Path(\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\")\nPORTAL_REL = pathlib.Path(\"src/addons/Newfi/Portal\")\nGIT_PORTAL = WT / PORTAL_REL\nEXPECTED_NEW = {\n \"Pub/Controller/Portal.php\": (363, \"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\"),\n \"Service/PortalData.php\": (5213, \"4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d\"),\n \"_data/templates.xml\": (10977, \"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\"),\n}\nEXPECTED_OLD = {\n \"Pub/Controller/Portal.php\": (247, \"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\"),\n \"_data/templates.xml\": (1461, \"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\"),\nEXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)]\nBACKUP = f\"/root/newfi-portal200-backup-{CID}\"\nclass PortalError(RuntimeError):\n pass\ndef emit(key, value=None):\n if value is None:\nfunction srcMeta(string $path): ?array {\n if (!is_file($path)) { return null; }\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\n$portal = $root . '/src/addons/Newfi/Portal';\n$templates = $db->fetchAll(\n \"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \" .\n \"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \" .\n \"LOCATE('--newfi-portal200', template) > 0 AS css_marker \" .\n \"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\"\n);\n$result = [\n 'hostname' => gethostname(),\n 'application' => [\n 'boardActive' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\"),\n 'boardUrl' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\"),\n 'indexRoute' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\"),\n 'defaultStyleId' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\"),\n 'error_count' => (int)$db->fetchOne(\"SELECT COUNT(*) FROM xf_error_log\"),\n 'error_max_id' => (int)$db->fetchOne(\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\")\n ],\n 'addon' => $db->fetchRow(\n \"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \" .\n \"FROM xf_addon WHERE addon_id='Newfi/Portal'\"\n ),\n 'source' => [\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n 'templates' => $templates\n];\necho \"PORTAL200_INSPECT_JSON_BEGIN\\n\";\necho json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\necho \"\\nPORTAL200_INSPECT_JSON_END\\n\";\n?>'''\nIMPORT_PHP = r'''addOnManager()->getById('Newfi/Portal');\nif (!$addOn || !$addOn->isInstalled()) {\n fwrite(STDERR, \"ADDON_NOT_INSTALLED\\n\");\n exit(30);\n$addOn->preRebuild();\n$dataManager = $app->addOnDataManager();\n$imported = [];\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\n$addOn->postDataImport();\n$repo = $app->repository(\\XF\\Repository\\StyleRepository::class);\n$repo->updateAllStylesLastModifiedDateLater();\n$addOn->postRebuild();\necho \"PORTAL200_IMPORT_JSON=\" . json_encode(\n ['result'=>'PASS','containers'=>$imported],\n JSON_UNESCAPED_SLASHES\n) . \"\\n\";\nERRORS_PHP_TEMPLATE = r'''(bool)($section['hasItems'] ?? false),\n 'status'=>(string)($section['status'] ?? '')\n ];\n }\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\"1\"') !== false;\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\"resources\"') !== false;\n $result['locked_media'] = strpos($html, 'data-portal-locked=\"media\"') !== false;\n $result['html_bytes'] = strlen($html);\n if (!$result['html_marker']) { fwrite(STDERR, \"HTML_MARKER_MISSING\\n\"); exit(61); }\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\n fwrite(STDERR, \"GUEST_PERMISSION_CONTRACT_CHANGED\\n\"); exit(62);\n if (!$result['locked_resources'] || !$result['locked_media']) {\n fwrite(STDERR, \"PUBLIC_SECTION_UNAVAILABLE\\n\"); exit(64);\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\n if ($result['html_marker'] || !$result['old_copy']) {\n fwrite(STDERR, \"OLD_RENDER_CONTRACT\\n\"); exit(65);\n} else {\n exit(66);\necho \"PORTAL200_RUNTIME_JSON=\" . json_encode(\nHTTP_VERIFY_PY = r'''import html,json,re,subprocess,sys,urllib.parse\nmode=sys.argv[1]\n out.append((k,v))\n if found:\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\n forced_css_status,forced_body=fetch(forced)\n forced_css_marker='--newfi-portal200' in forced_body\nresult={\n 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\nif status!=expected_status: raise SystemExit(45)\nif mode=='prod':\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\nelif mode=='staging':\n if any(x!=200 for x in normal_status): raise SystemExit(46)\nelse:\n raise SystemExit(50)\nif mode=='prod' and state=='new':\n'''\ndef parse_inspect(text):\n b = \"PORTAL200_INSPECT_JSON_BEGIN\\n\"\n e = \"\\nPORTAL200_INSPECT_JSON_END\"\n if b not in text or e not in text:\n fail(\"INSPECT_MARKERS\")\n try:\n return json.loads(text.split(b, 1)[1].split(e, 1)[0])\n except Exception:\n fail(\"INSPECT_JSON\")\n a.get(\"boardActive\") == expected[0]\n and a.get(\"boardUrl\") == expected[1]\n and a.get(\"indexRoute\") == \"portal/\"\n and a.get(\"defaultStyleId\") == \"4\"\n and addon.get(\"addon_id\") == \"Newfi/Portal\"\n and int(addon.get(\"version_id\", 0)) == 1000010\n and addon.get(\"version_string\") == \"1.0.0 Alpha 1\"\n and int(addon.get(\"active\", 0)) == 1\n and int(addon.get(\"is_processing\", 1)) == 0\n )\ndef new_errors(target, baseline):\n raw = qga(\n target,\ndef assert_no_new_errors(target, baseline, phase):\n rows = new_errors(target, baseline)\n if rows:\n emit(\n \"PORTAL200_NEW_XF_ERRORS\",\n {\n \"target\": target,\n \"phase\": phase,\n \"count\": len(rows),\n \"ids\": [r.get(\"error_id\") for r in rows],\n },\n )\n fail(\"NEW_XF_ERRORS\")\ndef guest_backup(target):\n script = f'''set -eu\nB={shlex.quote(BACKUP)}\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest ! -e \"$B\"\ninstall -d -m 700 \"$B/Pub/Controller\" \"$B/_data\"\ncp -p -- \"$P/Pub/Controller/Portal.php\" \"$B/Pub/Controller/Portal.php\"\ncp -p -- \"$P/_data/templates.xml\" \"$B/_data/templates.xml\"\ntest ! -e \"$P/Service/PortalData.php\"\nprintf 'PORTAL200_BACKUP=PASS\\n'\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_BACKUP=PASS\" not in out:\n fail(\"BACKUP_FAILED\")\ndef guest_write(target, rel, data):\n dest = f\"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}\"\n parent = str(pathlib.PurePosixPath(dest).parent)\n script = (\n \"set -eu; umask 022; \"\n + \"install -d -m 755 \"\n + shlex.quote(parent)\n + \"; T=$(mktemp \"\n + shlex.quote(parent + \"/.portal200.XXXXXX\")\n + \"); cat >\\\"$T\\\"; chmod 0644 \\\"$T\\\"; chown root:root \\\"$T\\\"; \"\n + \"mv -f -- \\\"$T\\\" \"\n + shlex.quote(dest)\n + \"; printf 'PORTAL200_WRITE=PASS\\\\n'\"\n out = qga(\n [\"/bin/bash\", \"-lc\", script],\n input_bytes=data,\n timeout=90,\n if \"PORTAL200_WRITE=PASS\" not in out:\n fail(\"WRITE_FAILED\")\ndef install_candidate(target):\n for rel in [\n \"Pub/Controller/Portal.php\",\n \"Service/PortalData.php\",\n \"_data/templates.xml\",\n ]:\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\ndef restore_old(target):\ntest -f \"$B/Pub/Controller/Portal.php\"\ntest -f \"$B/_data/templates.xml\"\ncp -p -- \"$B/Pub/Controller/Portal.php\" \"$P/Pub/Controller/Portal.php\"\ncp -p -- \"$B/_data/templates.xml\" \"$P/_data/templates.xml\"\nrm -f -- \"$P/Service/PortalData.php\"\nrmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\n if \"PORTAL200_SOURCE_RESTORE=PASS\" not in out:\n fail(\"SOURCE_RESTORE_FAILED\")\ndef import_addon(target):\n [\"/usr/bin/php8.3\"],\n input_bytes=IMPORT_PHP.encode(),\n timeout=240,\n if \"PORTAL200_IMPORT_JSON=\" not in out:\n fail(\"IMPORT_FAILED\")\ndef runtime_verify(target, state):\n src = RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", state)\n out = qga(target, [\"/usr/bin/php8.3\"], input_bytes=src.encode(), timeout=120)\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_RUNTIME_JSON=\")),\n None,\n if not line:\n fail(\"RUNTIME_MARKER_MISSING\")\n data = json.loads(line.split(\"=\", 1)[1])\n fail(\"RUNTIME_JSON\")\n emit(\"PORTAL200_RUNTIME\", {\"target\": target, **data})\n return data\ndef http_verify(target, state):\n argv = [\"/usr/bin/python3\", \"-\", target, state]\n if target == \"prod\":\n try:\n p = subprocess.run(\n fail(\"HTTP_PROD_TIMEOUT\")\n out = p.stdout.decode(\"utf-8\", \"replace\")\n err = p.stderr.decode(\"utf-8\", \"replace\")\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_HTTP_JSON=\")),\n None,\n data = None\n if line:\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\n except Exception:\n fail(\"HTTP_JSON\")\n if p.returncode != 0:\n emit(\n \"PORTAL200_HTTP_EXEC\",\n {\n \"target\": target,\n \"state\": state,\n \"rc\": p.returncode,\n \"stderr\": err[:800],\n },\n )\n fail(\"HTTP_PROD_RC\")\n input_bytes=HTTP_VERIFY_PY.encode(),\n timeout=180,\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_HTTP_JSON=\")),\n fail(\"HTTP_MARKER_MISSING\")\n fail(\"HTTP_JSON\")\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\ndef verify_new(target, baseline):\n data = inspect(target)\n if not app_contract(data, target):\n fail(\"APP_CONTRACT_NEW\")\n if not source_is_new(data):\n timeout=40,\n if \"status: running\" not in p.stdout.decode():\n fail(\"VM211_NOT_RUNNING\")\n emit(\"PORTAL200_CLUSTER\", \"PASS\")\ndef result_contract(status, changes, rollback_started, rollback_restored):\n emit(\n \"HOMELAB_RESULT_CONTRACT\",\n {\n \"version\": 1,\n \"command_id\": CID,\nemit(\"NEWFI200_BEGIN\")\nemit(\"COMMAND_ID\", CID)\nemit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\", 22)\nemit(\n \"PORTAL200_FLOW\",\n \"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\",\n)\nstaging_backup = False\nstaging_mutated = False\nstaging_final_new = False\nprod_backup = False\nprod_mutated = False\ngit_files_changed = False\ns_old_templates = None\np_old_templates = None\ntry:\n compile(HTTP_VERIFY_PY, \"\", \"exec\")\n for rel, (size, sha) in EXPECTED_NEW.items():\n p = CAND_ROOT / rel\n if not p.is_file() or file_meta(p) != (size, sha):\n fail(\"CANDIDATE_IDENTITY\")\n tree = ET.parse(CAND_ROOT / \"_data/templates.xml\")\n titles = sorted(x.attrib.get(\"title\") for x in tree.getroot().findall(\"template\"))\n if titles != [\"newfi_portal\", \"newfi_portal.less\"]:\n fail(\"CANDIDATE_TEMPLATE_SET\")\n service = (CAND_ROOT / \"Service/PortalData.php\").read_text(\"utf-8\")\n if any(\n token in service\n for token in [\n \"\\\\XF::db(\",\n \"->save(\",\n \"->delete(\",\n \"INSERT \",\n \"DELETE \",\n \"REPLACE \",\n ]\n ):\n fail(\"CANDIDATE_MUTATION_SCAN\")\n emit(\"PORTAL200_CANDIDATE_IDENTITY\", \"PASS\")\n \"PORTAL200_ORCHESTRATION_FIXES\",\n \"staging_503_without_css_links\": True,\n \"failure_status\": \"FAIL\",\n \"prod_http_via_pve01_public_route\": True,\n \"css_href_regex_fixed\": True,\n },\n remote = remote_out.split()[0] if remote_out else \"\"\n if branch != BRANCH or head != OLD_HEAD or remote != OLD_HEAD or status:\n fail(\"GIT_PRESTATE\")\n \"PORTAL200_GIT_PRESTATE\",\n {\"branch\": branch, \"head\": head, \"remote\": remote, \"clean\": True},\n cluster_guard()\n lint_php(\"staging\", INSPECT_PHP, \"INSPECT_STAGING\")\n lint_php(\"staging\", IMPORT_PHP, \"IMPORT_STAGING\")\n lint_php(\"staging\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\"), \"RUNTIME_STAGING\")\n lint_php(\"prod\", INSPECT_PHP, \"INSPECT_PROD\")\n lint_php(\"prod\", IMPORT_PHP, \"IMPORT_PROD\")\n lint_php(\"prod\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\"), \"RUNTIME_PROD\")\n emit(\"PORTAL200_RUNTIME_HELPER_LINT\", \"PASS\")\n s0 = inspect(\"staging\")\n p0 = inspect(\"prod\")\n if not app_contract(s0, \"staging\") or not app_contract(p0, \"prod\"):\n fail(\"APP_PRESTATE\")\n if not source_is_old(s0) or not source_is_old(p0):\n fail(\"SOURCE_PRESTATE\")\n if s0.get(\"templates\") != p0.get(\"templates\"):\n s_error = int(s0[\"application\"][\"error_max_id\"])\n p_error = int(p0[\"application\"][\"error_max_id\"])\n s_old_templates = s0[\"templates\"]\n p_old_templates = p0[\"templates\"]\n emit(\"PORTAL200_ERROR_BASELINE\", {\"staging\": s_error, \"prod\": p_error})\n http_verify(\"staging\", \"old\")\n runtime_verify(\"prod\", \"old\")\n http_verify(\"prod\", \"old\")\n assert_no_new_errors(\"staging\", s_error, \"preflight\")\n assert_no_new_errors(\"prod\", p_error, \"preflight\")\n emit(\"PORTAL200_PREFLIGHT\", \"PASS\")\n guest_backup(\"staging\")\n staging_backup = True\n staging_mutated = True\n install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n emit(\"PORTAL200_STAGING_FIRST_APPLY\", \"PASS\")\n restore_old(\"staging\")\n verify_old(\"staging\", s_error, s_old_templates)\n staging_mutated = False\n emit(\"PORTAL200_STAGING_ROLLBACK_PROOF\", \"PASS\")\n staging_final_new = True\n emit(\"PORTAL200_STAGING_FINAL\", \"PASS\")\n for rel in EXPECTED_NEW:\n dest = GIT_PORTAL / rel\n old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None\n dest.parent.mkdir(parents=True, exist_ok=True)\n shutil.copyfile(CAND_ROOT / rel, dest)\n git_files_changed = True\n if remote_head != new_head:\n fail(\"GIT_REMOTE_HEAD\")\n push_confirmed = True\n \"PORTAL200_GIT_PROMOTION\",\n \"old_head\": OLD_HEAD,\n \"new_head\": new_head,\n \"remote_head\": remote_head,\n \"result\": \"PASS\",\n fail(\"PROD_ERROR_DRIFT\")\n guest_backup(\"prod\")\n prod_backup = True\n prod_mutated = True\n install_candidate(\"prod\")\n import_addon(\"prod\")\n verify_new(\"prod\", p_error)\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n sf = verify_new(\"staging\", s_error)\n pf = verify_new(\"prod\", p_error)\n if sf[\"source\"] != pf[\"source\"]:\n fail(\"FINAL_SOURCE_PARITY\")\n if sf[\"templates\"] != pf[\"templates\"]:\n fail(\"FINAL_TEMPLATE_PARITY\")\n if git(\"rev-parse\", \"HEAD\") != new_head:\n if git(\"status\", \"--porcelain=v1\", \"--untracked-files=all\"):\n fail(\"FINAL_GIT_DIRTY\")\n \"PORTAL200_FINAL_EVIDENCE\",\n \"commit\": new_head,\n \"source_files\": 3,\n \"templates\": 2,\n \"staging_error_baseline\": s_error,\n \"prod_error_baseline\": p_error,\n emit(\"PORTAL200_PORTAL\", \"PASS\")\n emit(\"PORTAL200_STAGING\", \"PASS\")\n emit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\", 21)\n result_contract(\"OK\", True, False, None)\n emit(\"NEWFI200_END\")\n raise SystemExit(0)\nexcept PortalError as exc:\n code = str(exc)\n emit(\"PORTAL200_FAILURE\", code)\n if push_confirmed:\n if prod_mutated and prod_backup:\n failure_rollback_started = True\n restore_old(\"prod\")\n import_addon(\"prod\")\n verify_old(\"prod\", p_error, p_old_templates, strict_errors=False)\n prod_mutated = False\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_PROD_ROLLBACK\", \"PASS_PARTIAL_OVERALL_STATE_CHANGED\")\n emit(\"PORTAL200_FAILURE_PROD_ROLLBACK\", \"FAIL\")\n else:\n if staging_mutated and staging_backup:\n restore_old(\"staging\")\n import_addon(\"staging\")\n verify_old(\"staging\", s_error, s_old_templates, strict_errors=False)\n staging_mutated = False\n staging_final_new = False\n failure_rollback_restored = True\n emit(\"PORTAL200_FAILURE_STAGING_ROLLBACK\", \"PASS\")\n emit(\"PORTAL200_FAILURE_STAGING_ROLLBACK\", \"FAIL\")\n if git_files_changed and not commit_created:\n for rel, old in old_git_bytes.items():\n dest = GIT_PORTAL / rel\n if old is None:\n if dest.exists():\n dest.unlink()\n try:\n service_dir.rmdir()\n except OSError:\n pass\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"PASS\")\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"FAIL\")\n if commit_created:\n \"PORTAL200_FAILURE_GIT_HISTORY\",\n \"COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED\",\n persisted = bool(push_confirmed or commit_created or staging_final_new or prod_mutated)\n if failure_rollback_started and failure_rollback_restored is None:\n failure_rollback_restored = False\n result_contract(\n \"FAIL\",\n persisted,\n failure_rollback_started,\n failure_rollback_restored,\n emit(\"PORTAL200_ROADMAP_REMAINS\", 22)\n raise SystemExit(1)\nexcept Exception:\n emit(\"PORTAL200_FAILURE\", \"UNEXPECTED_EXCEPTION\")\n bool(push_confirmed or commit_created or staging_final_new or prod_mutated),\n False,\nCANDIDATE_LAYER_1_RELEVANT_END\nCANDIDATE_LAYER_1_FULL_BEGIN\nimport hashlib\nimport json\nimport pathlib\nimport shlex\nimport shutil\nimport subprocess\nimport sys\nimport xml.etree.ElementTree as ET\n\nCID = \"NEWFI-260907-PORTAL200-APPLY3\"\nBRANCH = \"implementation/newfi-closed-mvp-20260906\"\nOLD_HEAD = \"06bab5acd124e768f70305b3d7eca2f595871890\"\nWT = pathlib.Path(\"/root/newfi-portal/.worktrees/implementation-newfi-closed-mvp-20260906\")\nCAND_ROOT = pathlib.Path(\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\")\nPORTAL_REL = pathlib.Path(\"src/addons/Newfi/Portal\")\nGIT_PORTAL = WT / PORTAL_REL\nEXPECTED_NEW = {\n \"Pub/Controller/Portal.php\": (363, \"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\"),\n \"Service/PortalData.php\": (5213, \"4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d\"),\n \"_data/templates.xml\": (10977, \"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\"),\n}\nEXPECTED_OLD = {\n \"Pub/Controller/Portal.php\": (247, \"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\"),\n \"_data/templates.xml\": (1461, \"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\"),\n}\nEXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)]\nBACKUP = f\"/root/newfi-portal200-backup-{CID}\"\n\n\nclass PortalError(RuntimeError):\n pass\n\n\ndef emit(key, value=None):\n if value is None:\n print(key, flush=True)\n elif isinstance(value, (dict, list, tuple, bool, int, float)):\n print(\n key + \"=\" + json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(\",\", \":\")),\n flush=True,\n )\n else:\n print(key + \"=\" + str(value), flush=True)\n\n\ndef fail(code):\n raise PortalError(code)\n\n\ndef sha_bytes(data):\n return hashlib.sha256(data).hexdigest()\n\n\ndef file_meta(path):\n data = pathlib.Path(path).read_bytes()\n return len(data), sha_bytes(data)\n\n\ndef run(cmd, *, input_bytes=None, timeout=180, ok=(0,)):\n try:\n p = subprocess.run(\n cmd,\n input=input_bytes,\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n timeout=timeout,\n check=False,\n )\n except subprocess.TimeoutExpired:\n fail(\"SUBPROCESS_TIMEOUT\")\n if p.returncode not in ok:\n fail(\"SUBPROCESS_RC\")\n return p\n\n\ndef git(*args, ok=(0,), timeout=120):\n p = run([\"/usr/bin/git\", \"-C\", str(WT), *args], timeout=timeout, ok=ok)\n return p.stdout.decode(\"utf-8\", \"replace\").strip()\n\n\ndef qga(target, argv, *, input_bytes=None, timeout=180):\n if target == \"prod\":\n cmd = [\n \"/usr/sbin/qm\",\n \"guest\",\n \"exec\",\n \"210\",\n \"--synchronous\",\n \"1\",\n \"--timeout\",\n str(timeout),\n ]\n if input_bytes is not None:\n cmd += [\"--pass-stdin\", \"1\"]\n cmd += [\"--\", *argv]\n elif target == \"staging\":\n q = [\n \"/usr/sbin/qm\",\n \"guest\",\n \"exec\",\n \"211\",\n \"--synchronous\",\n \"1\",\n \"--timeout\",\n str(timeout),\n ]\n if input_bytes is not None:\n q += [\"--pass-stdin\", \"1\"]\n q += [\"--\", *argv]\n remote = (\n \"test \\\"$(id -u):$(hostname -s)\\\" = 0:pve03 || exit 72; \"\n \"for f in /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md /etc/pve/31_HOMELAB_REFERENCE.md; do \"\n \"test -r \\\"$f\\\" && test -s \\\"$f\\\" && cat -- \\\"$f\\\" >/dev/null || exit 73; done; exec \"\n + \" \".join(shlex.quote(x) for x in q)\n )\n cmd = [\n \"/usr/bin/ssh\",\n \"-T\",\n \"-o\",\n \"BatchMode=yes\",\n \"-o\",\n \"StrictHostKeyChecking=yes\",\n \"-o\",\n \"UpdateHostKeys=no\",\n \"-o\",\n \"ConnectTimeout=10\",\n \"-o\",\n \"ConnectionAttempts=1\",\n \"root@pve03\",\n remote,\n ]\n else:\n fail(\"BAD_TARGET\")\n\n p = run(cmd, input_bytes=input_bytes, timeout=timeout + 30)\n try:\n data = json.loads(p.stdout.decode(\"utf-8\"))\n except Exception:\n fail(\"QGA_JSON\")\n if data.get(\"exited\") not in (1, True) or data.get(\"exitcode\") != 0:\n fail(\"QGA_GUEST_RC\")\n if data.get(\"out-truncated\") or data.get(\"err-truncated\"):\n fail(\"QGA_TRUNCATED\")\n return data.get(\"out-data\", \"\")\n\n\nINSPECT_PHP = r'''db();\nfunction srcMeta(string $path): ?array {\n if (!is_file($path)) { return null; }\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\n}\n$portal = $root . '/src/addons/Newfi/Portal';\n$templates = $db->fetchAll(\n \"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \" .\n \"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \" .\n \"LOCATE('--newfi-portal200', template) > 0 AS css_marker \" .\n \"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\"\n);\n$result = [\n 'hostname' => gethostname(),\n 'application' => [\n 'boardActive' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\"),\n 'boardUrl' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\"),\n 'indexRoute' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\"),\n 'defaultStyleId' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\"),\n 'error_count' => (int)$db->fetchOne(\"SELECT COUNT(*) FROM xf_error_log\"),\n 'error_max_id' => (int)$db->fetchOne(\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\")\n ],\n 'addon' => $db->fetchRow(\n \"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \" .\n \"FROM xf_addon WHERE addon_id='Newfi/Portal'\"\n ),\n 'source' => [\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n ],\n 'templates' => $templates\n];\necho \"PORTAL200_INSPECT_JSON_BEGIN\\n\";\necho json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\necho \"\\nPORTAL200_INSPECT_JSON_END\\n\";\n?>'''\n\n\nIMPORT_PHP = r'''addOnManager()->getById('Newfi/Portal');\nif (!$addOn || !$addOn->isInstalled()) {\n fwrite(STDERR, \"ADDON_NOT_INSTALLED\\n\");\n exit(30);\n}\n$addOn->preRebuild();\n$dataManager = $app->addOnDataManager();\n$imported = [];\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\n $handler = $dataManager->getDataTypeHandler($dataType);\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\n if (!$xml || !$xml->count()) {\n $handler->deleteAddOnData($addOn->addon_id);\n continue;\n }\n $start = 0;\n $guard = 0;\n while (true) {\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\n if (!$last) { break; }\n if ($last <= $start || ++$guard > 100000) {\n fwrite(STDERR, \"IMPORT_PROGRESS_INVALID\\n\");\n exit(31);\n }\n $start = $last;\n }\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\n \\XF::triggerRunOnce();\n $imported[] = $handler->getContainerTag();\n}\n$addOn->postDataImport();\n$repo = $app->repository(\\XF\\Repository\\StyleRepository::class);\n$repo->updateAllStylesLastModifiedDateLater();\n$addOn->postRebuild();\necho \"PORTAL200_IMPORT_JSON=\" . json_encode(\n ['result'=>'PASS','containers'=>$imported],\n JSON_UNESCAPED_SLASHES\n) . \"\\n\";\n?>'''\n\n\nERRORS_PHP_TEMPLATE = r'''db();\n$b=%d;\n$rows=$db->fetchAll(\n \"SELECT error_id,exception_type,message,filename,line FROM xf_error_log WHERE error_id>? ORDER BY error_id\",\n [$b]\n);\necho json_encode(\n $rows,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\n?>'''\n\n\nRUNTIME_VERIFY_PHP_TEMPLATE = r'''$state];\nif ($state === 'new') {\n $service = new \\Newfi\\Portal\\Service\\PortalData($app);\n $portal = $service->build();\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\n $result['sections'] = [];\n foreach (['forums','resources','media','events','dogs'] as $key) {\n $section = $portal[$key] ?? null;\n if (!is_array($section)) { fwrite(STDERR, \"SECTION_MISSING\\n\"); exit(60); }\n $result['sections'][$key] = [\n 'available'=>(bool)($section['available'] ?? false),\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\n 'status'=>(string)($section['status'] ?? '')\n ];\n }\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\"1\"') !== false;\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\"resources\"') !== false;\n $result['locked_media'] = strpos($html, 'data-portal-locked=\"media\"') !== false;\n $result['html_bytes'] = strlen($html);\n if (!$result['html_marker']) { fwrite(STDERR, \"HTML_MARKER_MISSING\\n\"); exit(61); }\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\n fwrite(STDERR, \"GUEST_PERMISSION_CONTRACT_CHANGED\\n\"); exit(62);\n }\n if (!$result['locked_resources'] || !$result['locked_media']) {\n fwrite(STDERR, \"LOCKED_COPY_MISSING\\n\"); exit(63);\n }\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\n fwrite(STDERR, \"PUBLIC_SECTION_UNAVAILABLE\\n\"); exit(64);\n }\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\n $result['html_bytes'] = strlen($html);\n if ($result['html_marker'] || !$result['old_copy']) {\n fwrite(STDERR, \"OLD_RENDER_CONTRACT\\n\"); exit(65);\n }\n} else {\n exit(66);\n}\necho \"PORTAL200_RUNTIME_JSON=\" . json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n) . \"\\n\";\n?>'''\n\n\nHTTP_VERIFY_PY = r'''import html,json,re,subprocess,sys,urllib.parse\nmode=sys.argv[1]\nstate=sys.argv[2]\nif mode=='staging':\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\nelif mode=='prod':\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\nelse:\n raise SystemExit(40)\ndef fetch(url):\n cmd=[\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\n ]\n if mode=='staging':\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\n cmd += [\n '-o','-','-w','\\n__PORTAL_STATUS__%{http_code}',url\n ]\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\n if p.returncode!=0: raise SystemExit(41)\n text=p.stdout.decode('utf-8','replace')\n marker='\\n__PORTAL_STATUS__'\n if marker not in text: raise SystemExit(42)\n body,status=text.rsplit(marker,1)\n return int(status.strip()),body\nroot=f'{scheme}://{host}/'\nstatus,body=fetch(root)\nhrefs=[]\nfor raw in re.findall(r\"href=[\\\"']([^\\\"']*css\\.php[^\\\"']*)[\\\"']\",body,re.I):\n raw=html.unescape(raw)\n hrefs.append(urllib.parse.urljoin(root,raw))\nnormal_css=[]\nnormal_status=[]\nfor u in hrefs:\n s,b=fetch(u)\n normal_status.append(s)\n normal_css.append(b)\nnormal_css_text='\\n'.join(normal_css)\nforced_css_marker=None\nforced_css_status=None\nif mode=='staging' and state=='new' and hrefs:\n u=urllib.parse.urlsplit(hrefs[0])\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\n out=[]\n found=False\n for k,v in pairs:\n if k=='css':\n found=True\n parts=[x for x in v.split(',') if x]\n if 'public:newfi_portal.less' not in parts:\n parts.append('public:newfi_portal.less')\n v=','.join(parts)\n out.append((k,v))\n if found:\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\n forced_css_status,forced_body=fetch(forced)\n forced_css_marker='--newfi-portal200' in forced_body\nresult={\n 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\n}\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\nif status!=expected_status: raise SystemExit(45)\nif mode=='prod':\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\nelif mode=='staging':\n if any(x!=200 for x in normal_status): raise SystemExit(46)\nelse:\n raise SystemExit(50)\nif mode=='prod' and state=='new':\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\nelif mode=='prod' and state=='old':\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\nelif mode=='staging' and state=='new':\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\nelif mode=='staging' and state=='old':\n pass\nelse:\n raise SystemExit(50)\n'''\n\n\ndef parse_inspect(text):\n b = \"PORTAL200_INSPECT_JSON_BEGIN\\n\"\n e = \"\\nPORTAL200_INSPECT_JSON_END\"\n if b not in text or e not in text:\n fail(\"INSPECT_MARKERS\")\n try:\n return json.loads(text.split(b, 1)[1].split(e, 1)[0])\n except Exception:\n fail(\"INSPECT_JSON\")\n\n\ndef inspect(target):\n return parse_inspect(\n qga(target, [\"/usr/bin/php8.3\"], input_bytes=INSPECT_PHP.encode(), timeout=120)\n )\n\n\ndef source_is_old(data):\n src = data[\"source\"]\n for rel, (size, sha) in EXPECTED_OLD.items():\n row = src.get(rel)\n if not row or row.get(\"bytes\") != size or row.get(\"sha256\") != sha:\n return False\n return src.get(\"Service/PortalData.php\") is None\n\n\ndef source_is_new(data):\n src = data[\"source\"]\n for rel, (size, sha) in EXPECTED_NEW.items():\n row = src.get(rel)\n if not row or row.get(\"bytes\") != size or row.get(\"sha256\") != sha:\n return False\n return True\n\n\ndef template_state_new(data):\n rows = data.get(\"templates\", [])\n rows0 = [r for r in rows if int(r.get(\"style_id\", -1)) == 0]\n titles = [r.get(\"title\") for r in rows0]\n by = {r.get(\"title\"): r for r in rows0}\n return (\n titles == [\"newfi_portal\", \"newfi_portal.less\"]\n and int(by[\"newfi_portal\"].get(\"html_marker\", 0)) == 1\n and int(by[\"newfi_portal.less\"].get(\"css_marker\", 0)) == 1\n )\n\n\ndef app_contract(data, target):\n a = data.get(\"application\", {})\n addon = data.get(\"addon\") or {}\n expected = {\n \"staging\": (\"0\", \"http://newfi-staging.gram1.ru\"),\n \"prod\": (\"1\", \"https://newfi.ru\"),\n }[target]\n return (\n a.get(\"boardActive\") == expected[0]\n and a.get(\"boardUrl\") == expected[1]\n and a.get(\"indexRoute\") == \"portal/\"\n and a.get(\"defaultStyleId\") == \"4\"\n and addon.get(\"addon_id\") == \"Newfi/Portal\"\n and int(addon.get(\"version_id\", 0)) == 1000010\n and addon.get(\"version_string\") == \"1.0.0 Alpha 1\"\n and int(addon.get(\"active\", 0)) == 1\n and int(addon.get(\"is_processing\", 1)) == 0\n )\n\n\ndef new_errors(target, baseline):\n raw = qga(\n target,\n [\"/usr/bin/php8.3\"],\n input_bytes=(ERRORS_PHP_TEMPLATE % int(baseline)).encode(),\n timeout=90,\n )\n try:\n return json.loads(raw)\n except Exception:\n fail(\"ERROR_LOG_JSON\")\n\n\ndef assert_no_new_errors(target, baseline, phase):\n rows = new_errors(target, baseline)\n if rows:\n emit(\n \"PORTAL200_NEW_XF_ERRORS\",\n {\n \"target\": target,\n \"phase\": phase,\n \"count\": len(rows),\n \"ids\": [r.get(\"error_id\") for r in rows],\n },\n )\n fail(\"NEW_XF_ERRORS\")\n\n\ndef guest_backup(target):\n script = f'''set -eu\nB={shlex.quote(BACKUP)}\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest ! -e \"$B\"\ninstall -d -m 700 \"$B/Pub/Controller\" \"$B/_data\"\ncp -p -- \"$P/Pub/Controller/Portal.php\" \"$B/Pub/Controller/Portal.php\"\ncp -p -- \"$P/_data/templates.xml\" \"$B/_data/templates.xml\"\ntest ! -e \"$P/Service/PortalData.php\"\nprintf 'PORTAL200_BACKUP=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_BACKUP=PASS\" not in out:\n fail(\"BACKUP_FAILED\")\n\n\ndef guest_write(target, rel, data):\n dest = f\"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}\"\n parent = str(pathlib.PurePosixPath(dest).parent)\n script = (\n \"set -eu; umask 022; \"\n + \"install -d -m 755 \"\n + shlex.quote(parent)\n + \"; T=$(mktemp \"\n + shlex.quote(parent + \"/.portal200.XXXXXX\")\n + \"); cat >\\\"$T\\\"; chmod 0644 \\\"$T\\\"; chown root:root \\\"$T\\\"; \"\n + \"mv -f -- \\\"$T\\\" \"\n + shlex.quote(dest)\n + \"; printf 'PORTAL200_WRITE=PASS\\\\n'\"\n )\n out = qga(\n target,\n [\"/bin/bash\", \"-lc\", script],\n input_bytes=data,\n timeout=90,\n )\n if \"PORTAL200_WRITE=PASS\" not in out:\n fail(\"WRITE_FAILED\")\n\n\ndef install_candidate(target):\n for rel in [\n \"Pub/Controller/Portal.php\",\n \"Service/PortalData.php\",\n \"_data/templates.xml\",\n ]:\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\n\n\ndef restore_old(target):\n script = f'''set -eu\nB={shlex.quote(BACKUP)}\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest -f \"$B/Pub/Controller/Portal.php\"\ntest -f \"$B/_data/templates.xml\"\ncp -p -- \"$B/Pub/Controller/Portal.php\" \"$P/Pub/Controller/Portal.php\"\ncp -p -- \"$B/_data/templates.xml\" \"$P/_data/templates.xml\"\nrm -f -- \"$P/Service/PortalData.php\"\nrmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_SOURCE_RESTORE=PASS\" not in out:\n fail(\"SOURCE_RESTORE_FAILED\")\n\n\ndef import_addon(target):\n out = qga(\n target,\n [\"/usr/bin/php8.3\"],\n input_bytes=IMPORT_PHP.encode(),\n timeout=240,\n )\n if \"PORTAL200_IMPORT_JSON=\" not in out:\n fail(\"IMPORT_FAILED\")\n\n\ndef runtime_verify(target, state):\n src = RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", state)\n out = qga(target, [\"/usr/bin/php8.3\"], input_bytes=src.encode(), timeout=120)\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_RUNTIME_JSON=\")),\n None,\n )\n if not line:\n fail(\"RUNTIME_MARKER_MISSING\")\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n except Exception:\n fail(\"RUNTIME_JSON\")\n emit(\"PORTAL200_RUNTIME\", {\"target\": target, **data})\n return data\n\n\ndef http_verify(target, state):\n argv = [\"/usr/bin/python3\", \"-\", target, state]\n if target == \"prod\":\n try:\n p = subprocess.run(\n argv,\n input=HTTP_VERIFY_PY.encode(),\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n timeout=180,\n check=False,\n )\n except subprocess.TimeoutExpired:\n fail(\"HTTP_PROD_TIMEOUT\")\n out = p.stdout.decode(\"utf-8\", \"replace\")\n err = p.stderr.decode(\"utf-8\", \"replace\")\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_HTTP_JSON=\")),\n None,\n )\n data = None\n if line:\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\n except Exception:\n fail(\"HTTP_JSON\")\n if p.returncode != 0:\n emit(\n \"PORTAL200_HTTP_EXEC\",\n {\n \"target\": target,\n \"state\": state,\n \"rc\": p.returncode,\n \"stderr\": err[:800],\n },\n )\n fail(\"HTTP_PROD_RC\")\n if data is None:\n fail(\"HTTP_MARKER_MISSING\")\n return data\n\n out = qga(\n target,\n argv,\n input_bytes=HTTP_VERIFY_PY.encode(),\n timeout=180,\n )\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_HTTP_JSON=\")),\n None,\n )\n if not line:\n fail(\"HTTP_MARKER_MISSING\")\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n except Exception:\n fail(\"HTTP_JSON\")\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\n return data\n\n\ndef verify_new(target, baseline):\n data = inspect(target)\n if not app_contract(data, target):\n fail(\"APP_CONTRACT_NEW\")\n if not source_is_new(data):\n fail(\"SOURCE_NEW_MISMATCH\")\n if not template_state_new(data):\n fail(\"TEMPLATE_NEW_MISMATCH\")\n runtime_verify(target, \"new\")\n http_verify(target, \"new\")\n assert_no_new_errors(target, baseline, \"verify-new\")\n return inspect(target)\n\n\ndef verify_old(target, baseline, old_templates, strict_errors=True):\n data = inspect(target)\n if not app_contract(data, target):\n fail(\"APP_CONTRACT_OLD\")\n if not source_is_old(data):\n fail(\"SOURCE_OLD_MISMATCH\")\n if data.get(\"templates\") != old_templates:\n fail(\"TEMPLATE_ROLLBACK_MISMATCH\")\n if target == \"prod\":\n runtime_verify(target, \"old\")\n http_verify(target, \"old\")\n if strict_errors:\n assert_no_new_errors(target, baseline, \"verify-old\")\n return inspect(target)\n\n\ndef lint_php(target, source, label):\n out = qga(\n target,\n [\"/usr/bin/php8.3\", \"-l\"],\n input_bytes=source.encode(),\n timeout=90,\n )\n if \"No syntax errors detected\" not in out:\n fail(\"PHP_LINT_\" + label)\n\n\ndef cluster_guard():\n p = run([\"/usr/sbin/qm\", \"status\", \"210\"], timeout=30)\n if \"status: running\" not in p.stdout.decode():\n fail(\"VM210_NOT_RUNNING\")\n p = run(\n [\n \"/usr/bin/ssh\",\n \"-T\",\n \"-o\",\n \"BatchMode=yes\",\n \"-o\",\n \"StrictHostKeyChecking=yes\",\n \"-o\",\n \"UpdateHostKeys=no\",\n \"-o\",\n \"ConnectTimeout=10\",\n \"root@pve03\",\n \"/usr/sbin/qm status 211\",\n ],\n timeout=40,\n )\n if \"status: running\" not in p.stdout.decode():\n fail(\"VM211_NOT_RUNNING\")\n emit(\"PORTAL200_CLUSTER\", \"PASS\")\n\n\ndef result_contract(status, changes, rollback_started, rollback_restored):\n emit(\n \"HOMELAB_RESULT_CONTRACT\",\n {\n \"version\": 1,\n \"command_id\": CID,\n \"status\": status,\n \"changes_made\": bool(changes),\n \"rollback_started\": bool(rollback_started),\n \"rollback_restored\": rollback_restored,\n },\n )\n\n\nemit(\"NEWFI200_BEGIN\")\nemit(\"COMMAND_ID\", CID)\nemit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\", 22)\nemit(\n \"PORTAL200_FLOW\",\n \"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\",\n)\n\nstaging_backup = False\nstaging_mutated = False\nstaging_final_new = False\nprod_backup = False\nprod_mutated = False\ngit_files_changed = False\ncommit_created = False\npush_confirmed = False\nfailure_rollback_started = False\nfailure_rollback_restored = None\nold_git_bytes = {}\nnew_head = None\ns_error = None\np_error = None\ns_old_templates = None\np_old_templates = None\n\ntry:\n compile(HTTP_VERIFY_PY, \"\", \"exec\")\n for rel, (size, sha) in EXPECTED_NEW.items():\n p = CAND_ROOT / rel\n if not p.is_file() or file_meta(p) != (size, sha):\n fail(\"CANDIDATE_IDENTITY\")\n tree = ET.parse(CAND_ROOT / \"_data/templates.xml\")\n titles = sorted(x.attrib.get(\"title\") for x in tree.getroot().findall(\"template\"))\n if titles != [\"newfi_portal\", \"newfi_portal.less\"]:\n fail(\"CANDIDATE_TEMPLATE_SET\")\n service = (CAND_ROOT / \"Service/PortalData.php\").read_text(\"utf-8\")\n if any(\n token in service\n for token in [\n \"\\\\XF::db(\",\n \"->save(\",\n \"->delete(\",\n \"INSERT \",\n \"UPDATE \",\n \"DELETE \",\n \"REPLACE \",\n ]\n ):\n fail(\"CANDIDATE_MUTATION_SCAN\")\n emit(\"PORTAL200_CANDIDATE_IDENTITY\", \"PASS\")\n emit(\n \"PORTAL200_ORCHESTRATION_FIXES\",\n {\n \"staging_503_without_css_links\": True,\n \"failure_status\": \"FAIL\",\n \"prod_http_via_pve01_public_route\": True,\n \"css_href_regex_fixed\": True,\n },\n )\n\n branch = git(\"branch\", \"--show-current\")\n head = git(\"rev-parse\", \"HEAD\")\n status = git(\"status\", \"--porcelain=v1\", \"--untracked-files=all\")\n remote_out = git(\"ls-remote\", \"origin\", f\"refs/heads/{BRANCH}\")\n remote = remote_out.split()[0] if remote_out else \"\"\n if branch != BRANCH or head != OLD_HEAD or remote != OLD_HEAD or status:\n fail(\"GIT_PRESTATE\")\n emit(\n \"PORTAL200_GIT_PRESTATE\",\n {\"branch\": branch, \"head\": head, \"remote\": remote, \"clean\": True},\n )\n cluster_guard()\n\n lint_php(\"staging\", INSPECT_PHP, \"INSPECT_STAGING\")\n lint_php(\"staging\", IMPORT_PHP, \"IMPORT_STAGING\")\n lint_php(\"staging\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\"), \"RUNTIME_STAGING\")\n lint_php(\"prod\", INSPECT_PHP, \"INSPECT_PROD\")\n lint_php(\"prod\", IMPORT_PHP, \"IMPORT_PROD\")\n lint_php(\"prod\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\"), \"RUNTIME_PROD\")\n emit(\"PORTAL200_RUNTIME_HELPER_LINT\", \"PASS\")\n\n s0 = inspect(\"staging\")\n p0 = inspect(\"prod\")\n if not app_contract(s0, \"staging\") or not app_contract(p0, \"prod\"):\n fail(\"APP_PRESTATE\")\n if not source_is_old(s0) or not source_is_old(p0):\n fail(\"SOURCE_PRESTATE\")\n if s0.get(\"templates\") != p0.get(\"templates\"):\n fail(\"TEMPLATE_PRESTATE_PARITY\")\n s_error = int(s0[\"application\"][\"error_max_id\"])\n p_error = int(p0[\"application\"][\"error_max_id\"])\n s_old_templates = s0[\"templates\"]\n p_old_templates = p0[\"templates\"]\n emit(\"PORTAL200_ERROR_BASELINE\", {\"staging\": s_error, \"prod\": p_error})\n http_verify(\"staging\", \"old\")\n runtime_verify(\"prod\", \"old\")\n http_verify(\"prod\", \"old\")\n assert_no_new_errors(\"staging\", s_error, \"preflight\")\n assert_no_new_errors(\"prod\", p_error, \"preflight\")\n emit(\"PORTAL200_PREFLIGHT\", \"PASS\")\n\n guest_backup(\"staging\")\n staging_backup = True\n staging_mutated = True\n install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n emit(\"PORTAL200_STAGING_FIRST_APPLY\", \"PASS\")\n\n restore_old(\"staging\")\n import_addon(\"staging\")\n verify_old(\"staging\", s_error, s_old_templates)\n staging_mutated = False\n emit(\"PORTAL200_STAGING_ROLLBACK_PROOF\", \"PASS\")\n\n staging_mutated = True\n install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n staging_final_new = True\n emit(\"PORTAL200_STAGING_FINAL\", \"PASS\")\n\n for rel in EXPECTED_NEW:\n dest = GIT_PORTAL / rel\n old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None\n dest.parent.mkdir(parents=True, exist_ok=True)\n shutil.copyfile(CAND_ROOT / rel, dest)\n git_files_changed = True\n\n if git(\"diff\", \"--check\"):\n fail(\"GIT_DIFF_CHECK\")\n status_lines = git(\"status\", \"--porcelain=v1\", \"--untracked-files=all\").splitlines()\n actual_paths = sorted(line[3:] for line in status_lines if len(line) >= 4)\n if actual_paths != sorted(EXPECTED_GIT_PATHS):\n fail(\"GIT_SCOPE\")\n git(\"add\", \"--\", *EXPECTED_GIT_PATHS)\n staged = sorted(x for x in git(\"diff\", \"--cached\", \"--name-only\").splitlines() if x)\n if staged != sorted(EXPECTED_GIT_PATHS):\n fail(\"GIT_STAGED_SCOPE\")\n git(\"commit\", \"-m\", f\"feat: build data-backed Newfi portal [{CID}]\")\n commit_created = True\n new_head = git(\"rev-parse\", \"HEAD\")\n if new_head == OLD_HEAD:\n fail(\"GIT_COMMIT_HEAD\")\n if git(\"status\", \"--porcelain=v1\", \"--untracked-files=all\"):\n fail(\"GIT_POSTCOMMIT_DIRTY\")\n git(\"push\", \"origin\", f\"HEAD:refs/heads/{BRANCH}\", timeout=180)\n remote_out = git(\"ls-remote\", \"origin\", f\"refs/heads/{BRANCH}\")\n remote_head = remote_out.split()[0] if remote_out else \"\"\n if remote_head != new_head:\n fail(\"GIT_REMOTE_HEAD\")\n push_confirmed = True\n emit(\n \"PORTAL200_GIT_PROMOTION\",\n {\n \"old_head\": OLD_HEAD,\n \"new_head\": new_head,\n \"remote_head\": remote_head,\n \"result\": \"PASS\",\n },\n )\n\n p_pre = inspect(\"prod\")\n if not source_is_old(p_pre) or p_pre.get(\"templates\") != p_old_templates:\n fail(\"PROD_DRIFT_BEFORE_APPLY\")\n if int(p_pre[\"application\"][\"error_max_id\"]) != p_error:\n fail(\"PROD_ERROR_DRIFT\")\n guest_backup(\"prod\")\n prod_backup = True\n prod_mutated = True\n install_candidate(\"prod\")\n import_addon(\"prod\")\n verify_new(\"prod\", p_error)\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n\n sf = verify_new(\"staging\", s_error)\n pf = verify_new(\"prod\", p_error)\n if sf[\"source\"] != pf[\"source\"]:\n fail(\"FINAL_SOURCE_PARITY\")\n if sf[\"templates\"] != pf[\"templates\"]:\n fail(\"FINAL_TEMPLATE_PARITY\")\n if git(\"rev-parse\", \"HEAD\") != new_head:\n fail(\"FINAL_LOCAL_HEAD\")\n remote_out = git(\"ls-remote\", \"origin\", f\"refs/heads/{BRANCH}\")\n if not remote_out or remote_out.split()[0] != new_head:\n fail(\"FINAL_REMOTE_HEAD\")\n if git(\"status\", \"--porcelain=v1\", \"--untracked-files=all\"):\n fail(\"FINAL_GIT_DIRTY\")\n cluster_guard()\n emit(\n \"PORTAL200_FINAL_EVIDENCE\",\n {\n \"commit\": new_head,\n \"source_files\": 3,\n \"templates\": 2,\n \"staging_error_baseline\": s_error,\n \"prod_error_baseline\": p_error,\n },\n )\n emit(\"PORTAL200_PORTAL\", \"PASS\")\n emit(\"PORTAL200_STAGING\", \"PASS\")\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n emit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\", 21)\n result_contract(\"OK\", True, False, None)\n emit(\"NEWFI200_END\")\n raise SystemExit(0)\n\nexcept PortalError as exc:\n code = str(exc)\n emit(\"PORTAL200_FAILURE\", code)\n\n if push_confirmed:\n if prod_mutated and prod_backup:\n failure_rollback_started = True\n try:\n restore_old(\"prod\")\n import_addon(\"prod\")\n verify_old(\"prod\", p_error, p_old_templates, strict_errors=False)\n prod_mutated = False\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_PROD_ROLLBACK\", \"PASS_PARTIAL_OVERALL_STATE_CHANGED\")\n except Exception:\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_PROD_ROLLBACK\", \"FAIL\")\n else:\n if staging_mutated and staging_backup:\n failure_rollback_started = True\n try:\n restore_old(\"staging\")\n import_addon(\"staging\")\n verify_old(\"staging\", s_error, s_old_templates, strict_errors=False)\n staging_mutated = False\n staging_final_new = False\n failure_rollback_restored = True\n emit(\"PORTAL200_FAILURE_STAGING_ROLLBACK\", \"PASS\")\n except Exception:\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_STAGING_ROLLBACK\", \"FAIL\")\n\n if git_files_changed and not commit_created:\n try:\n for rel, old in old_git_bytes.items():\n dest = GIT_PORTAL / rel\n if old is None:\n if dest.exists():\n dest.unlink()\n else:\n dest.write_bytes(old)\n service_dir = GIT_PORTAL / \"Service\"\n try:\n service_dir.rmdir()\n except OSError:\n pass\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"PASS\")\n except Exception:\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"FAIL\")\n\n if commit_created:\n emit(\n \"PORTAL200_FAILURE_GIT_HISTORY\",\n \"COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED\",\n )\n\n persisted = bool(push_confirmed or commit_created or staging_final_new or prod_mutated)\n if failure_rollback_started and failure_rollback_restored is None:\n failure_rollback_restored = False\n result_contract(\n \"FAIL\",\n persisted,\n failure_rollback_started,\n failure_rollback_restored,\n )\n emit(\"PORTAL200_ROADMAP_REMAINS\", 22)\n emit(\"NEWFI200_END\")\n raise SystemExit(1)\n\nexcept Exception:\n emit(\"PORTAL200_FAILURE\", \"UNEXPECTED_EXCEPTION\")\n result_contract(\n \"FAIL\",\n bool(push_confirmed or commit_created or staging_final_new or prod_mutated),\n False,\n None,\n )\n emit(\"PORTAL200_ROADMAP_REMAINS\", 22)\n emit(\"NEWFI200_END\")\n raise SystemExit(1)\n\nCANDIDATE_LAYER_1_FULL_END\nCANDIDATE_LAYER_2_SOURCE=carrier\nCANDIDATE_LAYER_2_SHA256=573c5c94fe59ed072e094281668ce9847dc4a9882019a2b89d8ba5f190d92176\nCANDIDATE_LAYER_2_BYTES=17209\nCANDIDATE_LAYER_2_RELEVANT_BEGIN\nexit /b %RC%\n# NEWFI54_POWERSHELL_BODY\n$ErrorActionPreference='Stop'\n$plinkPath='C:\\Program Files\\PuTTY\\plink.exe'\n$remotePath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_REMOTE.txt'\n$stdoutPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STDOUT.txt'\n$stderrPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STDERR.txt'\n$statusPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STATUS.txt'\n\nforeach($p in @($remotePath,$stdoutPath,$stderrPath,$statusPath)) {\n Remove-Item -LiteralPath $p -Force -ErrorAction SilentlyContinue\n}\nif(-not (Test-Path -LiteralPath $plinkPath -PathType Leaf)) {\n [IO.File]::WriteAllText($statusPath,'STOP=PLINK_MISSING'+[Environment]::NewLine)\n exit 2\n$remote=@'\n(set -eu; PATH=/usr/sbin:/usr/bin:/sbin:/bin; LC_ALL=C; export PATH LC_ALL; printf 'TARGET=pve01 STEP=NEWFI54 PORTAL200_APPLY3\\n'; case $(id -u):$(hostname -s) in 0:pve01) ;; *) exit 2;; esac; test -f /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && test -r /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && test -s /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && cat -- /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md >/dev/null || exit 2; test -f /etc/pve/31_HOMELAB_REFERENCE.md && test -r /etc/pve/31_HOMELAB_REFERENCE.md && test -s /etc/pve/31_HOMELAB_REFERENCE.md && cat -- /etc/pve/31_HOMELAB_REFERENCE.md >/dev/null || exit 2; printf '\\033[31mERROR_REGISTER_CHECK=OK\\033[0m\\n\\033[31mREFERENCE_CHECK=OK\\033[0m\\n'; umask 077; unset BASH_ENV ENV PYTHONPATH PYTHONHOME; test -f /usr/local/sbin/homelab-chat-run && test ! -L /usr/local/sbin/homelab-chat-run && test -x /usr/local/sbin/homelab-chat-run && test -x /usr/bin/python3 || exit 2; test -d /root/newfi-portal200-candidate || exit 2; d=/root/newfi-run-NEWFI-260907-PORTAL200-APPLY3; mkdir -m 700 -- $d 2>/dev/null || exit 2; printf 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\\nSTATE=RESERVED_BEFORE_EXECUTION\\n' >$d/attempt.txt; if /usr/local/sbin/homelab-chat-run NEWFI-260907-PORTAL200-APPLY3 change newfi-portal200-deploy -- /usr/bin/python3 -I -B -c 'import base64,hashlib,sys,zlib;d=zlib.decompress(base64.b64decode(sys.argv[1]));assert hashlib.sha256(d).hexdigest()==sys.argv[2];exec(d)' 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 97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184 >$d/wrapper.log 2>&1; then rc=0; else rc=$?; fi; printf 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\\n' >$d/receipt.txt; printf 'WRAPPER_RC=' >>$d/receipt.txt; echo $rc >>$d/receipt.txt; printf 'PUBLICATION=GITEA_RUNTIME_EXPECTED\\n' >>$d/receipt.txt; exit $rc)\n'@\n$remote=$remote.Replace(\"`r`n\",\"`n\").TrimEnd(\"`n\")+\"`n\"\n[IO.File]::WriteAllText($remotePath,$remote,[Text.UTF8Encoding]::new($false))\n$h=[Security.Cryptography.SHA256]::Create()\ntry {\n $actual=([BitConverter]::ToString($h.ComputeHash([IO.File]::ReadAllBytes($remotePath)))).Replace('-','').ToLowerInvariant()\n} finally {$h.Dispose()}\n$p=Start-Process -FilePath $plinkPath -ArgumentList $args -NoNewWindow -Wait -PassThru -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath\n$rc=$p.ExitCode\n$status=@(\n 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3',\n 'PLINK_RC='+$rc,\n 'REMOTE_RESULT=VERIFY_IN_GITEA',\n 'LOCAL_STDOUT='+$stdoutPath,\n 'LOCAL_STDERR='+$stderrPath\n) -join [Environment]::NewLine\n[IO.File]::WriteAllText($statusPath,$status+[Environment]::NewLine,[Text.UTF8Encoding]::new($false))\nexit $rc\nCANDIDATE_LAYER_2_RELEVANT_END\nCANDIDATE_LAYER_2_FULL_BEGIN\n@echo off\nsetlocal DisableDelayedExpansion\nset \"SELF=%~f0\"\n\"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command \"$ErrorActionPreference='Stop'; $t=[IO.File]::ReadAllText($env:SELF).Replace([string][char]13,''); $m='# NEWFI54_POWERSHELL_BODY'+[char]10; $i=$t.IndexOf($m); if($i -lt 0){exit 2}; $b=$t.Substring($i+$m.Length); $h=[Security.Cryptography.SHA256]::Create(); $d=([BitConverter]::ToString($h.ComputeHash([Text.Encoding]::UTF8.GetBytes($b)))).Replace('-','').ToLowerInvariant(); $h.Dispose(); if($d -ne 'fe66d389a08cceef3f1b8d656e926cc75a85f71d1e44b8b8b29ee9fa4f785dfc'){exit 2}; & ([ScriptBlock]::Create($b));\"\nset \"RC=%ERRORLEVEL%\"\nexit /b %RC%\n# NEWFI54_POWERSHELL_BODY\n$ErrorActionPreference='Stop'\n$plinkPath='C:\\Program Files\\PuTTY\\plink.exe'\n$remotePath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_REMOTE.txt'\n$stdoutPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STDOUT.txt'\n$stderrPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STDERR.txt'\n$statusPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STATUS.txt'\n\nforeach($p in @($remotePath,$stdoutPath,$stderrPath,$statusPath)) {\n Remove-Item -LiteralPath $p -Force -ErrorAction SilentlyContinue\n}\n\nif(-not (Test-Path -LiteralPath $plinkPath -PathType Leaf)) {\n [IO.File]::WriteAllText($statusPath,'STOP=PLINK_MISSING'+[Environment]::NewLine)\n exit 2\n}\nif(@(Get-Process -Name 'pageant*' -ErrorAction SilentlyContinue).Count -lt 1) {\n [IO.File]::WriteAllText($statusPath,'STOP=PAGEANT_NOT_RUNNING'+[Environment]::NewLine)\n exit 2\n}\n\n$remote=@'\n(set -eu; PATH=/usr/sbin:/usr/bin:/sbin:/bin; LC_ALL=C; export PATH LC_ALL; printf 'TARGET=pve01 STEP=NEWFI54 PORTAL200_APPLY3\\n'; case $(id -u):$(hostname -s) in 0:pve01) ;; *) exit 2;; esac; test -f /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && test -r /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && test -s /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && cat -- /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md >/dev/null || exit 2; test -f /etc/pve/31_HOMELAB_REFERENCE.md && test -r /etc/pve/31_HOMELAB_REFERENCE.md && test -s /etc/pve/31_HOMELAB_REFERENCE.md && cat -- /etc/pve/31_HOMELAB_REFERENCE.md >/dev/null || exit 2; printf '\\033[31mERROR_REGISTER_CHECK=OK\\033[0m\\n\\033[31mREFERENCE_CHECK=OK\\033[0m\\n'; umask 077; unset BASH_ENV ENV PYTHONPATH PYTHONHOME; test -f /usr/local/sbin/homelab-chat-run && test ! -L /usr/local/sbin/homelab-chat-run && test -x /usr/local/sbin/homelab-chat-run && test -x /usr/bin/python3 || exit 2; test -d /root/newfi-portal200-candidate || exit 2; d=/root/newfi-run-NEWFI-260907-PORTAL200-APPLY3; mkdir -m 700 -- $d 2>/dev/null || exit 2; printf 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\\nSTATE=RESERVED_BEFORE_EXECUTION\\n' >$d/attempt.txt; if /usr/local/sbin/homelab-chat-run NEWFI-260907-PORTAL200-APPLY3 change newfi-portal200-deploy -- /usr/bin/python3 -I -B -c 'import base64,hashlib,sys,zlib;d=zlib.decompress(base64.b64decode(sys.argv[1]));assert hashlib.sha256(d).hexdigest()==sys.argv[2];exec(d)' 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 97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184 >$d/wrapper.log 2>&1; then rc=0; else rc=$?; fi; printf 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\\n' >$d/receipt.txt; printf 'WRAPPER_RC=' >>$d/receipt.txt; echo $rc >>$d/receipt.txt; printf 'PUBLICATION=GITEA_RUNTIME_EXPECTED\\n' >>$d/receipt.txt; exit $rc)\n'@\n$remote=$remote.Replace(\"`r`n\",\"`n\").TrimEnd(\"`n\")+\"`n\"\n[IO.File]::WriteAllText($remotePath,$remote,[Text.UTF8Encoding]::new($false))\n\n$h=[Security.Cryptography.SHA256]::Create()\ntry {\n $actual=([BitConverter]::ToString($h.ComputeHash([IO.File]::ReadAllBytes($remotePath)))).Replace('-','').ToLowerInvariant()\n} finally {$h.Dispose()}\n\nif($actual -ne '71da2a66224075527d8aa04c1281fdb7a2b9f078d76c349fc05b48a4b803a4c3') {\n [IO.File]::WriteAllText($statusPath,'STOP=REMOTE_HASH_MISMATCH'+[Environment]::NewLine)\n exit 2\n}\n\n$args=@('-batch','-agent','-ssh','-P','22','-m',$remotePath,'root@[PRIVATE_IP]')\n$p=Start-Process -FilePath $plinkPath -ArgumentList $args -NoNewWindow -Wait -PassThru -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath\n$rc=$p.ExitCode\n\n$status=@(\n 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3',\n 'PLINK_RC='+$rc,\n 'REMOTE_RESULT=VERIFY_IN_GITEA',\n 'LOCAL_STDOUT='+$stdoutPath,\n 'LOCAL_STDERR='+$stderrPath\n) -join [Environment]::NewLine\n[IO.File]::WriteAllText($statusPath,$status+[Environment]::NewLine,[Text.UTF8Encoding]::new($false))\n\nexit $rc\n\nCANDIDATE_LAYER_2_FULL_END\nRECOVERED_VERSION_IDS=\nRECOVERED_VERSION_STRINGS==\nRECOVERED_SHA256_COUNT=8\nRECOVERED_SHA256=619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d,4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d,1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861,53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3,c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf,fe66d389a08cceef3f1b8d656e926cc75a85f71d1e44b8b8b29ee9fa4f785dfc,97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184,71da2a66224075527d8aa04c1281fdb7a2b9f078d76c349fc05b48a4b803a4c3\nRECOVERY_RESULT=EXACT_OLD_APPLY3_CARRIER_DECODED\nLOCAL_APPLY3_EXACT_CARRIER_RECOVERY_END=true\n" + "output": "AST_RECOVERY_BEGIN=true\nOLD_CARRIER_SHA256=573c5c94fe59ed072e094281668ce9847dc4a9882019a2b89d8ba5f190d92176\nOLD_INNER_SHA256=97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184\nOLD_INNER_BYTES=34155\nTOP_LEVEL_LITERAL_ASSIGNMENTS={\"BRANCH\":\"implementation/newfi-closed-mvp-20260906\",\"CID\":\"NEWFI-260907-PORTAL200-APPLY3\",\"ERRORS_PHP_TEMPLATE\":\"db();\\n$b=%d;\\n$rows=$db->fetchAll(\\n \\\"SELECT error_id,exception_type,message,filename,line FROM xf_error_log WHERE error_id>? ORDER BY error_id\\\",\\n [$b]\\n);\\necho json_encode(\\n $rows,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\\n?>\",\"EXPECTED_NEW\":{\"Pub/Controller/Portal.php\":[363,\"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\"],\"Service/PortalData.php\":[5213,\"4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d\"],\"_data/templates.xml\":[10977,\"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\"]},\"EXPECTED_OLD\":{\"Pub/Controller/Portal.php\":[247,\"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\"],\"_data/templates.xml\":[1461,\"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\"]},\"HTTP_VERIFY_PY\":\"import html,json,re,subprocess,sys,urllib.parse\\nmode=sys.argv[1]\\nstate=sys.argv[2]\\nif mode=='staging':\\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\\nelif mode=='prod':\\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\\nelse:\\n raise SystemExit(40)\\ndef fetch(url):\\n cmd=[\\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\\n ]\\n if mode=='staging':\\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\\n cmd += [\\n '-o','-','-w','\\\\n__PORTAL_STATUS__%{http_code}',url\\n ]\\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\\n if p.returncode!=0: raise SystemExit(41)\\n text=p.stdout.decode('utf-8','replace')\\n marker='\\\\n__PORTAL_STATUS__'\\n if marker not in text: raise SystemExit(42)\\n body,status=text.rsplit(marker,1)\\n return int(status.strip()),body\\nroot=f'{scheme}://{host}/'\\nstatus,body=fetch(root)\\nhrefs=[]\\nfor raw in re.findall(r\\\"href=[\\\\\\\"']([^\\\\\\\"']*css\\\\.php[^\\\\\\\"']*)[\\\\\\\"']\\\",body,re.I):\\n raw=html.unescape(raw)\\n hrefs.append(urllib.parse.urljoin(root,raw))\\nnormal_css=[]\\nnormal_status=[]\\nfor u in hrefs:\\n s,b=fetch(u)\\n normal_status.append(s)\\n normal_css.append(b)\\nnormal_css_text='\\\\n'.join(normal_css)\\nforced_css_marker=None\\nforced_css_status=None\\nif mode=='staging' and state=='new' and hrefs:\\n u=urllib.parse.urlsplit(hrefs[0])\\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\\n out=[]\\n found=False\\n for k,v in pairs:\\n if k=='css':\\n found=True\\n parts=[x for x in v.split(',') if x]\\n if 'public:newfi_portal.less' not in parts:\\n parts.append('public:newfi_portal.less')\\n v=','.join(parts)\\n out.append((k,v))\\n if found:\\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\\n forced_css_status,forced_body=fetch(forced)\\n forced_css_marker='--newfi-portal200' in forced_body\\nresult={\\n 'status':status,\\n 'bytes':len(body.encode()),\\n 'html_marker':'data-newfi-portal200=\\\"1\\\"' in body,\\n 'css_marker':'--newfi-portal200' in normal_css_text,\\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\\n 'css_count':len(hrefs),\\n 'css_status':normal_status,\\n 'forced_css_status':forced_css_status,\\n 'forced_css_marker':forced_css_marker,\\n}\\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\\nif status!=expected_status: raise SystemExit(45)\\nif mode=='prod':\\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\\nelif mode=='staging':\\n if any(x!=200 for x in normal_status): raise SystemExit(46)\\nelse:\\n raise SystemExit(50)\\nif mode=='prod' and state=='new':\\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\\nelif mode=='prod' and state=='old':\\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\\nelif mode=='staging' and state=='new':\\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\\nelif mode=='staging' and state=='old':\\n pass\\nelse:\\n raise SystemExit(50)\\n\",\"IMPORT_PHP\":\"addOnManager()->getById('Newfi/Portal');\\nif (!$addOn || !$addOn->isInstalled()) {\\n fwrite(STDERR, \\\"ADDON_NOT_INSTALLED\\\\n\\\");\\n exit(30);\\n}\\n$addOn->preRebuild();\\n$dataManager = $app->addOnDataManager();\\n$imported = [];\\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\\n $handler = $dataManager->getDataTypeHandler($dataType);\\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\\n if (!$xml || !$xml->count()) {\\n $handler->deleteAddOnData($addOn->addon_id);\\n continue;\\n }\\n $start = 0;\\n $guard = 0;\\n while (true) {\\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\\n if (!$last) { break; }\\n if ($last <= $start || ++$guard > 100000) {\\n fwrite(STDERR, \\\"IMPORT_PROGRESS_INVALID\\\\n\\\");\\n exit(31);\\n }\\n $start = $last;\\n }\\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\\n \\\\XF::triggerRunOnce();\\n $imported[] = $handler->getContainerTag();\\n}\\n$addOn->postDataImport();\\n$repo = $app->repository(\\\\XF\\\\Repository\\\\StyleRepository::class);\\n$repo->updateAllStylesLastModifiedDateLater();\\n$addOn->postRebuild();\\necho \\\"PORTAL200_IMPORT_JSON=\\\" . json_encode(\\n ['result'=>'PASS','containers'=>$imported],\\n JSON_UNESCAPED_SLASHES\\n) . \\\"\\\\n\\\";\\n?>\",\"INSPECT_PHP\":\"db();\\nfunction srcMeta(string $path): ?array {\\n if (!is_file($path)) { return null; }\\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\\n}\\n$portal = $root . '/src/addons/Newfi/Portal';\\n$templates = $db->fetchAll(\\n \\\"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \\\" .\\n \\\"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \\\" .\\n \\\"LOCATE('--newfi-portal200', template) > 0 AS css_marker \\\" .\\n \\\"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\\\"\\n);\\n$result = [\\n 'hostname' => gethostname(),\\n 'application' => [\\n 'boardActive' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\\\"),\\n 'boardUrl' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\\\"),\\n 'indexRoute' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\\\"),\\n 'defaultStyleId' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\\\"),\\n 'error_count' => (int)$db->fetchOne(\\\"SELECT COUNT(*) FROM xf_error_log\\\"),\\n 'error_max_id' => (int)$db->fetchOne(\\\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\\\")\\n ],\\n 'addon' => $db->fetchRow(\\n \\\"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \\\" .\\n \\\"FROM xf_addon WHERE addon_id='Newfi/Portal'\\\"\\n ),\\n 'source' => [\\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\\n ],\\n 'templates' => $templates\\n];\\necho \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\";\\necho json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\\necho \\\"\\\\nPORTAL200_INSPECT_JSON_END\\\\n\\\";\\n?>\",\"OLD_HEAD\":\"06bab5acd124e768f70305b3d7eca2f595871890\",\"RUNTIME_VERIFY_PHP_TEMPLATE\":\"$state];\\nif ($state === 'new') {\\n $service = new \\\\Newfi\\\\Portal\\\\Service\\\\PortalData($app);\\n $portal = $service->build();\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\\n $result['sections'] = [];\\n foreach (['forums','resources','media','events','dogs'] as $key) {\\n $section = $portal[$key] ?? null;\\n if (!is_array($section)) { fwrite(STDERR, \\\"SECTION_MISSING\\\\n\\\"); exit(60); }\\n $result['sections'][$key] = [\\n 'available'=>(bool)($section['available'] ?? false),\\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\\n 'status'=>(string)($section['status'] ?? '')\\n ];\\n }\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\\\"1\\\"') !== false;\\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\\\"resources\\\"') !== false;\\n $result['locked_media'] = strpos($html, 'data-portal-locked=\\\"media\\\"') !== false;\\n $result['html_bytes'] = strlen($html);\\n if (!$result['html_marker']) { fwrite(STDERR, \\\"HTML_MARKER_MISSING\\\\n\\\"); exit(61); }\\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\\n fwrite(STDERR, \\\"GUEST_PERMISSION_CONTRACT_CHANGED\\\\n\\\"); exit(62);\\n }\\n if (!$result['locked_resources'] || !$result['locked_media']) {\\n fwrite(STDERR, \\\"LOCKED_COPY_MISSING\\\\n\\\"); exit(63);\\n }\\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\\n fwrite(STDERR, \\\"PUBLIC_SECTION_UNAVAILABLE\\\\n\\\"); exit(64);\\n }\\n} else if ($state === 'old') {\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\\n $result['html_bytes'] = strlen($html);\\n if ($result['html_marker'] || !$result['old_copy']) {\\n fwrite(STDERR, \\\"OLD_RENDER_CONTRACT\\\\n\\\"); exit(65);\\n }\\n} else {\\n exit(66);\\n}\\necho \\\"PORTAL200_RUNTIME_JSON=\\\" . json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n) . \\\"\\\\n\\\";\\n?>\",\"commit_created\":false,\"failure_rollback_started\":false,\"git_files_changed\":false,\"old_git_bytes\":{},\"prod_backup\":false,\"prod_mutated\":false,\"push_confirmed\":false,\"staging_backup\":false,\"staging_final_new\":false,\"staging_mutated\":false}\nCONST_CID=NEWFI-260907-PORTAL200-APPLY3\nCONST_BRANCH=implementation/newfi-closed-mvp-20260906\nCONST_OLD_HEAD=06bab5acd124e768f70305b3d7eca2f595871890\nCONST_WT=UNPROVEN\nCONST_CAND_ROOT=UNPROVEN\nRELEVANT_CONSTANTS=[{\"lineno\":10,\"segment\":\"\\\"NEWFI-260907-PORTAL200-APPLY3\\\"\",\"value\":\"NEWFI-260907-PORTAL200-APPLY3\"},{\"lineno\":147,\"segment\":\"r'''db();\\nfunction srcMeta(string $path): ?array {\\n if (!is_file($path)) { return null; }\\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\\n}\\n$portal = $root . '/src/addons/Newfi/Portal';\\n$templates = $db->fetchAll(\\n \\\"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \\\" .\\n \\\"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \\\" .\\n \\\"LOCATE('--newfi-portal200', template) > 0 AS css_marker \\\" .\\n \\\"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\\\"\\n);\\n$result = [\\n 'hostname' => gethostname(),\\n 'application' => [\\n 'boardActive' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\\\"),\\n 'boardUrl' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\\\"),\\n 'indexRoute' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\\\"),\\n 'defaultStyleId' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\\\"),\\n 'error_count' => (int)$db->fetchOne(\\\"SELECT COUNT(*) FROM xf_error_log\\\"),\\n 'error_max_id' => (int)$db->fetchOne(\\\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\\\")\\n ],\\n 'addon' => $db->fetchRow(\\n \\\"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \\\" .\\n \\\"FROM xf_addon WHERE addon_id='Newfi/Portal'\\\"\\n ),\\n 'source' => [\\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\\n ],\\n 'templates' => $templates\\n];\\necho \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\";\\necho json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\\necho \\\"\\\\nPORTAL200_INSPECT_JSO\",\"value\":\"db();\\nfunction srcMeta(string $path): ?array {\\n if (!is_file($path)) { return null; }\\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\\n}\\n$portal = $root . '/src/addons/Newfi/Portal';\\n$templates = $db->fetchAll(\\n \\\"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \\\" .\\n \\\"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \\\" .\\n \\\"LOCATE('--newfi-portal200', template) > 0 AS css_marker \\\" .\\n \\\"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\\\"\\n);\\n$result = [\\n 'hostname' => gethostname(),\\n 'application' => [\\n 'boardActive' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\\\"),\\n 'boardUrl' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\\\"),\\n 'indexRoute' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\\\"),\\n 'defaultStyleId' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\\\"),\\n 'error_count' => (int)$db->fetchOne(\\\"SELECT COUNT(*) FROM xf_error_log\\\"),\\n 'error_max_id' => (int)$db->fetchOne(\\\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\\\")\\n ],\\n 'addon' => $db->fetchRow(\\n \\\"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \\\" .\\n \\\"FROM xf_addon WHERE addon_id='Newfi/Portal'\\\"\\n ),\\n 'source' => [\\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\\n ],\\n 'templates' => $templates\\n];\\necho \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\";\\necho json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\\necho \\\"\\\\nPORTAL200_INSPECT_JSON_END\\\\n\\\";\\n?>\"},{\"lineno\":194,\"segment\":\"r'''addOnManager()->getById('Newfi/Portal');\\nif (!$addOn || !$addOn->isInstalled()) {\\n fwrite(STDERR, \\\"ADDON_NOT_INSTALLED\\\\n\\\");\\n exit(30);\\n}\\n$addOn->preRebuild();\\n$dataManager = $app->addOnDataManager();\\n$imported = [];\\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\\n $handler = $dataManager->getDataTypeHandler($dataType);\\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\\n if (!$xml || !$xml->count()) {\\n $handler->deleteAddOnData($addOn->addon_id);\\n continue;\\n }\\n $start = 0;\\n $guard = 0;\\n while (true) {\\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\\n if (!$last) { break; }\\n if ($last <= $start || ++$guard > 100000) {\\n fwrite(STDERR, \\\"IMPORT_PROGRESS_INVALID\\\\n\\\");\\n exit(31);\\n }\\n $start = $last;\\n }\\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\\n \\\\XF::triggerRunOnce();\\n $imported[] = $handler->getContainerTag();\\n}\\n$addOn->postDataImport();\\n$repo = $app->repository(\\\\XF\\\\Repository\\\\StyleRepository::class);\\n$repo->updateAllStylesLastModifiedDateLater();\\n$addOn->postRebuild();\\necho \\\"PORTAL200_IMPORT_JSON=\\\" . json_encode(\\n ['result'=>'PASS','containers'=>$imported],\\n JSON_UNESCAPED_SLASHES\\n) . \\\"\\\\n\\\";\\n?>'''\",\"value\":\"addOnManager()->getById('Newfi/Portal');\\nif (!$addOn || !$addOn->isInstalled()) {\\n fwrite(STDERR, \\\"ADDON_NOT_INSTALLED\\\\n\\\");\\n exit(30);\\n}\\n$addOn->preRebuild();\\n$dataManager = $app->addOnDataManager();\\n$imported = [];\\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\\n $handler = $dataManager->getDataTypeHandler($dataType);\\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\\n if (!$xml || !$xml->count()) {\\n $handler->deleteAddOnData($addOn->addon_id);\\n continue;\\n }\\n $start = 0;\\n $guard = 0;\\n while (true) {\\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\\n if (!$last) { break; }\\n if ($last <= $start || ++$guard > 100000) {\\n fwrite(STDERR, \\\"IMPORT_PROGRESS_INVALID\\\\n\\\");\\n exit(31);\\n }\\n $start = $last;\\n }\\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\\n \\\\XF::triggerRunOnce();\\n $imported[] = $handler->getContainerTag();\\n}\\n$addOn->postDataImport();\\n$repo = $app->repository(\\\\XF\\\\Repository\\\\StyleRepository::class);\\n$repo->updateAllStylesLastModifiedDateLater();\\n$addOn->postRebuild();\\necho \\\"PORTAL200_IMPORT_JSON=\\\" . json_encode(\\n ['result'=>'PASS','containers'=>$imported],\\n JSON_UNESCAPED_SLASHES\\n) . \\\"\\\\n\\\";\\n?>\"},{\"lineno\":258,\"segment\":\"r'''$state];\\nif ($state === 'new') {\\n $service = new \\\\Newfi\\\\Portal\\\\Service\\\\PortalData($app);\\n $portal = $service->build();\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\\n $result['sections'] = [];\\n foreach (['forums','resources','media','events','dogs'] as $key) {\\n $section = $portal[$key] ?? null;\\n if (!is_array($section)) { fwrite(STDERR, \\\"SECTION_MISSING\\\\n\\\"); exit(60); }\\n $result['sections'][$key] = [\\n 'available'=>(bool)($section['available'] ?? false),\\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\\n 'status'=>(string)($section['status'] ?? '')\\n ];\\n }\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\\\"1\\\"') !== false;\\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\\\"resources\\\"') !== false;\\n $result['locked_media'] = strpos($html, 'data-portal-locked=\\\"media\\\"') !== false;\\n $result['html_bytes'] = strlen($html);\\n if (!$result['html_marker']) { fwrite(STDERR, \\\"HTML_MARKER_MISSING\\\\n\\\"); exit(61); }\\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\\n fwrite(STDERR, \\\"GUEST_PERMISSION_CONTRACT_CHANGED\\\\n\\\"); exit(62);\\n }\\n if (!$result['locked_resources'] || !$result['locked_media']) {\\n fwrite(STDERR, \\\"LOCKED_COPY_MISSING\\\\n\\\"); exit(63);\\n }\\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\\n fwrite(STDERR, \\\"PUBLIC_SECTION_UNAVAILABLE\\\\n\\\"); exit(64);\\n }\\n} else if ($state === 'old') {\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\\n $result['o\",\"value\":\"$state];\\nif ($state === 'new') {\\n $service = new \\\\Newfi\\\\Portal\\\\Service\\\\PortalData($app);\\n $portal = $service->build();\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\\n $result['sections'] = [];\\n foreach (['forums','resources','media','events','dogs'] as $key) {\\n $section = $portal[$key] ?? null;\\n if (!is_array($section)) { fwrite(STDERR, \\\"SECTION_MISSING\\\\n\\\"); exit(60); }\\n $result['sections'][$key] = [\\n 'available'=>(bool)($section['available'] ?? false),\\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\\n 'status'=>(string)($section['status'] ?? '')\\n ];\\n }\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\\\"1\\\"') !== false;\\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\\\"resources\\\"') !== false;\\n $result['locked_media'] = strpos($html, 'data-portal-locked=\\\"media\\\"') !== false;\\n $result['html_bytes'] = strlen($html);\\n if (!$result['html_marker']) { fwrite(STDERR, \\\"HTML_MARKER_MISSING\\\\n\\\"); exit(61); }\\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\\n fwrite(STDERR, \\\"GUEST_PERMISSION_CONTRACT_CHANGED\\\\n\\\"); exit(62);\\n }\\n if (!$result['locked_resources'] || !$result['locked_media']) {\\n fwrite(STDERR, \\\"LOCKED_COPY_MISSING\\\\n\\\"); exit(63);\\n }\\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\\n fwrite(STDERR, \\\"PUBLIC_SECTION_UNAVAILABLE\\\\n\\\"); exit(64);\\n }\\n} else if ($state === 'old') {\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\\n $result['html_bytes'] = strlen($html);\\n if ($result['html_marker'] || !$result['old_copy']) {\\n fwrite(STDERR, \\\"OLD_RENDER_CONTRACT\\\\n\\\"); exit(65);\\n }\\n} else {\\n exit(66);\\n}\\necho \\\"PORTAL200_RUNTIME_JSON=\\\" . json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n) . \\\"\\\\n\\\";\\n?>\"},{\"lineno\":311,\"segment\":\"r'''import html,json,re,subprocess,sys,urllib.parse\\nmode=sys.argv[1]\\nstate=sys.argv[2]\\nif mode=='staging':\\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\\nelif mode=='prod':\\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\\nelse:\\n raise SystemExit(40)\\ndef fetch(url):\\n cmd=[\\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\\n ]\\n if mode=='staging':\\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\\n cmd += [\\n '-o','-','-w','\\\\n__PORTAL_STATUS__%{http_code}',url\\n ]\\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\\n if p.returncode!=0: raise SystemExit(41)\\n text=p.stdout.decode('utf-8','replace')\\n marker='\\\\n__PORTAL_STATUS__'\\n if marker not in text: raise SystemExit(42)\\n body,status=text.rsplit(marker,1)\\n return int(status.strip()),body\\nroot=f'{scheme}://{host}/'\\nstatus,body=fetch(root)\\nhrefs=[]\\nfor raw in re.findall(r\\\"href=[\\\\\\\"']([^\\\\\\\"']*css\\\\.php[^\\\\\\\"']*)[\\\\\\\"']\\\",body,re.I):\\n raw=html.unescape(raw)\\n hrefs.append(urllib.parse.urljoin(root,raw))\\nnormal_css=[]\\nnormal_status=[]\\nfor u in hrefs:\\n s,b=fetch(u)\\n normal_status.append(s)\\n normal_css.append(b)\\nnormal_css_text='\\\\n'.join(normal_css)\\nforced_css_marker=None\\nforced_css_status=None\\nif mode=='staging' and state=='new' and hrefs:\\n u=urllib.parse.urlsplit(hrefs[0])\\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\\n out=[]\\n found=False\\n for k,v in pairs:\\n if k=='css':\\n found=True\\n parts=[x for x in v.split(',') if x]\\n if 'public:newfi_portal.less' not in parts:\\n parts.append('public:newfi_portal.less')\\n v=','.join(parts)\\n out.append((k,v))\\n if found:\\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\\n forced_css_status,forced_body=fetch(forced)\\n forced_css_marker='--newfi-portal200' in \",\"value\":\"import html,json,re,subprocess,sys,urllib.parse\\nmode=sys.argv[1]\\nstate=sys.argv[2]\\nif mode=='staging':\\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\\nelif mode=='prod':\\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\\nelse:\\n raise SystemExit(40)\\ndef fetch(url):\\n cmd=[\\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\\n ]\\n if mode=='staging':\\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\\n cmd += [\\n '-o','-','-w','\\\\n__PORTAL_STATUS__%{http_code}',url\\n ]\\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\\n if p.returncode!=0: raise SystemExit(41)\\n text=p.stdout.decode('utf-8','replace')\\n marker='\\\\n__PORTAL_STATUS__'\\n if marker not in text: raise SystemExit(42)\\n body,status=text.rsplit(marker,1)\\n return int(status.strip()),body\\nroot=f'{scheme}://{host}/'\\nstatus,body=fetch(root)\\nhrefs=[]\\nfor raw in re.findall(r\\\"href=[\\\\\\\"']([^\\\\\\\"']*css\\\\.php[^\\\\\\\"']*)[\\\\\\\"']\\\",body,re.I):\\n raw=html.unescape(raw)\\n hrefs.append(urllib.parse.urljoin(root,raw))\\nnormal_css=[]\\nnormal_status=[]\\nfor u in hrefs:\\n s,b=fetch(u)\\n normal_status.append(s)\\n normal_css.append(b)\\nnormal_css_text='\\\\n'.join(normal_css)\\nforced_css_marker=None\\nforced_css_status=None\\nif mode=='staging' and state=='new' and hrefs:\\n u=urllib.parse.urlsplit(hrefs[0])\\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\\n out=[]\\n found=False\\n for k,v in pairs:\\n if k=='css':\\n found=True\\n parts=[x for x in v.split(',') if x]\\n if 'public:newfi_portal.less' not in parts:\\n parts.append('public:newfi_portal.less')\\n v=','.join(parts)\\n out.append((k,v))\\n if found:\\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\\n forced_css_status,forced_body=fetch(forced)\\n forced_css_marker='--newfi-portal200' in forced_body\\nresult={\\n 'status':status,\\n 'bytes':len(body.encode()),\\n 'html_marker':'data-newfi-portal200=\\\"1\\\"' in body,\\n 'css_marker':'--newfi-portal200' in normal_css_text,\\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\\n 'css_count':len(hrefs),\\n 'css_status':normal_status,\\n 'forced_css_status':forced_css_status,\\n 'forced_css_marker':forced_css_marker,\\n}\\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\\nif status!=expected_status: raise SystemExit(45)\\nif mode=='prod':\\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\\nelif mode=='staging':\\n if any(x!=200 for x in normal_status): raise SystemExit(46)\\nelse:\\n raise SystemExit(50)\\nif mode=='prod' and state=='new':\\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\\nelif mode=='prod' and state=='old':\\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\\nelif mode=='staging' and state=='new':\\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\\nelif mode=='staging' and state=='old':\\n pass\\nelse:\\n raise SystemExit(50)\\n\"},{\"lineno\":14,\"segment\":\"\\\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\\\"\",\"value\":\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\"},{\"lineno\":20,\"segment\":\"\\\"_data/templates.xml\\\"\",\"value\":\"_data/templates.xml\"},{\"lineno\":24,\"segment\":\"\\\"_data/templates.xml\\\"\",\"value\":\"_data/templates.xml\"},{\"lineno\":27,\"segment\":\"/root/newfi-portal200-backup-\",\"value\":\"/root/newfi-portal200-backup-\"},{\"lineno\":401,\"segment\":\"\\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\"\",\"value\":\"PORTAL200_INSPECT_JSON_BEGIN\\n\"},{\"lineno\":402,\"segment\":\"\\\"\\\\nPORTAL200_INSPECT_JSON_END\\\"\",\"value\":\"\\nPORTAL200_INSPECT_JSON_END\"},{\"lineno\":734,\"segment\":\"\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\\\"\",\"value\":\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\"},{\"lineno\":736,\"segment\":\"\\\"PORTAL200_FLOW\\\"\",\"value\":\"PORTAL200_FLOW\"},{\"lineno\":737,\"segment\":\"\\\"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\\\"\",\"value\":\"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\"},{\"lineno\":497,\"segment\":\"\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest ! -e \\\"$B\\\"\\ninstall -d -m 700 \\\"$B/Pub/Controller\\\" \\\"$B/_data\\\"\\ncp -p -- \\\"$P/Pub/Controller/Portal.php\\\" \\\"$B/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$P/_data/templates.xml\\\" \\\"$B/_data/templates.xml\\\"\\ntest ! -e \\\"$P/Service/PortalData.php\\\"\\nprintf 'PORTAL200_BACKUP=PASS\\\\n'\\n\",\"value\":\"\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest ! -e \\\"$B\\\"\\ninstall -d -m 700 \\\"$B/Pub/Controller\\\" \\\"$B/_data\\\"\\ncp -p -- \\\"$P/Pub/Controller/Portal.php\\\" \\\"$B/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$P/_data/templates.xml\\\" \\\"$B/_data/templates.xml\\\"\\ntest ! -e \\\"$P/Service/PortalData.php\\\"\\nprintf 'PORTAL200_BACKUP=PASS\\n'\\n\"},{\"lineno\":507,\"segment\":\"\\\"PORTAL200_BACKUP=PASS\\\"\",\"value\":\"PORTAL200_BACKUP=PASS\"},{\"lineno\":523,\"segment\":\"\\\"; printf 'PORTAL200_WRITE=PASS\\\\\\\\n'\\\"\",\"value\":\"; printf 'PORTAL200_WRITE=PASS\\\\n'\"},{\"lineno\":531,\"segment\":\"\\\"PORTAL200_WRITE=PASS\\\"\",\"value\":\"PORTAL200_WRITE=PASS\"},{\"lineno\":539,\"segment\":\"\\\"_data/templates.xml\\\"\",\"value\":\"_data/templates.xml\"},{\"lineno\":546,\"segment\":\"\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest -f \\\"$B/Pub/Controller/Portal.php\\\"\\ntest -f \\\"$B/_data/templates.xml\\\"\\ncp -p -- \\\"$B/Pub/Controller/Portal.php\\\" \\\"$P/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$B/_data/templates.xml\\\" \\\"$P/_data/templates.xml\\\"\\nrm -f -- \\\"$P/Service/PortalData.php\\\"\\nrmdir -- \\\"$P/Service\\\" 2>/dev/null || true\\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\\\n'\\n\",\"value\":\"\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest -f \\\"$B/Pub/Controller/Portal.php\\\"\\ntest -f \\\"$B/_data/templates.xml\\\"\\ncp -p -- \\\"$B/Pub/Controller/Portal.php\\\" \\\"$P/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$B/_data/templates.xml\\\" \\\"$P/_data/templates.xml\\\"\\nrm -f -- \\\"$P/Service/PortalData.php\\\"\\nrmdir -- \\\"$P/Service\\\" 2>/dev/null || true\\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\\n\"},{\"lineno\":557,\"segment\":\"\\\"PORTAL200_SOURCE_RESTORE=PASS\\\"\",\"value\":\"PORTAL200_SOURCE_RESTORE=PASS\"},{\"lineno\":568,\"segment\":\"\\\"PORTAL200_IMPORT_JSON=\\\"\",\"value\":\"PORTAL200_IMPORT_JSON=\"},{\"lineno\":585,\"segment\":\"\\\"PORTAL200_RUNTIME\\\"\",\"value\":\"PORTAL200_RUNTIME\"},{\"lineno\":647,\"segment\":\"\\\"PORTAL200_HTTP\\\"\",\"value\":\"PORTAL200_HTTP\"},{\"lineno\":715,\"segment\":\"\\\"PORTAL200_CLUSTER\\\"\",\"value\":\"PORTAL200_CLUSTER\"},{\"lineno\":758,\"segment\":\"\\\"\\\"\",\"value\":\"\"},{\"lineno\":781,\"segment\":\"\\\"PORTAL200_CANDIDATE_IDENTITY\\\"\",\"value\":\"PORTAL200_CANDIDATE_IDENTITY\"},{\"lineno\":783,\"segment\":\"\\\"PORTAL200_ORCHESTRATION_FIXES\\\"\",\"value\":\"PORTAL200_ORCHESTRATION_FIXES\"},{\"lineno\":800,\"segment\":\"\\\"PORTAL200_GIT_PRESTATE\\\"\",\"value\":\"PORTAL200_GIT_PRESTATE\"},{\"lineno\":811,\"segment\":\"\\\"PORTAL200_RUNTIME_HELPER_LINT\\\"\",\"value\":\"PORTAL200_RUNTIME_HELPER_LINT\"},{\"lineno\":825,\"segment\":\"\\\"PORTAL200_ERROR_BASELINE\\\"\",\"value\":\"PORTAL200_ERROR_BASELINE\"},{\"lineno\":831,\"segment\":\"\\\"PORTAL200_PREFLIGHT\\\"\",\"value\":\"PORTAL200_PREFLIGHT\"},{\"lineno\":839,\"segment\":\"\\\"PORTAL200_STAGING_FIRST_APPLY\\\"\",\"value\":\"PORTAL200_STAGING_FIRST_APPLY\"},{\"lineno\":845,\"segment\":\"\\\"PORTAL200_STAGING_ROLLBACK_PROOF\\\"\",\"value\":\"PORTAL200_STAGING_ROLLBACK_PROOF\"},{\"lineno\":852,\"segment\":\"\\\"PORTAL200_STAGING_FINAL\\\"\",\"value\":\"PORTAL200_STAGING_FINAL\"},{\"lineno\":885,\"segment\":\"\\\"PORTAL200_GIT_PROMOTION\\\"\",\"value\":\"PORTAL200_GIT_PROMOTION\"},{\"lineno\":905,\"segment\":\"\\\"PORTAL200_PRODUCTION\\\"\",\"value\":\"PORTAL200_PRODUCTION\"},{\"lineno\":922,\"segment\":\"\\\"PORTAL200_FINAL_EVIDENCE\\\"\",\"value\":\"PORTAL200_FINAL_EVIDENCE\"},{\"lineno\":931,\"segment\":\"\\\"PORTAL200_PORTAL\\\"\",\"value\":\"PORTAL200_PORTAL\"},{\"lineno\":932,\"segment\":\"\\\"PORTAL200_STAGING\\\"\",\"value\":\"PORTAL200_STAGING\"},{\"lineno\":933,\"segment\":\"\\\"PORTAL200_PRODUCTION\\\"\",\"value\":\"PORTAL200_PRODUCTION\"},{\"lineno\":934,\"segment\":\"\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\\\"\",\"value\":\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\"},{\"lineno\":460,\"segment\":\"1000010\",\"value\":1000010},{\"lineno\":484,\"segment\":\"\\\"PORTAL200_NEW_XF_ERRORS\\\"\",\"value\":\"PORTAL200_NEW_XF_ERRORS\"},{\"lineno\":763,\"segment\":\"\\\"_data/templates.xml\\\"\",\"value\":\"_data/templates.xml\"},{\"lineno\":766,\"segment\":\"\\\"CANDIDATE_TEMPLATE_SET\\\"\",\"value\":\"CANDIDATE_TEMPLATE_SET\"},{\"lineno\":780,\"segment\":\"\\\"CANDIDATE_MUTATION_SCAN\\\"\",\"value\":\"CANDIDATE_MUTATION_SCAN\"},{\"lineno\":941,\"segment\":\"\\\"PORTAL200_FAILURE\\\"\",\"value\":\"PORTAL200_FAILURE\"},{\"lineno\":1004,\"segment\":\"\\\"PORTAL200_ROADMAP_REMAINS\\\"\",\"value\":\"PORTAL200_ROADMAP_REMAINS\"},{\"lineno\":1009,\"segment\":\"\\\"PORTAL200_FAILURE\\\"\",\"value\":\"PORTAL200_FAILURE\"},{\"lineno\":1016,\"segment\":\"\\\"PORTAL200_ROADMAP_REMAINS\\\"\",\"value\":\"PORTAL200_ROADMAP_REMAINS\"},{\"lineno\":461,\"segment\":\"\\\"version_string\\\"\",\"value\":\"version_string\"},{\"lineno\":618,\"segment\":\"\\\"PORTAL200_HTTP_EXEC\\\"\",\"value\":\"PORTAL200_HTTP_EXEC\"},{\"lineno\":762,\"segment\":\"\\\"CANDIDATE_IDENTITY\\\"\",\"value\":\"CANDIDATE_IDENTITY\"},{\"lineno\":460,\"segment\":\"\\\"version_id\\\"\",\"value\":\"version_id\"},{\"lineno\":576,\"segment\":\"\\\"PORTAL200_RUNTIME_JSON=\\\"\",\"value\":\"PORTAL200_RUNTIME_JSON=\"},{\"lineno\":613,\"segment\":\"\\\"PORTAL200_HTTP\\\"\",\"value\":\"PORTAL200_HTTP\"},{\"lineno\":638,\"segment\":\"\\\"PORTAL200_HTTP_JSON=\\\"\",\"value\":\"PORTAL200_HTTP_JSON=\"},{\"lineno\":991,\"segment\":\"\\\"PORTAL200_FAILURE_GIT_HISTORY\\\"\",\"value\":\"PORTAL200_FAILURE_GIT_HISTORY\"},{\"lineno\":606,\"segment\":\"\\\"PORTAL200_HTTP_JSON=\\\"\",\"value\":\"PORTAL200_HTTP_JSON=\"},{\"lineno\":952,\"segment\":\"\\\"PORTAL200_FAILURE_PROD_ROLLBACK\\\"\",\"value\":\"PORTAL200_FAILURE_PROD_ROLLBACK\"},{\"lineno\":966,\"segment\":\"\\\"PORTAL200_FAILURE_STAGING_ROLLBACK\\\"\",\"value\":\"PORTAL200_FAILURE_STAGING_ROLLBACK\"},{\"lineno\":985,\"segment\":\"\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\"\",\"value\":\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\"},{\"lineno\":955,\"segment\":\"\\\"PORTAL200_FAILURE_PROD_ROLLBACK\\\"\",\"value\":\"PORTAL200_FAILURE_PROD_ROLLBACK\"},{\"lineno\":969,\"segment\":\"\\\"PORTAL200_FAILURE_STAGING_ROLLBACK\\\"\",\"value\":\"PORTAL200_FAILURE_STAGING_ROLLBACK\"},{\"lineno\":987,\"segment\":\"\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\"\",\"value\":\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\"},{\"lineno\":519,\"segment\":\"\\\"/.portal200.XXXXXX\\\"\",\"value\":\"/.portal200.XXXXXX\"}]\nRELEVANT_CALLS=[{\"args\":[\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\"],\"kwargs\":{},\"lineno\":14,\"name\":\"pathlib.Path\",\"source\":\"pathlib.Path(\\\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\\\")\"},{\"args\":[\"src/addons/Newfi/Portal\"],\"kwargs\":{},\"lineno\":15,\"name\":\"pathlib.Path\",\"source\":\"pathlib.Path(\\\"src/addons/Newfi/Portal\\\")\"},{\"args\":[\"PORTAL200_FLOW\",\"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\"],\"kwargs\":{},\"lineno\":735,\"name\":\"emit\",\"source\":\"emit(\\n \\\"PORTAL200_FLOW\\\",\\n \\\"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\\\",\\n)\"},{\"args\":[\"[\\\"/usr/bin/git\\\", \\\"-C\\\", str(WT), *args]\"],\"kwargs\":{\"ok\":\"ok\",\"timeout\":\"timeout\"},\"lineno\":77,\"name\":\"run\",\"source\":\"run([\\\"/usr/bin/git\\\", \\\"-C\\\", str(WT), *args], timeout=timeout, ok=ok)\"},{\"args\":[\"cmd\"],\"kwargs\":{\"input_bytes\":\"input_bytes\",\"timeout\":\"timeout + 30\"},\"lineno\":135,\"name\":\"run\",\"source\":\"run(cmd, input_bytes=input_bytes, timeout=timeout + 30)\"},{\"args\":[\"__STATE__\",\"state\"],\"kwargs\":{},\"lineno\":573,\"name\":\"RUNTIME_VERIFY_PHP_TEMPLATE.replace\",\"source\":\"RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", state)\"},{\"args\":[\"target\",\"new\"],\"kwargs\":{},\"lineno\":659,\"name\":\"runtime_verify\",\"source\":\"runtime_verify(target, \\\"new\\\")\"},{\"args\":[[\"/usr/sbin/qm\",\"status\",\"210\"]],\"kwargs\":{\"timeout\":30},\"lineno\":693,\"name\":\"run\",\"source\":\"run([\\\"/usr/sbin/qm\\\", \\\"status\\\", \\\"210\\\"], timeout=30)\"},{\"args\":[[\"/usr/bin/ssh\",\"-T\",\"-o\",\"BatchMode=yes\",\"-o\",\"StrictHostKeyChecking=yes\",\"-o\",\"UpdateHostKeys=no\",\"-o\",\"ConnectTimeout=10\",\"root@pve03\",\"/usr/sbin/qm status 211\"]],\"kwargs\":{\"timeout\":40},\"lineno\":696,\"name\":\"run\",\"source\":\"run(\\n [\\n \\\"/usr/bin/ssh\\\",\\n \\\"-T\\\",\\n \\\"-o\\\",\\n \\\"BatchMode=yes\\\",\\n \\\"-o\\\",\\n \\\"StrictHostKeyChecking=yes\\\",\\n \\\"-o\\\",\\n \\\"UpdateHostKeys=no\\\",\\n \\\"-o\\\",\\n \\\"ConnectTimeout=10\\\",\\n \\\"root@pve03\\\",\\n \\\"/usr/sbin/qm status 211\\\",\\n ],\\n timeout=40,\\n )\"},{\"args\":[\"CAND_ROOT / \\\"_data/templates.xml\\\"\"],\"kwargs\":{},\"lineno\":763,\"name\":\"ET.parse\",\"source\":\"ET.parse(CAND_ROOT / \\\"_data/templates.xml\\\")\"},{\"args\":[\"utf-8\"],\"kwargs\":{},\"lineno\":767,\"name\":\"read_text\",\"source\":\"(CAND_ROOT / \\\"Service/PortalData.php\\\").read_text(\\\"utf-8\\\")\"},{\"args\":[\"PORTAL200_CANDIDATE_IDENTITY\",\"PASS\"],\"kwargs\":{},\"lineno\":781,\"name\":\"emit\",\"source\":\"emit(\\\"PORTAL200_CANDIDATE_IDENTITY\\\", \\\"PASS\\\")\"},{\"args\":[\"prod\",\"old\"],\"kwargs\":{},\"lineno\":827,\"name\":\"runtime_verify\",\"source\":\"runtime_verify(\\\"prod\\\", \\\"old\\\")\"},{\"args\":[\"staging\"],\"kwargs\":{},\"lineno\":836,\"name\":\"install_candidate\",\"source\":\"install_candidate(\\\"staging\\\")\"},{\"args\":[\"staging\"],\"kwargs\":{},\"lineno\":848,\"name\":\"install_candidate\",\"source\":\"install_candidate(\\\"staging\\\")\"},{\"args\":[\"prod\"],\"kwargs\":{},\"lineno\":902,\"name\":\"install_candidate\",\"source\":\"install_candidate(\\\"prod\\\")\"},{\"args\":[\"cmd\"],\"kwargs\":{\"check\":false,\"input\":\"input_bytes\",\"stderr\":\"subprocess.PIPE\",\"stdout\":\"subprocess.PIPE\",\"timeout\":\"timeout\"},\"lineno\":61,\"name\":\"subprocess.run\",\"source\":\"subprocess.run(\\n cmd,\\n input=input_bytes,\\n stdout=subprocess.PIPE,\\n stderr=subprocess.PIPE,\\n timeout=timeout,\\n check=False,\\n )\"},{\"args\":[\"target\",\"rel\",\"(CAND_ROOT / rel).read_bytes()\"],\"kwargs\":{},\"lineno\":541,\"name\":\"guest_write\",\"source\":\"guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\"},{\"args\":[\"target\",\"old\"],\"kwargs\":{},\"lineno\":674,\"name\":\"runtime_verify\",\"source\":\"runtime_verify(target, \\\"old\\\")\"},{\"args\":[\"CANDIDATE_TEMPLATE_SET\"],\"kwargs\":{},\"lineno\":766,\"name\":\"fail\",\"source\":\"fail(\\\"CANDIDATE_TEMPLATE_SET\\\")\"},{\"args\":[\"CANDIDATE_MUTATION_SCAN\"],\"kwargs\":{},\"lineno\":780,\"name\":\"fail\",\"source\":\"fail(\\\"CANDIDATE_MUTATION_SCAN\\\")\"},{\"args\":[\"__STATE__\",\"new\"],\"kwargs\":{},\"lineno\":807,\"name\":\"RUNTIME_VERIFY_PHP_TEMPLATE.replace\",\"source\":\"RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", \\\"new\\\")\"},{\"args\":[\"__STATE__\",\"new\"],\"kwargs\":{},\"lineno\":810,\"name\":\"RUNTIME_VERIFY_PHP_TEMPLATE.replace\",\"source\":\"RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", \\\"new\\\")\"},{\"args\":[\"CAND_ROOT / rel\",\"dest\"],\"kwargs\":{},\"lineno\":858,\"name\":\"shutil.copyfile\",\"source\":\"shutil.copyfile(CAND_ROOT / rel, dest)\"},{\"args\":[\"addon.get(\\\"version_id\\\", 0)\"],\"kwargs\":{},\"lineno\":460,\"name\":\"int\",\"source\":\"int(addon.get(\\\"version_id\\\", 0))\"},{\"args\":[\"version_string\"],\"kwargs\":{},\"lineno\":461,\"name\":\"addon.get\",\"source\":\"addon.get(\\\"version_string\\\")\"},{\"args\":[],\"kwargs\":{},\"lineno\":541,\"name\":\"read_bytes\",\"source\":\"(CAND_ROOT / rel).read_bytes()\"},{\"args\":[\"argv\"],\"kwargs\":{\"check\":false,\"input\":\"HTTP_VERIFY_PY.encode()\",\"stderr\":\"subprocess.PIPE\",\"stdout\":\"subprocess.PIPE\",\"timeout\":180},\"lineno\":593,\"name\":\"subprocess.run\",\"source\":\"subprocess.run(\\n argv,\\n input=HTTP_VERIFY_PY.encode(),\\n stdout=subprocess.PIPE,\\n stderr=subprocess.PIPE,\\n timeout=180,\\n check=False,\\n )\"},{\"args\":[\"CANDIDATE_IDENTITY\"],\"kwargs\":{},\"lineno\":762,\"name\":\"fail\",\"source\":\"fail(\\\"CANDIDATE_IDENTITY\\\")\"},{\"args\":[\"version_id\",0],\"kwargs\":{},\"lineno\":460,\"name\":\"addon.get\",\"source\":\"addon.get(\\\"version_id\\\", 0)\"},{\"args\":[\"old\"],\"kwargs\":{},\"lineno\":979,\"name\":\"dest.write_bytes\",\"source\":\"dest.write_bytes(old)\"}]\nRELEVANT_SOURCE_CONTEXTS=[{\"line\":10,\"text\":\"import subprocess\\nimport sys\\nimport xml.etree.ElementTree as ET\\n\\nCID = \\\"NEWFI-260907-PORTAL200-APPLY3\\\"\\nBRANCH = \\\"implementation/newfi-closed-mvp-20260906\\\"\\nOLD_HEAD = \\\"06bab5acd124e768f70305b3d7eca2f595871890\\\"\\nWT = pathlib.Path(\\\"/root/newfi-portal/.worktrees/implementation-newfi-closed-mvp-20260906\\\")\\nCAND_ROOT = pathlib.Path(\\\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\\\")\\nPORTAL_REL = pathlib.Path(\\\"src/addons/Newfi/Portal\\\")\\nGIT_PORTAL = WT / PORTAL_REL\\nEXPECTED_NEW = {\\n \\\"Pub/Controller/Portal.php\\\": (363, \\\"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\\\"),\"},{\"line\":14,\"text\":\"CID = \\\"NEWFI-260907-PORTAL200-APPLY3\\\"\\nBRANCH = \\\"implementation/newfi-closed-mvp-20260906\\\"\\nOLD_HEAD = \\\"06bab5acd124e768f70305b3d7eca2f595871890\\\"\\nWT = pathlib.Path(\\\"/root/newfi-portal/.worktrees/implementation-newfi-closed-mvp-20260906\\\")\\nCAND_ROOT = pathlib.Path(\\\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\\\")\\nPORTAL_REL = pathlib.Path(\\\"src/addons/Newfi/Portal\\\")\\nGIT_PORTAL = WT / PORTAL_REL\\nEXPECTED_NEW = {\\n \\\"Pub/Controller/Portal.php\\\": (363, \\\"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\\\"),\\n \\\"Service/PortalData.php\\\": (5213, \\\"4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d\\\"),\\n \\\"_data/templates.xml\\\": (10977, \\\"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\\\"),\\n}\\nEXPECTED_OLD = {\"},{\"line\":20,\"text\":\"GIT_PORTAL = WT / PORTAL_REL\\nEXPECTED_NEW = {\\n \\\"Pub/Controller/Portal.php\\\": (363, \\\"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\\\"),\\n \\\"Service/PortalData.php\\\": (5213, \\\"4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d\\\"),\\n \\\"_data/templates.xml\\\": (10977, \\\"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\\\"),\\n}\\nEXPECTED_OLD = {\\n \\\"Pub/Controller/Portal.php\\\": (247, \\\"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\\\"),\\n \\\"_data/templates.xml\\\": (1461, \\\"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\\\"),\\n}\\nEXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)]\\nBACKUP = f\\\"/root/newfi-portal200-backup-{CID}\\\"\\n\"},{\"line\":24,\"text\":\" \\\"_data/templates.xml\\\": (10977, \\\"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\\\"),\\n}\\nEXPECTED_OLD = {\\n \\\"Pub/Controller/Portal.php\\\": (247, \\\"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\\\"),\\n \\\"_data/templates.xml\\\": (1461, \\\"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\\\"),\\n}\\nEXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)]\\nBACKUP = f\\\"/root/newfi-portal200-backup-{CID}\\\"\\n\\n\\nclass PortalError(RuntimeError):\\n pass\\n\"},{\"line\":27,\"text\":\" \\\"Pub/Controller/Portal.php\\\": (247, \\\"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\\\"),\\n \\\"_data/templates.xml\\\": (1461, \\\"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\\\"),\\n}\\nEXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)]\\nBACKUP = f\\\"/root/newfi-portal200-backup-{CID}\\\"\\n\\n\\nclass PortalError(RuntimeError):\\n pass\\n\\n\\ndef emit(key, value=None):\\n if value is None:\"},{\"line\":160,\"text\":\"}\\n$portal = $root . '/src/addons/Newfi/Portal';\\n$templates = $db->fetchAll(\\n \\\"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \\\" .\\n \\\"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \\\" .\\n \\\"LOCATE('--newfi-portal200', template) > 0 AS css_marker \\\" .\\n \\\"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\\\"\\n);\\n$result = [\\n 'hostname' => gethostname(),\\n 'application' => [\\n 'boardActive' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\\\"),\\n 'boardUrl' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\\\"),\"},{\"line\":161,\"text\":\"$portal = $root . '/src/addons/Newfi/Portal';\\n$templates = $db->fetchAll(\\n \\\"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \\\" .\\n \\\"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \\\" .\\n \\\"LOCATE('--newfi-portal200', template) > 0 AS css_marker \\\" .\\n \\\"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\\\"\\n);\\n$result = [\\n 'hostname' => gethostname(),\\n 'application' => [\\n 'boardActive' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\\\"),\\n 'boardUrl' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\\\"),\\n 'indexRoute' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\\\"),\"},{\"line\":175,\"text\":\" 'error_count' => (int)$db->fetchOne(\\\"SELECT COUNT(*) FROM xf_error_log\\\"),\\n 'error_max_id' => (int)$db->fetchOne(\\\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\\\")\\n ],\\n 'addon' => $db->fetchRow(\\n \\\"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \\\" .\\n \\\"FROM xf_addon WHERE addon_id='Newfi/Portal'\\\"\\n ),\\n 'source' => [\\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\\n ],\\n 'templates' => $templates\"},{\"line\":181,\"text\":\" ),\\n 'source' => [\\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\\n ],\\n 'templates' => $templates\\n];\\necho \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\";\\necho json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\"},{\"line\":185,\"text\":\" '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\\n ],\\n 'templates' => $templates\\n];\\necho \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\";\\necho json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\\necho \\\"\\\\nPORTAL200_INSPECT_JSON_END\\\\n\\\";\\n?>'''\\n\\n\"},{\"line\":190,\"text\":\"echo json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\\necho \\\"\\\\nPORTAL200_INSPECT_JSON_END\\\\n\\\";\\n?>'''\\n\\n\\nIMPORT_PHP = r'''postDataImport();\\n$repo = $app->repository(\\\\XF\\\\Repository\\\\StyleRepository::class);\\n$repo->updateAllStylesLastModifiedDateLater();\\n$addOn->postRebuild();\\necho \\\"PORTAL200_IMPORT_JSON=\\\" . json_encode(\\n ['result'=>'PASS','containers'=>$imported],\\n JSON_UNESCAPED_SLASHES\\n) . \\\"\\\\n\\\";\\n?>'''\\n\\n\\nERRORS_PHP_TEMPLATE = r'''(bool)($section['hasItems'] ?? false),\\n 'status'=>(string)($section['status'] ?? '')\\n ];\\n }\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\\\"1\\\"') !== false;\\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\\\"resources\\\"') !== false;\\n $result['locked_media'] = strpos($html, 'data-portal-locked=\\\"media\\\"') !== false;\\n $result['html_bytes'] = strlen($html);\\n if (!$result['html_marker']) { fwrite(STDERR, \\\"HTML_MARKER_MISSING\\\\n\\\"); exit(61); }\\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\\n fwrite(STDERR, \\\"GUEST_PERMISSION_CONTRACT_CHANGED\\\\n\\\"); exit(62);\\n }\\n if (!$result['locked_resources'] || !$result['locked_media']) {\"},{\"line\":288,\"text\":\" if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\\n fwrite(STDERR, \\\"GUEST_PERMISSION_CONTRACT_CHANGED\\\\n\\\"); exit(62);\\n }\\n if (!$result['locked_resources'] || !$result['locked_media']) {\\n fwrite(STDERR, \\\"LOCKED_COPY_MISSING\\\\n\\\"); exit(63);\\n }\\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\\n fwrite(STDERR, \\\"PUBLIC_SECTION_UNAVAILABLE\\\\n\\\"); exit(64);\\n }\\n} else if ($state === 'old') {\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\"},{\"line\":295,\"text\":\" fwrite(STDERR, \\\"PUBLIC_SECTION_UNAVAILABLE\\\\n\\\"); exit(64);\\n }\\n} else if ($state === 'old') {\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\\n $result['html_bytes'] = strlen($html);\\n if ($result['html_marker'] || !$result['old_copy']) {\\n fwrite(STDERR, \\\"OLD_RENDER_CONTRACT\\\\n\\\"); exit(65);\\n }\\n} else {\\n exit(66);\\n}\"},{\"line\":296,\"text\":\" }\\n} else if ($state === 'old') {\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\\n $result['html_bytes'] = strlen($html);\\n if ($result['html_marker'] || !$result['old_copy']) {\\n fwrite(STDERR, \\\"OLD_RENDER_CONTRACT\\\\n\\\"); exit(65);\\n }\\n} else {\\n exit(66);\\n}\\necho \\\"PORTAL200_RUNTIME_JSON=\\\" . json_encode(\"},{\"line\":298,\"text\":\" $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\\n $result['html_bytes'] = strlen($html);\\n if ($result['html_marker'] || !$result['old_copy']) {\\n fwrite(STDERR, \\\"OLD_RENDER_CONTRACT\\\\n\\\"); exit(65);\\n }\\n} else {\\n exit(66);\\n}\\necho \\\"PORTAL200_RUNTIME_JSON=\\\" . json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\"},{\"line\":304,\"text\":\" }\\n} else {\\n exit(66);\\n}\\necho \\\"PORTAL200_RUNTIME_JSON=\\\" . json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n) . \\\"\\\\n\\\";\\n?>'''\\n\\n\\nHTTP_VERIFY_PY = r'''import html,json,re,subprocess,sys,urllib.parse\\nmode=sys.argv[1]\"},{\"line\":367,\"text\":\" out.append((k,v))\\n if found:\\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\\n forced_css_status,forced_body=fetch(forced)\\n forced_css_marker='--newfi-portal200' in forced_body\\nresult={\\n 'status':status,\\n 'bytes':len(body.encode()),\\n 'html_marker':'data-newfi-portal200=\\\"1\\\"' in body,\\n 'css_marker':'--newfi-portal200' in normal_css_text,\\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\\n 'css_count':len(hrefs),\\n 'css_status':normal_status,\"},{\"line\":371,\"text\":\" forced_css_marker='--newfi-portal200' in forced_body\\nresult={\\n 'status':status,\\n 'bytes':len(body.encode()),\\n 'html_marker':'data-newfi-portal200=\\\"1\\\"' in body,\\n 'css_marker':'--newfi-portal200' in normal_css_text,\\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\\n 'css_count':len(hrefs),\\n 'css_status':normal_status,\\n 'forced_css_status':forced_css_status,\\n 'forced_css_marker':forced_css_marker,\\n}\\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\"},{\"line\":372,\"text\":\"result={\\n 'status':status,\\n 'bytes':len(body.encode()),\\n 'html_marker':'data-newfi-portal200=\\\"1\\\"' in body,\\n 'css_marker':'--newfi-portal200' in normal_css_text,\\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\\n 'css_count':len(hrefs),\\n 'css_status':normal_status,\\n 'forced_css_status':forced_css_status,\\n 'forced_css_marker':forced_css_marker,\\n}\\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\\nif status!=expected_status: raise SystemExit(45)\"},{\"line\":373,\"text\":\" 'status':status,\\n 'bytes':len(body.encode()),\\n 'html_marker':'data-newfi-portal200=\\\"1\\\"' in body,\\n 'css_marker':'--newfi-portal200' in normal_css_text,\\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\\n 'css_count':len(hrefs),\\n 'css_status':normal_status,\\n 'forced_css_status':forced_css_status,\\n 'forced_css_marker':forced_css_marker,\\n}\\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\\nif status!=expected_status: raise SystemExit(45)\\nif mode=='prod':\"},{\"line\":379,\"text\":\" 'css_status':normal_status,\\n 'forced_css_status':forced_css_status,\\n 'forced_css_marker':forced_css_marker,\\n}\\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\\nif status!=expected_status: raise SystemExit(45)\\nif mode=='prod':\\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\\nelif mode=='staging':\\n if any(x!=200 for x in normal_status): raise SystemExit(46)\\nelse:\\n raise SystemExit(50)\\nif mode=='prod' and state=='new':\"},{\"line\":390,\"text\":\" raise SystemExit(50)\\nif mode=='prod' and state=='new':\\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\\nelif mode=='prod' and state=='old':\\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\\nelif mode=='staging' and state=='new':\\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\\nelif mode=='staging' and state=='old':\\n pass\\nelse:\\n raise SystemExit(50)\\n'''\\n\"},{\"line\":401,\"text\":\"'''\\n\\n\\ndef parse_inspect(text):\\n b = \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\"\\n e = \\\"\\\\nPORTAL200_INSPECT_JSON_END\\\"\\n if b not in text or e not in text:\\n fail(\\\"INSPECT_MARKERS\\\")\\n try:\\n return json.loads(text.split(b, 1)[1].split(e, 1)[0])\\n except Exception:\\n fail(\\\"INSPECT_JSON\\\")\\n\"},{\"line\":402,\"text\":\"\\n\\ndef parse_inspect(text):\\n b = \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\"\\n e = \\\"\\\\nPORTAL200_INSPECT_JSON_END\\\"\\n if b not in text or e not in text:\\n fail(\\\"INSPECT_MARKERS\\\")\\n try:\\n return json.loads(text.split(b, 1)[1].split(e, 1)[0])\\n except Exception:\\n fail(\\\"INSPECT_JSON\\\")\\n\\n\"},{\"line\":460,\"text\":\" and a.get(\\\"boardUrl\\\") == expected[1]\\n and a.get(\\\"indexRoute\\\") == \\\"portal/\\\"\\n and a.get(\\\"defaultStyleId\\\") == \\\"4\\\"\\n and addon.get(\\\"addon_id\\\") == \\\"Newfi/Portal\\\"\\n and int(addon.get(\\\"version_id\\\", 0)) == 1000010\\n and addon.get(\\\"version_string\\\") == \\\"1.0.0 Alpha 1\\\"\\n and int(addon.get(\\\"active\\\", 0)) == 1\\n and int(addon.get(\\\"is_processing\\\", 1)) == 0\\n )\\n\\n\\ndef new_errors(target, baseline):\\n raw = qga(\"},{\"line\":461,\"text\":\" and a.get(\\\"indexRoute\\\") == \\\"portal/\\\"\\n and a.get(\\\"defaultStyleId\\\") == \\\"4\\\"\\n and addon.get(\\\"addon_id\\\") == \\\"Newfi/Portal\\\"\\n and int(addon.get(\\\"version_id\\\", 0)) == 1000010\\n and addon.get(\\\"version_string\\\") == \\\"1.0.0 Alpha 1\\\"\\n and int(addon.get(\\\"active\\\", 0)) == 1\\n and int(addon.get(\\\"is_processing\\\", 1)) == 0\\n )\\n\\n\\ndef new_errors(target, baseline):\\n raw = qga(\\n target,\"},{\"line\":484,\"text\":\"def assert_no_new_errors(target, baseline, phase):\\n rows = new_errors(target, baseline)\\n if rows:\\n emit(\\n \\\"PORTAL200_NEW_XF_ERRORS\\\",\\n {\\n \\\"target\\\": target,\\n \\\"phase\\\": phase,\\n \\\"count\\\": len(rows),\\n \\\"ids\\\": [r.get(\\\"error_id\\\") for r in rows],\\n },\\n )\\n fail(\\\"NEW_XF_ERRORS\\\")\"},{\"line\":502,\"text\":\"P=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest ! -e \\\"$B\\\"\\ninstall -d -m 700 \\\"$B/Pub/Controller\\\" \\\"$B/_data\\\"\\ncp -p -- \\\"$P/Pub/Controller/Portal.php\\\" \\\"$B/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$P/_data/templates.xml\\\" \\\"$B/_data/templates.xml\\\"\\ntest ! -e \\\"$P/Service/PortalData.php\\\"\\nprintf 'PORTAL200_BACKUP=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_BACKUP=PASS\\\" not in out:\\n fail(\\\"BACKUP_FAILED\\\")\\n\\n\"},{\"line\":504,\"text\":\"install -d -m 700 \\\"$B/Pub/Controller\\\" \\\"$B/_data\\\"\\ncp -p -- \\\"$P/Pub/Controller/Portal.php\\\" \\\"$B/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$P/_data/templates.xml\\\" \\\"$B/_data/templates.xml\\\"\\ntest ! -e \\\"$P/Service/PortalData.php\\\"\\nprintf 'PORTAL200_BACKUP=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_BACKUP=PASS\\\" not in out:\\n fail(\\\"BACKUP_FAILED\\\")\\n\\n\\ndef guest_write(target, rel, data):\\n dest = f\\\"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}\\\"\"},{\"line\":507,\"text\":\"test ! -e \\\"$P/Service/PortalData.php\\\"\\nprintf 'PORTAL200_BACKUP=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_BACKUP=PASS\\\" not in out:\\n fail(\\\"BACKUP_FAILED\\\")\\n\\n\\ndef guest_write(target, rel, data):\\n dest = f\\\"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}\\\"\\n parent = str(pathlib.PurePosixPath(dest).parent)\\n script = (\\n \\\"set -eu; umask 022; \\\"\"},{\"line\":519,\"text\":\" \\\"set -eu; umask 022; \\\"\\n + \\\"install -d -m 755 \\\"\\n + shlex.quote(parent)\\n + \\\"; T=$(mktemp \\\"\\n + shlex.quote(parent + \\\"/.portal200.XXXXXX\\\")\\n + \\\"); cat >\\\\\\\"$T\\\\\\\"; chmod 0644 \\\\\\\"$T\\\\\\\"; chown root:root \\\\\\\"$T\\\\\\\"; \\\"\\n + \\\"mv -f -- \\\\\\\"$T\\\\\\\" \\\"\\n + shlex.quote(dest)\\n + \\\"; printf 'PORTAL200_WRITE=PASS\\\\\\\\n'\\\"\\n )\\n out = qga(\\n target,\\n [\\\"/bin/bash\\\", \\\"-lc\\\", script],\"},{\"line\":523,\"text\":\" + shlex.quote(parent + \\\"/.portal200.XXXXXX\\\")\\n + \\\"); cat >\\\\\\\"$T\\\\\\\"; chmod 0644 \\\\\\\"$T\\\\\\\"; chown root:root \\\\\\\"$T\\\\\\\"; \\\"\\n + \\\"mv -f -- \\\\\\\"$T\\\\\\\" \\\"\\n + shlex.quote(dest)\\n + \\\"; printf 'PORTAL200_WRITE=PASS\\\\\\\\n'\\\"\\n )\\n out = qga(\\n target,\\n [\\\"/bin/bash\\\", \\\"-lc\\\", script],\\n input_bytes=data,\\n timeout=90,\\n )\\n if \\\"PORTAL200_WRITE=PASS\\\" not in out:\"},{\"line\":531,\"text\":\" [\\\"/bin/bash\\\", \\\"-lc\\\", script],\\n input_bytes=data,\\n timeout=90,\\n )\\n if \\\"PORTAL200_WRITE=PASS\\\" not in out:\\n fail(\\\"WRITE_FAILED\\\")\\n\\n\\ndef install_candidate(target):\\n for rel in [\\n \\\"Pub/Controller/Portal.php\\\",\\n \\\"Service/PortalData.php\\\",\\n \\\"_data/templates.xml\\\",\"},{\"line\":532,\"text\":\" input_bytes=data,\\n timeout=90,\\n )\\n if \\\"PORTAL200_WRITE=PASS\\\" not in out:\\n fail(\\\"WRITE_FAILED\\\")\\n\\n\\ndef install_candidate(target):\\n for rel in [\\n \\\"Pub/Controller/Portal.php\\\",\\n \\\"Service/PortalData.php\\\",\\n \\\"_data/templates.xml\\\",\\n ]:\"},{\"line\":535,\"text\":\" if \\\"PORTAL200_WRITE=PASS\\\" not in out:\\n fail(\\\"WRITE_FAILED\\\")\\n\\n\\ndef install_candidate(target):\\n for rel in [\\n \\\"Pub/Controller/Portal.php\\\",\\n \\\"Service/PortalData.php\\\",\\n \\\"_data/templates.xml\\\",\\n ]:\\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\\n\\n\"},{\"line\":539,\"text\":\"def install_candidate(target):\\n for rel in [\\n \\\"Pub/Controller/Portal.php\\\",\\n \\\"Service/PortalData.php\\\",\\n \\\"_data/templates.xml\\\",\\n ]:\\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\\n\\n\\ndef restore_old(target):\\n script = f'''set -eu\\nB={shlex.quote(BACKUP)}\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\"},{\"line\":541,\"text\":\" \\\"Pub/Controller/Portal.php\\\",\\n \\\"Service/PortalData.php\\\",\\n \\\"_data/templates.xml\\\",\\n ]:\\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\\n\\n\\ndef restore_old(target):\\n script = f'''set -eu\\nB={shlex.quote(BACKUP)}\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest -f \\\"$B/Pub/Controller/Portal.php\\\"\\ntest -f \\\"$B/_data/templates.xml\\\"\"},{\"line\":549,\"text\":\" script = f'''set -eu\\nB={shlex.quote(BACKUP)}\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest -f \\\"$B/Pub/Controller/Portal.php\\\"\\ntest -f \\\"$B/_data/templates.xml\\\"\\ncp -p -- \\\"$B/Pub/Controller/Portal.php\\\" \\\"$P/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$B/_data/templates.xml\\\" \\\"$P/_data/templates.xml\\\"\\nrm -f -- \\\"$P/Service/PortalData.php\\\"\\nrmdir -- \\\"$P/Service\\\" 2>/dev/null || true\\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_SOURCE_RESTORE=PASS\\\" not in out:\"},{\"line\":551,\"text\":\"P=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest -f \\\"$B/Pub/Controller/Portal.php\\\"\\ntest -f \\\"$B/_data/templates.xml\\\"\\ncp -p -- \\\"$B/Pub/Controller/Portal.php\\\" \\\"$P/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$B/_data/templates.xml\\\" \\\"$P/_data/templates.xml\\\"\\nrm -f -- \\\"$P/Service/PortalData.php\\\"\\nrmdir -- \\\"$P/Service\\\" 2>/dev/null || true\\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_SOURCE_RESTORE=PASS\\\" not in out:\\n fail(\\\"SOURCE_RESTORE_FAILED\\\")\\n\"},{\"line\":554,\"text\":\"cp -p -- \\\"$B/Pub/Controller/Portal.php\\\" \\\"$P/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$B/_data/templates.xml\\\" \\\"$P/_data/templates.xml\\\"\\nrm -f -- \\\"$P/Service/PortalData.php\\\"\\nrmdir -- \\\"$P/Service\\\" 2>/dev/null || true\\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_SOURCE_RESTORE=PASS\\\" not in out:\\n fail(\\\"SOURCE_RESTORE_FAILED\\\")\\n\\n\\ndef import_addon(target):\\n out = qga(\"},{\"line\":557,\"text\":\"rmdir -- \\\"$P/Service\\\" 2>/dev/null || true\\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_SOURCE_RESTORE=PASS\\\" not in out:\\n fail(\\\"SOURCE_RESTORE_FAILED\\\")\\n\\n\\ndef import_addon(target):\\n out = qga(\\n target,\\n [\\\"/usr/bin/php8.3\\\"],\\n input_bytes=IMPORT_PHP.encode(),\"},{\"line\":568,\"text\":\" [\\\"/usr/bin/php8.3\\\"],\\n input_bytes=IMPORT_PHP.encode(),\\n timeout=240,\\n )\\n if \\\"PORTAL200_IMPORT_JSON=\\\" not in out:\\n fail(\\\"IMPORT_FAILED\\\")\\n\\n\\ndef runtime_verify(target, state):\\n src = RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", state)\\n out = qga(target, [\\\"/usr/bin/php8.3\\\"], input_bytes=src.encode(), timeout=120)\\n line = next(\\n (x for x in out.splitlines() if x.startswith(\\\"PORTAL200_RUNTIME_JSON=\\\")),\"},{\"line\":576,\"text\":\"def runtime_verify(target, state):\\n src = RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", state)\\n out = qga(target, [\\\"/usr/bin/php8.3\\\"], input_bytes=src.encode(), timeout=120)\\n line = next(\\n (x for x in out.splitlines() if x.startswith(\\\"PORTAL200_RUNTIME_JSON=\\\")),\\n None,\\n )\\n if not line:\\n fail(\\\"RUNTIME_MARKER_MISSING\\\")\\n try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n except Exception:\\n fail(\\\"RUNTIME_JSON\\\")\"},{\"line\":585,\"text\":\" try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n except Exception:\\n fail(\\\"RUNTIME_JSON\\\")\\n emit(\\\"PORTAL200_RUNTIME\\\", {\\\"target\\\": target, **data})\\n return data\\n\\n\\ndef http_verify(target, state):\\n argv = [\\\"/usr/bin/python3\\\", \\\"-\\\", target, state]\\n if target == \\\"prod\\\":\\n try:\\n p = subprocess.run(\"},{\"line\":606,\"text\":\" fail(\\\"HTTP_PROD_TIMEOUT\\\")\\n out = p.stdout.decode(\\\"utf-8\\\", \\\"replace\\\")\\n err = p.stderr.decode(\\\"utf-8\\\", \\\"replace\\\")\\n line = next(\\n (x for x in out.splitlines() if x.startswith(\\\"PORTAL200_HTTP_JSON=\\\")),\\n None,\\n )\\n data = None\\n if line:\\n try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n emit(\\\"PORTAL200_HTTP\\\", {\\\"target\\\": target, \\\"state\\\": state, **data})\\n except Exception:\"},{\"line\":613,\"text\":\" data = None\\n if line:\\n try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n emit(\\\"PORTAL200_HTTP\\\", {\\\"target\\\": target, \\\"state\\\": state, **data})\\n except Exception:\\n fail(\\\"HTTP_JSON\\\")\\n if p.returncode != 0:\\n emit(\\n \\\"PORTAL200_HTTP_EXEC\\\",\\n {\\n \\\"target\\\": target,\\n \\\"state\\\": state,\"},{\"line\":618,\"text\":\" except Exception:\\n fail(\\\"HTTP_JSON\\\")\\n if p.returncode != 0:\\n emit(\\n \\\"PORTAL200_HTTP_EXEC\\\",\\n {\\n \\\"target\\\": target,\\n \\\"state\\\": state,\\n \\\"rc\\\": p.returncode,\\n \\\"stderr\\\": err[:800],\\n },\\n )\\n fail(\\\"HTTP_PROD_RC\\\")\"},{\"line\":638,\"text\":\" input_bytes=HTTP_VERIFY_PY.encode(),\\n timeout=180,\\n )\\n line = next(\\n (x for x in out.splitlines() if x.startswith(\\\"PORTAL200_HTTP_JSON=\\\")),\\n None,\\n )\\n if not line:\\n fail(\\\"HTTP_MARKER_MISSING\\\")\\n try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n except Exception:\\n fail(\\\"HTTP_JSON\\\")\"},{\"line\":647,\"text\":\" try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n except Exception:\\n fail(\\\"HTTP_JSON\\\")\\n emit(\\\"PORTAL200_HTTP\\\", {\\\"target\\\": target, \\\"state\\\": state, **data})\\n return data\\n\\n\\ndef verify_new(target, baseline):\\n data = inspect(target)\\n if not app_contract(data, target):\\n fail(\\\"APP_CONTRACT_NEW\\\")\\n if not source_is_new(data):\"},{\"line\":715,\"text\":\" timeout=40,\\n )\\n if \\\"status: running\\\" not in p.stdout.decode():\\n fail(\\\"VM211_NOT_RUNNING\\\")\\n emit(\\\"PORTAL200_CLUSTER\\\", \\\"PASS\\\")\\n\\n\\ndef result_contract(status, changes, rollback_started, rollback_restored):\\n emit(\\n \\\"HOMELAB_RESULT_CONTRACT\\\",\\n {\\n \\\"version\\\": 1,\\n \\\"command_id\\\": CID,\"},{\"line\":734,\"text\":\"\\n\\nemit(\\\"NEWFI200_BEGIN\\\")\\nemit(\\\"COMMAND_ID\\\", CID)\\nemit(\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\\\", 22)\\nemit(\\n \\\"PORTAL200_FLOW\\\",\\n \\\"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\\\",\\n)\\n\\nstaging_backup = False\\nstaging_mutated = False\\nstaging_final_new = False\"},{\"line\":736,\"text\":\"emit(\\\"NEWFI200_BEGIN\\\")\\nemit(\\\"COMMAND_ID\\\", CID)\\nemit(\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\\\", 22)\\nemit(\\n \\\"PORTAL200_FLOW\\\",\\n \\\"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\\\",\\n)\\n\\nstaging_backup = False\\nstaging_mutated = False\\nstaging_final_new = False\\nprod_backup = False\\nprod_mutated = False\"},{\"line\":737,\"text\":\"emit(\\\"COMMAND_ID\\\", CID)\\nemit(\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\\\", 22)\\nemit(\\n \\\"PORTAL200_FLOW\\\",\\n \\\"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\\\",\\n)\\n\\nstaging_backup = False\\nstaging_mutated = False\\nstaging_final_new = False\\nprod_backup = False\\nprod_mutated = False\\ngit_files_changed = False\"},{\"line\":758,\"text\":\"s_old_templates = None\\np_old_templates = None\\n\\ntry:\\n compile(HTTP_VERIFY_PY, \\\"\\\", \\\"exec\\\")\\n for rel, (size, sha) in EXPECTED_NEW.items():\\n p = CAND_ROOT / rel\\n if not p.is_file() or file_meta(p) != (size, sha):\\n fail(\\\"CANDIDATE_IDENTITY\\\")\\n tree = ET.parse(CAND_ROOT / \\\"_data/templates.xml\\\")\\n titles = sorted(x.attrib.get(\\\"title\\\") for x in tree.getroot().findall(\\\"template\\\"))\\n if titles != [\\\"newfi_portal\\\", \\\"newfi_portal.less\\\"]:\\n fail(\\\"CANDIDATE_TEMPLATE_SET\\\")\"},{\"line\":760,\"text\":\"\\ntry:\\n compile(HTTP_VERIFY_PY, \\\"\\\", \\\"exec\\\")\\n for rel, (size, sha) in EXPECTED_NEW.items():\\n p = CAND_ROOT / rel\\n if not p.is_file() or file_meta(p) != (size, sha):\\n fail(\\\"CANDIDATE_IDENTITY\\\")\\n tree = ET.parse(CAND_ROOT / \\\"_data/templates.xml\\\")\\n titles = sorted(x.attrib.get(\\\"title\\\") for x in tree.getroot().findall(\\\"template\\\"))\\n if titles != [\\\"newfi_portal\\\", \\\"newfi_portal.less\\\"]:\\n fail(\\\"CANDIDATE_TEMPLATE_SET\\\")\\n service = (CAND_ROOT / \\\"Service/PortalData.php\\\").read_text(\\\"utf-8\\\")\\n if any(\"},{\"line\":762,\"text\":\" compile(HTTP_VERIFY_PY, \\\"\\\", \\\"exec\\\")\\n for rel, (size, sha) in EXPECTED_NEW.items():\\n p = CAND_ROOT / rel\\n if not p.is_file() or file_meta(p) != (size, sha):\\n fail(\\\"CANDIDATE_IDENTITY\\\")\\n tree = ET.parse(CAND_ROOT / \\\"_data/templates.xml\\\")\\n titles = sorted(x.attrib.get(\\\"title\\\") for x in tree.getroot().findall(\\\"template\\\"))\\n if titles != [\\\"newfi_portal\\\", \\\"newfi_portal.less\\\"]:\\n fail(\\\"CANDIDATE_TEMPLATE_SET\\\")\\n service = (CAND_ROOT / \\\"Service/PortalData.php\\\").read_text(\\\"utf-8\\\")\\n if any(\\n token in service\\n for token in [\"},{\"line\":763,\"text\":\" for rel, (size, sha) in EXPECTED_NEW.items():\\n p = CAND_ROOT / rel\\n if not p.is_file() or file_meta(p) != (size, sha):\\n fail(\\\"CANDIDATE_IDENTITY\\\")\\n tree = ET.parse(CAND_ROOT / \\\"_data/templates.xml\\\")\\n titles = sorted(x.attrib.get(\\\"title\\\") for x in tree.getroot().findall(\\\"template\\\"))\\n if titles != [\\\"newfi_portal\\\", \\\"newfi_portal.less\\\"]:\\n fail(\\\"CANDIDATE_TEMPLATE_SET\\\")\\n service = (CAND_ROOT / \\\"Service/PortalData.php\\\").read_text(\\\"utf-8\\\")\\n if any(\\n token in service\\n for token in [\\n \\\"\\\\\\\\XF::db(\\\",\"},{\"line\":766,\"text\":\" fail(\\\"CANDIDATE_IDENTITY\\\")\\n tree = ET.parse(CAND_ROOT / \\\"_data/templates.xml\\\")\\n titles = sorted(x.attrib.get(\\\"title\\\") for x in tree.getroot().findall(\\\"template\\\"))\\n if titles != [\\\"newfi_portal\\\", \\\"newfi_portal.less\\\"]:\\n fail(\\\"CANDIDATE_TEMPLATE_SET\\\")\\n service = (CAND_ROOT / \\\"Service/PortalData.php\\\").read_text(\\\"utf-8\\\")\\n if any(\\n token in service\\n for token in [\\n \\\"\\\\\\\\XF::db(\\\",\\n \\\"->save(\\\",\\n \\\"->delete(\\\",\\n \\\"INSERT \\\",\"},{\"line\":767,\"text\":\" tree = ET.parse(CAND_ROOT / \\\"_data/templates.xml\\\")\\n titles = sorted(x.attrib.get(\\\"title\\\") for x in tree.getroot().findall(\\\"template\\\"))\\n if titles != [\\\"newfi_portal\\\", \\\"newfi_portal.less\\\"]:\\n fail(\\\"CANDIDATE_TEMPLATE_SET\\\")\\n service = (CAND_ROOT / \\\"Service/PortalData.php\\\").read_text(\\\"utf-8\\\")\\n if any(\\n token in service\\n for token in [\\n \\\"\\\\\\\\XF::db(\\\",\\n \\\"->save(\\\",\\n \\\"->delete(\\\",\\n \\\"INSERT \\\",\\n \\\"UPDATE \\\",\"},{\"line\":780,\"text\":\" \\\"DELETE \\\",\\n \\\"REPLACE \\\",\\n ]\\n ):\\n fail(\\\"CANDIDATE_MUTATION_SCAN\\\")\\n emit(\\\"PORTAL200_CANDIDATE_IDENTITY\\\", \\\"PASS\\\")\\n emit(\\n \\\"PORTAL200_ORCHESTRATION_FIXES\\\",\\n {\\n \\\"staging_503_without_css_links\\\": True,\\n \\\"failure_status\\\": \\\"FAIL\\\",\\n \\\"prod_http_via_pve01_public_route\\\": True,\\n \\\"css_href_regex_fixed\\\": True,\"},{\"line\":781,\"text\":\" \\\"REPLACE \\\",\\n ]\\n ):\\n fail(\\\"CANDIDATE_MUTATION_SCAN\\\")\\n emit(\\\"PORTAL200_CANDIDATE_IDENTITY\\\", \\\"PASS\\\")\\n emit(\\n \\\"PORTAL200_ORCHESTRATION_FIXES\\\",\\n {\\n \\\"staging_503_without_css_links\\\": True,\\n \\\"failure_status\\\": \\\"FAIL\\\",\\n \\\"prod_http_via_pve01_public_route\\\": True,\\n \\\"css_href_regex_fixed\\\": True,\\n },\"},{\"line\":783,\"text\":\" ):\\n fail(\\\"CANDIDATE_MUTATION_SCAN\\\")\\n emit(\\\"PORTAL200_CANDIDATE_IDENTITY\\\", \\\"PASS\\\")\\n emit(\\n \\\"PORTAL200_ORCHESTRATION_FIXES\\\",\\n {\\n \\\"staging_503_without_css_links\\\": True,\\n \\\"failure_status\\\": \\\"FAIL\\\",\\n \\\"prod_http_via_pve01_public_route\\\": True,\\n \\\"css_href_regex_fixed\\\": True,\\n },\\n )\\n\"},{\"line\":800,\"text\":\" remote = remote_out.split()[0] if remote_out else \\\"\\\"\\n if branch != BRANCH or head != OLD_HEAD or remote != OLD_HEAD or status:\\n fail(\\\"GIT_PRESTATE\\\")\\n emit(\\n \\\"PORTAL200_GIT_PRESTATE\\\",\\n {\\\"branch\\\": branch, \\\"head\\\": head, \\\"remote\\\": remote, \\\"clean\\\": True},\\n )\\n cluster_guard()\\n\\n lint_php(\\\"staging\\\", INSPECT_PHP, \\\"INSPECT_STAGING\\\")\\n lint_php(\\\"staging\\\", IMPORT_PHP, \\\"IMPORT_STAGING\\\")\\n lint_php(\\\"staging\\\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", \\\"new\\\"), \\\"RUNTIME_STAGING\\\")\\n lint_php(\\\"prod\\\", INSPECT_PHP, \\\"INSPECT_PROD\\\")\"},{\"line\":811,\"text\":\" lint_php(\\\"staging\\\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", \\\"new\\\"), \\\"RUNTIME_STAGING\\\")\\n lint_php(\\\"prod\\\", INSPECT_PHP, \\\"INSPECT_PROD\\\")\\n lint_php(\\\"prod\\\", IMPORT_PHP, \\\"IMPORT_PROD\\\")\\n lint_php(\\\"prod\\\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", \\\"new\\\"), \\\"RUNTIME_PROD\\\")\\n emit(\\\"PORTAL200_RUNTIME_HELPER_LINT\\\", \\\"PASS\\\")\\n\\n s0 = inspect(\\\"staging\\\")\\n p0 = inspect(\\\"prod\\\")\\n if not app_contract(s0, \\\"staging\\\") or not app_contract(p0, \\\"prod\\\"):\\n fail(\\\"APP_PRESTATE\\\")\\n if not source_is_old(s0) or not source_is_old(p0):\\n fail(\\\"SOURCE_PRESTATE\\\")\\n if s0.get(\\\"templates\\\") != p0.get(\\\"templates\\\"):\"},{\"line\":825,\"text\":\" s_error = int(s0[\\\"application\\\"][\\\"error_max_id\\\"])\\n p_error = int(p0[\\\"application\\\"][\\\"error_max_id\\\"])\\n s_old_templates = s0[\\\"templates\\\"]\\n p_old_templates = p0[\\\"templates\\\"]\\n emit(\\\"PORTAL200_ERROR_BASELINE\\\", {\\\"staging\\\": s_error, \\\"prod\\\": p_error})\\n http_verify(\\\"staging\\\", \\\"old\\\")\\n runtime_verify(\\\"prod\\\", \\\"old\\\")\\n http_verify(\\\"prod\\\", \\\"old\\\")\\n assert_no_new_errors(\\\"staging\\\", s_error, \\\"preflight\\\")\\n assert_no_new_errors(\\\"prod\\\", p_error, \\\"preflight\\\")\\n emit(\\\"PORTAL200_PREFLIGHT\\\", \\\"PASS\\\")\\n\\n guest_backup(\\\"staging\\\")\"},{\"line\":831,\"text\":\" runtime_verify(\\\"prod\\\", \\\"old\\\")\\n http_verify(\\\"prod\\\", \\\"old\\\")\\n assert_no_new_errors(\\\"staging\\\", s_error, \\\"preflight\\\")\\n assert_no_new_errors(\\\"prod\\\", p_error, \\\"preflight\\\")\\n emit(\\\"PORTAL200_PREFLIGHT\\\", \\\"PASS\\\")\\n\\n guest_backup(\\\"staging\\\")\\n staging_backup = True\\n staging_mutated = True\\n install_candidate(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_new(\\\"staging\\\", s_error)\\n emit(\\\"PORTAL200_STAGING_FIRST_APPLY\\\", \\\"PASS\\\")\"},{\"line\":836,\"text\":\"\\n guest_backup(\\\"staging\\\")\\n staging_backup = True\\n staging_mutated = True\\n install_candidate(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_new(\\\"staging\\\", s_error)\\n emit(\\\"PORTAL200_STAGING_FIRST_APPLY\\\", \\\"PASS\\\")\\n\\n restore_old(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_old(\\\"staging\\\", s_error, s_old_templates)\\n staging_mutated = False\"},{\"line\":839,\"text\":\" staging_mutated = True\\n install_candidate(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_new(\\\"staging\\\", s_error)\\n emit(\\\"PORTAL200_STAGING_FIRST_APPLY\\\", \\\"PASS\\\")\\n\\n restore_old(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_old(\\\"staging\\\", s_error, s_old_templates)\\n staging_mutated = False\\n emit(\\\"PORTAL200_STAGING_ROLLBACK_PROOF\\\", \\\"PASS\\\")\\n\\n staging_mutated = True\"},{\"line\":845,\"text\":\" restore_old(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_old(\\\"staging\\\", s_error, s_old_templates)\\n staging_mutated = False\\n emit(\\\"PORTAL200_STAGING_ROLLBACK_PROOF\\\", \\\"PASS\\\")\\n\\n staging_mutated = True\\n install_candidate(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_new(\\\"staging\\\", s_error)\\n staging_final_new = True\\n emit(\\\"PORTAL200_STAGING_FINAL\\\", \\\"PASS\\\")\\n\"},{\"line\":848,\"text\":\" staging_mutated = False\\n emit(\\\"PORTAL200_STAGING_ROLLBACK_PROOF\\\", \\\"PASS\\\")\\n\\n staging_mutated = True\\n install_candidate(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_new(\\\"staging\\\", s_error)\\n staging_final_new = True\\n emit(\\\"PORTAL200_STAGING_FINAL\\\", \\\"PASS\\\")\\n\\n for rel in EXPECTED_NEW:\\n dest = GIT_PORTAL / rel\\n old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None\"},{\"line\":852,\"text\":\" install_candidate(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_new(\\\"staging\\\", s_error)\\n staging_final_new = True\\n emit(\\\"PORTAL200_STAGING_FINAL\\\", \\\"PASS\\\")\\n\\n for rel in EXPECTED_NEW:\\n dest = GIT_PORTAL / rel\\n old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None\\n dest.parent.mkdir(parents=True, exist_ok=True)\\n shutil.copyfile(CAND_ROOT / rel, dest)\\n git_files_changed = True\\n\"},{\"line\":858,\"text\":\" for rel in EXPECTED_NEW:\\n dest = GIT_PORTAL / rel\\n old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None\\n dest.parent.mkdir(parents=True, exist_ok=True)\\n shutil.copyfile(CAND_ROOT / rel, dest)\\n git_files_changed = True\\n\\n if git(\\\"diff\\\", \\\"--check\\\"):\\n fail(\\\"GIT_DIFF_CHECK\\\")\\n status_lines = git(\\\"status\\\", \\\"--porcelain=v1\\\", \\\"--untracked-files=all\\\").splitlines()\\n actual_paths = sorted(line[3:] for line in status_lines if len(line) >= 4)\\n if actual_paths != sorted(EXPECTED_GIT_PATHS):\\n fail(\\\"GIT_SCOPE\\\")\"},{\"line\":885,\"text\":\" if remote_head != new_head:\\n fail(\\\"GIT_REMOTE_HEAD\\\")\\n push_confirmed = True\\n emit(\\n \\\"PORTAL200_GIT_PROMOTION\\\",\\n {\\n \\\"old_head\\\": OLD_HEAD,\\n \\\"new_head\\\": new_head,\\n \\\"remote_head\\\": remote_head,\\n \\\"result\\\": \\\"PASS\\\",\\n },\\n )\\n\"},{\"line\":902,\"text\":\" fail(\\\"PROD_ERROR_DRIFT\\\")\\n guest_backup(\\\"prod\\\")\\n prod_backup = True\\n prod_mutated = True\\n install_candidate(\\\"prod\\\")\\n import_addon(\\\"prod\\\")\\n verify_new(\\\"prod\\\", p_error)\\n emit(\\\"PORTAL200_PRODUCTION\\\", \\\"PASS\\\")\\n\\n sf = verify_new(\\\"staging\\\", s_error)\\n pf = verify_new(\\\"prod\\\", p_error)\\n if sf[\\\"source\\\"] != pf[\\\"source\\\"]:\\n fail(\\\"FINAL_SOURCE_PARITY\\\")\"},{\"line\":905,\"text\":\" prod_mutated = True\\n install_candidate(\\\"prod\\\")\\n import_addon(\\\"prod\\\")\\n verify_new(\\\"prod\\\", p_error)\\n emit(\\\"PORTAL200_PRODUCTION\\\", \\\"PASS\\\")\\n\\n sf = verify_new(\\\"staging\\\", s_error)\\n pf = verify_new(\\\"prod\\\", p_error)\\n if sf[\\\"source\\\"] != pf[\\\"source\\\"]:\\n fail(\\\"FINAL_SOURCE_PARITY\\\")\\n if sf[\\\"templates\\\"] != pf[\\\"templates\\\"]:\\n fail(\\\"FINAL_TEMPLATE_PARITY\\\")\\n if git(\\\"rev-parse\\\", \\\"HEAD\\\") != new_head:\"},{\"line\":922,\"text\":\" if git(\\\"status\\\", \\\"--porcelain=v1\\\", \\\"--untracked-files=all\\\"):\\n fail(\\\"FINAL_GIT_DIRTY\\\")\\n cluster_guard()\\n emit(\\n \\\"PORTAL200_FINAL_EVIDENCE\\\",\\n {\\n \\\"commit\\\": new_head,\\n \\\"source_files\\\": 3,\\n \\\"templates\\\": 2,\\n \\\"staging_error_baseline\\\": s_error,\\n \\\"prod_error_baseline\\\": p_error,\\n },\\n )\"},{\"line\":931,\"text\":\" \\\"staging_error_baseline\\\": s_error,\\n \\\"prod_error_baseline\\\": p_error,\\n },\\n )\\n emit(\\\"PORTAL200_PORTAL\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_STAGING\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_PRODUCTION\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\\\", 21)\\n result_contract(\\\"OK\\\", True, False, None)\\n emit(\\\"NEWFI200_END\\\")\\n raise SystemExit(0)\\n\\nexcept PortalError as exc:\"},{\"line\":932,\"text\":\" \\\"prod_error_baseline\\\": p_error,\\n },\\n )\\n emit(\\\"PORTAL200_PORTAL\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_STAGING\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_PRODUCTION\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\\\", 21)\\n result_contract(\\\"OK\\\", True, False, None)\\n emit(\\\"NEWFI200_END\\\")\\n raise SystemExit(0)\\n\\nexcept PortalError as exc:\\n code = str(exc)\"},{\"line\":933,\"text\":\" },\\n )\\n emit(\\\"PORTAL200_PORTAL\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_STAGING\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_PRODUCTION\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\\\", 21)\\n result_contract(\\\"OK\\\", True, False, None)\\n emit(\\\"NEWFI200_END\\\")\\n raise SystemExit(0)\\n\\nexcept PortalError as exc:\\n code = str(exc)\\n emit(\\\"PORTAL200_FAILURE\\\", code)\"},{\"line\":934,\"text\":\" )\\n emit(\\\"PORTAL200_PORTAL\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_STAGING\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_PRODUCTION\\\", \\\"PASS\\\")\\n emit(\\\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\\\", 21)\\n result_contract(\\\"OK\\\", True, False, None)\\n emit(\\\"NEWFI200_END\\\")\\n raise SystemExit(0)\\n\\nexcept PortalError as exc:\\n code = str(exc)\\n emit(\\\"PORTAL200_FAILURE\\\", code)\\n\"},{\"line\":941,\"text\":\" raise SystemExit(0)\\n\\nexcept PortalError as exc:\\n code = str(exc)\\n emit(\\\"PORTAL200_FAILURE\\\", code)\\n\\n if push_confirmed:\\n if prod_mutated and prod_backup:\\n failure_rollback_started = True\\n try:\\n restore_old(\\\"prod\\\")\\n import_addon(\\\"prod\\\")\\n verify_old(\\\"prod\\\", p_error, p_old_templates, strict_errors=False)\"},{\"line\":952,\"text\":\" import_addon(\\\"prod\\\")\\n verify_old(\\\"prod\\\", p_error, p_old_templates, strict_errors=False)\\n prod_mutated = False\\n failure_rollback_restored = False\\n emit(\\\"PORTAL200_FAILURE_PROD_ROLLBACK\\\", \\\"PASS_PARTIAL_OVERALL_STATE_CHANGED\\\")\\n except Exception:\\n failure_rollback_restored = False\\n emit(\\\"PORTAL200_FAILURE_PROD_ROLLBACK\\\", \\\"FAIL\\\")\\n else:\\n if staging_mutated and staging_backup:\\n failure_rollback_started = True\\n try:\\n restore_old(\\\"staging\\\")\"},{\"line\":955,\"text\":\" failure_rollback_restored = False\\n emit(\\\"PORTAL200_FAILURE_PROD_ROLLBACK\\\", \\\"PASS_PARTIAL_OVERALL_STATE_CHANGED\\\")\\n except Exception:\\n failure_rollback_restored = False\\n emit(\\\"PORTAL200_FAILURE_PROD_ROLLBACK\\\", \\\"FAIL\\\")\\n else:\\n if staging_mutated and staging_backup:\\n failure_rollback_started = True\\n try:\\n restore_old(\\\"staging\\\")\\n import_addon(\\\"staging\\\")\\n verify_old(\\\"staging\\\", s_error, s_old_templates, strict_errors=False)\\n staging_mutated = False\"},{\"line\":966,\"text\":\" verify_old(\\\"staging\\\", s_error, s_old_templates, strict_errors=False)\\n staging_mutated = False\\n staging_final_new = False\\n failure_rollback_restored = True\\n emit(\\\"PORTAL200_FAILURE_STAGING_ROLLBACK\\\", \\\"PASS\\\")\\n except Exception:\\n failure_rollback_restored = False\\n emit(\\\"PORTAL200_FAILURE_STAGING_ROLLBACK\\\", \\\"FAIL\\\")\\n\\n if git_files_changed and not commit_created:\\n try:\\n for rel, old in old_git_bytes.items():\\n dest = GIT_PORTAL / rel\"},{\"line\":969,\"text\":\" failure_rollback_restored = True\\n emit(\\\"PORTAL200_FAILURE_STAGING_ROLLBACK\\\", \\\"PASS\\\")\\n except Exception:\\n failure_rollback_restored = False\\n emit(\\\"PORTAL200_FAILURE_STAGING_ROLLBACK\\\", \\\"FAIL\\\")\\n\\n if git_files_changed and not commit_created:\\n try:\\n for rel, old in old_git_bytes.items():\\n dest = GIT_PORTAL / rel\\n if old is None:\\n if dest.exists():\\n dest.unlink()\"},{\"line\":979,\"text\":\" if old is None:\\n if dest.exists():\\n dest.unlink()\\n else:\\n dest.write_bytes(old)\\n service_dir = GIT_PORTAL / \\\"Service\\\"\\n try:\\n service_dir.rmdir()\\n except OSError:\\n pass\\n emit(\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\", \\\"PASS\\\")\\n except Exception:\\n emit(\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\", \\\"FAIL\\\")\"},{\"line\":985,\"text\":\" try:\\n service_dir.rmdir()\\n except OSError:\\n pass\\n emit(\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\", \\\"PASS\\\")\\n except Exception:\\n emit(\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\", \\\"FAIL\\\")\\n\\n if commit_created:\\n emit(\\n \\\"PORTAL200_FAILURE_GIT_HISTORY\\\",\\n \\\"COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED\\\",\\n )\"},{\"line\":987,\"text\":\" except OSError:\\n pass\\n emit(\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\", \\\"PASS\\\")\\n except Exception:\\n emit(\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\", \\\"FAIL\\\")\\n\\n if commit_created:\\n emit(\\n \\\"PORTAL200_FAILURE_GIT_HISTORY\\\",\\n \\\"COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED\\\",\\n )\\n\\n persisted = bool(push_confirmed or commit_created or staging_final_new or prod_mutated)\"},{\"line\":991,\"text\":\" emit(\\\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\\\", \\\"FAIL\\\")\\n\\n if commit_created:\\n emit(\\n \\\"PORTAL200_FAILURE_GIT_HISTORY\\\",\\n \\\"COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED\\\",\\n )\\n\\n persisted = bool(push_confirmed or commit_created or staging_final_new or prod_mutated)\\n if failure_rollback_started and failure_rollback_restored is None:\\n failure_rollback_restored = False\\n result_contract(\\n \\\"FAIL\\\",\"},{\"line\":1004,\"text\":\" persisted,\\n failure_rollback_started,\\n failure_rollback_restored,\\n )\\n emit(\\\"PORTAL200_ROADMAP_REMAINS\\\", 22)\\n emit(\\\"NEWFI200_END\\\")\\n raise SystemExit(1)\\n\\nexcept Exception:\\n emit(\\\"PORTAL200_FAILURE\\\", \\\"UNEXPECTED_EXCEPTION\\\")\\n result_contract(\\n \\\"FAIL\\\",\\n bool(push_confirmed or commit_created or staging_final_new or prod_mutated),\"},{\"line\":1009,\"text\":\" emit(\\\"NEWFI200_END\\\")\\n raise SystemExit(1)\\n\\nexcept Exception:\\n emit(\\\"PORTAL200_FAILURE\\\", \\\"UNEXPECTED_EXCEPTION\\\")\\n result_contract(\\n \\\"FAIL\\\",\\n bool(push_confirmed or commit_created or staging_final_new or prod_mutated),\\n False,\\n None,\\n )\\n emit(\\\"PORTAL200_ROADMAP_REMAINS\\\", 22)\\n emit(\\\"NEWFI200_END\\\")\"},{\"line\":1016,\"text\":\" bool(push_confirmed or commit_created or staging_final_new or prod_mutated),\\n False,\\n None,\\n )\\n emit(\\\"PORTAL200_ROADMAP_REMAINS\\\", 22)\\n emit(\\\"NEWFI200_END\\\")\\n raise SystemExit(1)\"}]\nRELEVANT_FUNCTIONS=[{\"lineno\":400,\"name\":\"parse_inspect\",\"sha256\":\"7b72b62a0ff4c617b7175627d8630294160a6c129f3acfb0d109b4c72c2e8fe2\",\"source\":\"def parse_inspect(text):\\n b = \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\"\\n e = \\\"\\\\nPORTAL200_INSPECT_JSON_END\\\"\\n if b not in text or e not in text:\\n fail(\\\"INSPECT_MARKERS\\\")\\n try:\\n return json.loads(text.split(b, 1)[1].split(e, 1)[0])\\n except Exception:\\n fail(\\\"INSPECT_JSON\\\")\"},{\"lineno\":447,\"name\":\"app_contract\",\"sha256\":\"a3a06560de8f2db42e0c3bafc27ad29ee985fd5bf13b01c527a283731ed7904c\",\"source\":\"def app_contract(data, target):\\n a = data.get(\\\"application\\\", {})\\n addon = data.get(\\\"addon\\\") or {}\\n expected = {\\n \\\"staging\\\": (\\\"0\\\", \\\"http://newfi-staging.gram1.ru\\\"),\\n \\\"prod\\\": (\\\"1\\\", \\\"https://newfi.ru\\\"),\\n }[target]\\n return (\\n a.get(\\\"boardActive\\\") == expected[0]\\n and a.get(\\\"boardUrl\\\") == expected[1]\\n and a.get(\\\"indexRoute\\\") == \\\"portal/\\\"\\n and a.get(\\\"defaultStyleId\\\") == \\\"4\\\"\\n and addon.get(\\\"addon_id\\\") == \\\"Newfi/Portal\\\"\\n and int(addon.get(\\\"version_id\\\", 0)) == 1000010\\n and addon.get(\\\"version_string\\\") == \\\"1.0.0 Alpha 1\\\"\\n and int(addon.get(\\\"active\\\", 0)) == 1\\n and int(addon.get(\\\"is_processing\\\", 1)) == 0\\n )\"},{\"lineno\":480,\"name\":\"assert_no_new_errors\",\"sha256\":\"801839c1705804fed1a507709263d2882a332f0276b42a7c5439204b99428dfb\",\"source\":\"def assert_no_new_errors(target, baseline, phase):\\n rows = new_errors(target, baseline)\\n if rows:\\n emit(\\n \\\"PORTAL200_NEW_XF_ERRORS\\\",\\n {\\n \\\"target\\\": target,\\n \\\"phase\\\": phase,\\n \\\"count\\\": len(rows),\\n \\\"ids\\\": [r.get(\\\"error_id\\\") for r in rows],\\n },\\n )\\n fail(\\\"NEW_XF_ERRORS\\\")\"},{\"lineno\":495,\"name\":\"guest_backup\",\"sha256\":\"3e0d88922bf3d3420512f69cd0e50232e0325391cebb99845ed1c8c206ec08a1\",\"source\":\"def guest_backup(target):\\n script = f'''set -eu\\nB={shlex.quote(BACKUP)}\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest ! -e \\\"$B\\\"\\ninstall -d -m 700 \\\"$B/Pub/Controller\\\" \\\"$B/_data\\\"\\ncp -p -- \\\"$P/Pub/Controller/Portal.php\\\" \\\"$B/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$P/_data/templates.xml\\\" \\\"$B/_data/templates.xml\\\"\\ntest ! -e \\\"$P/Service/PortalData.php\\\"\\nprintf 'PORTAL200_BACKUP=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_BACKUP=PASS\\\" not in out:\\n fail(\\\"BACKUP_FAILED\\\")\"},{\"lineno\":511,\"name\":\"guest_write\",\"sha256\":\"f5f8ed7e6553eadf19e219ed9cab5dd6d7b9ad72dca3926e5368ad663f9b2881\",\"source\":\"def guest_write(target, rel, data):\\n dest = f\\\"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}\\\"\\n parent = str(pathlib.PurePosixPath(dest).parent)\\n script = (\\n \\\"set -eu; umask 022; \\\"\\n + \\\"install -d -m 755 \\\"\\n + shlex.quote(parent)\\n + \\\"; T=$(mktemp \\\"\\n + shlex.quote(parent + \\\"/.portal200.XXXXXX\\\")\\n + \\\"); cat >\\\\\\\"$T\\\\\\\"; chmod 0644 \\\\\\\"$T\\\\\\\"; chown root:root \\\\\\\"$T\\\\\\\"; \\\"\\n + \\\"mv -f -- \\\\\\\"$T\\\\\\\" \\\"\\n + shlex.quote(dest)\\n + \\\"; printf 'PORTAL200_WRITE=PASS\\\\\\\\n'\\\"\\n )\\n out = qga(\\n target,\\n [\\\"/bin/bash\\\", \\\"-lc\\\", script],\\n input_bytes=data,\\n timeout=90,\\n )\\n if \\\"PORTAL200_WRITE=PASS\\\" not in out:\\n fail(\\\"WRITE_FAILED\\\")\"},{\"lineno\":535,\"name\":\"install_candidate\",\"sha256\":\"b46ecffec3c0972b1306ffb057164c1223e08f26bd3121bfde7f624ffad66903\",\"source\":\"def install_candidate(target):\\n for rel in [\\n \\\"Pub/Controller/Portal.php\\\",\\n \\\"Service/PortalData.php\\\",\\n \\\"_data/templates.xml\\\",\\n ]:\\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\"},{\"lineno\":544,\"name\":\"restore_old\",\"sha256\":\"eafa8284ff101e7e3c189922361201ce5684ce970e90e2c933c39b65e25e21da\",\"source\":\"def restore_old(target):\\n script = f'''set -eu\\nB={shlex.quote(BACKUP)}\\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\\ntest -f \\\"$B/Pub/Controller/Portal.php\\\"\\ntest -f \\\"$B/_data/templates.xml\\\"\\ncp -p -- \\\"$B/Pub/Controller/Portal.php\\\" \\\"$P/Pub/Controller/Portal.php\\\"\\ncp -p -- \\\"$B/_data/templates.xml\\\" \\\"$P/_data/templates.xml\\\"\\nrm -f -- \\\"$P/Service/PortalData.php\\\"\\nrmdir -- \\\"$P/Service\\\" 2>/dev/null || true\\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\\\n'\\n'''\\n out = qga(target, [\\\"/bin/bash\\\", \\\"-lc\\\", script], timeout=90)\\n if \\\"PORTAL200_SOURCE_RESTORE=PASS\\\" not in out:\\n fail(\\\"SOURCE_RESTORE_FAILED\\\")\"},{\"lineno\":561,\"name\":\"import_addon\",\"sha256\":\"8af6f854e1917ee8e02cfe345f730da0b4b6fbe1b44fdb73ced52c8a3eeebb50\",\"source\":\"def import_addon(target):\\n out = qga(\\n target,\\n [\\\"/usr/bin/php8.3\\\"],\\n input_bytes=IMPORT_PHP.encode(),\\n timeout=240,\\n )\\n if \\\"PORTAL200_IMPORT_JSON=\\\" not in out:\\n fail(\\\"IMPORT_FAILED\\\")\"},{\"lineno\":572,\"name\":\"runtime_verify\",\"sha256\":\"cbe3a884afd8bb310ce9d055ddaf74a9923ac55e975ad5a9c3fbb0b910776497\",\"source\":\"def runtime_verify(target, state):\\n src = RUNTIME_VERIFY_PHP_TEMPLATE.replace(\\\"__STATE__\\\", state)\\n out = qga(target, [\\\"/usr/bin/php8.3\\\"], input_bytes=src.encode(), timeout=120)\\n line = next(\\n (x for x in out.splitlines() if x.startswith(\\\"PORTAL200_RUNTIME_JSON=\\\")),\\n None,\\n )\\n if not line:\\n fail(\\\"RUNTIME_MARKER_MISSING\\\")\\n try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n except Exception:\\n fail(\\\"RUNTIME_JSON\\\")\\n emit(\\\"PORTAL200_RUNTIME\\\", {\\\"target\\\": target, **data})\\n return data\"},{\"lineno\":589,\"name\":\"http_verify\",\"sha256\":\"75eb5fbec1862b5d926cbc91519c284218a4bb05db9edf616422fa187b8348a6\",\"source\":\"def http_verify(target, state):\\n argv = [\\\"/usr/bin/python3\\\", \\\"-\\\", target, state]\\n if target == \\\"prod\\\":\\n try:\\n p = subprocess.run(\\n argv,\\n input=HTTP_VERIFY_PY.encode(),\\n stdout=subprocess.PIPE,\\n stderr=subprocess.PIPE,\\n timeout=180,\\n check=False,\\n )\\n except subprocess.TimeoutExpired:\\n fail(\\\"HTTP_PROD_TIMEOUT\\\")\\n out = p.stdout.decode(\\\"utf-8\\\", \\\"replace\\\")\\n err = p.stderr.decode(\\\"utf-8\\\", \\\"replace\\\")\\n line = next(\\n (x for x in out.splitlines() if x.startswith(\\\"PORTAL200_HTTP_JSON=\\\")),\\n None,\\n )\\n data = None\\n if line:\\n try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n emit(\\\"PORTAL200_HTTP\\\", {\\\"target\\\": target, \\\"state\\\": state, **data})\\n except Exception:\\n fail(\\\"HTTP_JSON\\\")\\n if p.returncode != 0:\\n emit(\\n \\\"PORTAL200_HTTP_EXEC\\\",\\n {\\n \\\"target\\\": target,\\n \\\"state\\\": state,\\n \\\"rc\\\": p.returncode,\\n \\\"stderr\\\": err[:800],\\n },\\n )\\n fail(\\\"HTTP_PROD_RC\\\")\\n if data is None:\\n fail(\\\"HTTP_MARKER_MISSING\\\")\\n return data\\n\\n out = qga(\\n target,\\n argv,\\n input_bytes=HTTP_VERIFY_PY.encode(),\\n timeout=180,\\n )\\n line = next(\\n (x for x in out.splitlines() if x.startswith(\\\"PORTAL200_HTTP_JSON=\\\")),\\n None,\\n )\\n if not line:\\n fail(\\\"HTTP_MARKER_MISSING\\\")\\n try:\\n data = json.loads(line.split(\\\"=\\\", 1)[1])\\n except Exception:\\n fail(\\\"HTTP_JSON\\\")\\n emit(\\\"PORTAL200_HTTP\\\", {\\\"target\\\": target, \\\"state\\\": state, **data})\\n return data\"},{\"lineno\":692,\"name\":\"cluster_guard\",\"sha256\":\"bd6547918004a2f74ae4ac8cc7d7caf80b4ced2bcf57cc98ddc454561121dcac\",\"source\":\"def cluster_guard():\\n p = run([\\\"/usr/sbin/qm\\\", \\\"status\\\", \\\"210\\\"], timeout=30)\\n if \\\"status: running\\\" not in p.stdout.decode():\\n fail(\\\"VM210_NOT_RUNNING\\\")\\n p = run(\\n [\\n \\\"/usr/bin/ssh\\\",\\n \\\"-T\\\",\\n \\\"-o\\\",\\n \\\"BatchMode=yes\\\",\\n \\\"-o\\\",\\n \\\"StrictHostKeyChecking=yes\\\",\\n \\\"-o\\\",\\n \\\"UpdateHostKeys=no\\\",\\n \\\"-o\\\",\\n \\\"ConnectTimeout=10\\\",\\n \\\"root@pve03\\\",\\n \\\"/usr/sbin/qm status 211\\\",\\n ],\\n timeout=40,\\n )\\n if \\\"status: running\\\" not in p.stdout.decode():\\n fail(\\\"VM211_NOT_RUNNING\\\")\\n emit(\\\"PORTAL200_CLUSTER\\\", \\\"PASS\\\")\"}]\nLITERAL_PAYLOADS=[{\"bytes\":2008,\"lineno\":147,\"names\":[\"INSPECT_PHP\"],\"sha256\":\"8de3d66a7adfca4c179db55bb5c4e956f6750fb41e9e058b449efeb214cbd7f1\",\"value\":\"db();\\nfunction srcMeta(string $path): ?array {\\n if (!is_file($path)) { return null; }\\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\\n}\\n$portal = $root . '/src/addons/Newfi/Portal';\\n$templates = $db->fetchAll(\\n \\\"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \\\" .\\n \\\"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \\\" .\\n \\\"LOCATE('--newfi-portal200', template) > 0 AS css_marker \\\" .\\n \\\"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\\\"\\n);\\n$result = [\\n 'hostname' => gethostname(),\\n 'application' => [\\n 'boardActive' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\\\"),\\n 'boardUrl' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\\\"),\\n 'indexRoute' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\\\"),\\n 'defaultStyleId' => (string)$db->fetchOne(\\\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\\\"),\\n 'error_count' => (int)$db->fetchOne(\\\"SELECT COUNT(*) FROM xf_error_log\\\"),\\n 'error_max_id' => (int)$db->fetchOne(\\\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\\\")\\n ],\\n 'addon' => $db->fetchRow(\\n \\\"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \\\" .\\n \\\"FROM xf_addon WHERE addon_id='Newfi/Portal'\\\"\\n ),\\n 'source' => [\\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\\n ],\\n 'templates' => $templates\\n];\\necho \\\"PORTAL200_INSPECT_JSON_BEGIN\\\\n\\\";\\necho json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\\necho \\\"\\\\nPORTAL200_INSPECT_JSON_END\\\\n\\\";\\n?>\"},{\"bytes\":1429,\"lineno\":194,\"names\":[\"IMPORT_PHP\"],\"sha256\":\"8f4109ed896f29035cfa5534751507efff2786d4d6d18840adf30b84771a18e8\",\"value\":\"addOnManager()->getById('Newfi/Portal');\\nif (!$addOn || !$addOn->isInstalled()) {\\n fwrite(STDERR, \\\"ADDON_NOT_INSTALLED\\\\n\\\");\\n exit(30);\\n}\\n$addOn->preRebuild();\\n$dataManager = $app->addOnDataManager();\\n$imported = [];\\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\\n $handler = $dataManager->getDataTypeHandler($dataType);\\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\\n if (!$xml || !$xml->count()) {\\n $handler->deleteAddOnData($addOn->addon_id);\\n continue;\\n }\\n $start = 0;\\n $guard = 0;\\n while (true) {\\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\\n if (!$last) { break; }\\n if ($last <= $start || ++$guard > 100000) {\\n fwrite(STDERR, \\\"IMPORT_PROGRESS_INVALID\\\\n\\\");\\n exit(31);\\n }\\n $start = $last;\\n }\\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\\n \\\\XF::triggerRunOnce();\\n $imported[] = $handler->getContainerTag();\\n}\\n$addOn->postDataImport();\\n$repo = $app->repository(\\\\XF\\\\Repository\\\\StyleRepository::class);\\n$repo->updateAllStylesLastModifiedDateLater();\\n$addOn->postRebuild();\\necho \\\"PORTAL200_IMPORT_JSON=\\\" . json_encode(\\n ['result'=>'PASS','containers'=>$imported],\\n JSON_UNESCAPED_SLASHES\\n) . \\\"\\\\n\\\";\\n?>\"},{\"bytes\":410,\"lineno\":240,\"names\":[\"ERRORS_PHP_TEMPLATE\"],\"sha256\":\"ee412cafa99d7d809c8b682db8ebb0bc2958ab0cbc9e37537e22a78f10489635\",\"value\":\"db();\\n$b=%d;\\n$rows=$db->fetchAll(\\n \\\"SELECT error_id,exception_type,message,filename,line FROM xf_error_log WHERE error_id>? ORDER BY error_id\\\",\\n [$b]\\n);\\necho json_encode(\\n $rows,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n);\\n?>\"},{\"bytes\":2501,\"lineno\":258,\"names\":[\"RUNTIME_VERIFY_PHP_TEMPLATE\"],\"sha256\":\"884c03648203992cf6d5b96be8342243739b0412fdff55f4a6b64cf96aad3313\",\"value\":\"$state];\\nif ($state === 'new') {\\n $service = new \\\\Newfi\\\\Portal\\\\Service\\\\PortalData($app);\\n $portal = $service->build();\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\\n $result['sections'] = [];\\n foreach (['forums','resources','media','events','dogs'] as $key) {\\n $section = $portal[$key] ?? null;\\n if (!is_array($section)) { fwrite(STDERR, \\\"SECTION_MISSING\\\\n\\\"); exit(60); }\\n $result['sections'][$key] = [\\n 'available'=>(bool)($section['available'] ?? false),\\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\\n 'status'=>(string)($section['status'] ?? '')\\n ];\\n }\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\\\"1\\\"') !== false;\\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\\\"resources\\\"') !== false;\\n $result['locked_media'] = strpos($html, 'data-portal-locked=\\\"media\\\"') !== false;\\n $result['html_bytes'] = strlen($html);\\n if (!$result['html_marker']) { fwrite(STDERR, \\\"HTML_MARKER_MISSING\\\\n\\\"); exit(61); }\\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\\n fwrite(STDERR, \\\"GUEST_PERMISSION_CONTRACT_CHANGED\\\\n\\\"); exit(62);\\n }\\n if (!$result['locked_resources'] || !$result['locked_media']) {\\n fwrite(STDERR, \\\"LOCKED_COPY_MISSING\\\\n\\\"); exit(63);\\n }\\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\\n fwrite(STDERR, \\\"PUBLIC_SECTION_UNAVAILABLE\\\\n\\\"); exit(64);\\n }\\n} else if ($state === 'old') {\\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\\n $result['html_bytes'] = strlen($html);\\n if ($result['html_marker'] || !$result['old_copy']) {\\n fwrite(STDERR, \\\"OLD_RENDER_CONTRACT\\\\n\\\"); exit(65);\\n }\\n} else {\\n exit(66);\\n}\\necho \\\"PORTAL200_RUNTIME_JSON=\\\" . json_encode(\\n $result,\\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\\n) . \\\"\\\\n\\\";\\n?>\"},{\"bytes\":3277,\"lineno\":311,\"names\":[\"HTTP_VERIFY_PY\"],\"sha256\":\"15b9839d95798833aef597b59787d301155d32cfb404b14ca7e0bf7d83a1a71e\",\"value\":\"import html,json,re,subprocess,sys,urllib.parse\\nmode=sys.argv[1]\\nstate=sys.argv[2]\\nif mode=='staging':\\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\\nelif mode=='prod':\\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\\nelse:\\n raise SystemExit(40)\\ndef fetch(url):\\n cmd=[\\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\\n ]\\n if mode=='staging':\\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\\n cmd += [\\n '-o','-','-w','\\\\n__PORTAL_STATUS__%{http_code}',url\\n ]\\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\\n if p.returncode!=0: raise SystemExit(41)\\n text=p.stdout.decode('utf-8','replace')\\n marker='\\\\n__PORTAL_STATUS__'\\n if marker not in text: raise SystemExit(42)\\n body,status=text.rsplit(marker,1)\\n return int(status.strip()),body\\nroot=f'{scheme}://{host}/'\\nstatus,body=fetch(root)\\nhrefs=[]\\nfor raw in re.findall(r\\\"href=[\\\\\\\"']([^\\\\\\\"']*css\\\\.php[^\\\\\\\"']*)[\\\\\\\"']\\\",body,re.I):\\n raw=html.unescape(raw)\\n hrefs.append(urllib.parse.urljoin(root,raw))\\nnormal_css=[]\\nnormal_status=[]\\nfor u in hrefs:\\n s,b=fetch(u)\\n normal_status.append(s)\\n normal_css.append(b)\\nnormal_css_text='\\\\n'.join(normal_css)\\nforced_css_marker=None\\nforced_css_status=None\\nif mode=='staging' and state=='new' and hrefs:\\n u=urllib.parse.urlsplit(hrefs[0])\\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\\n out=[]\\n found=False\\n for k,v in pairs:\\n if k=='css':\\n found=True\\n parts=[x for x in v.split(',') if x]\\n if 'public:newfi_portal.less' not in parts:\\n parts.append('public:newfi_portal.less')\\n v=','.join(parts)\\n out.append((k,v))\\n if found:\\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\\n forced_css_status,forced_body=fetch(forced)\\n forced_css_marker='--newfi-portal200' in forced_body\\nresult={\\n 'status':status,\\n 'bytes':len(body.encode()),\\n 'html_marker':'data-newfi-portal200=\\\"1\\\"' in body,\\n 'css_marker':'--newfi-portal200' in normal_css_text,\\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\\n 'css_count':len(hrefs),\\n 'css_status':normal_status,\\n 'forced_css_status':forced_css_status,\\n 'forced_css_marker':forced_css_marker,\\n}\\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\\nif status!=expected_status: raise SystemExit(45)\\nif mode=='prod':\\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\\nelif mode=='staging':\\n if any(x!=200 for x in normal_status): raise SystemExit(46)\\nelse:\\n raise SystemExit(50)\\nif mode=='prod' and state=='new':\\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\\nelif mode=='prod' and state=='old':\\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\\nelif mode=='staging' and state=='new':\\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\\nelif mode=='staging' and state=='old':\\n pass\\nelse:\\n raise SystemExit(50)\\n\"}]\nSOURCE_VERSION_IDS=[]\nSOURCE_VERSION_STRINGS=[]\nAST_RECOVERY_END=true\n" } diff --git a/runtime/latest.txt b/runtime/latest.txt index b99c6275..1ace0fd1 100644 --- a/runtime/latest.txt +++ b/runtime/latest.txt @@ -1,15 +1,15 @@ CHAT_OUTPUT_BEGIN -COMMAND_ID=NEWFI-260907-A-PORTAL200-APPLY4DESIGN6 +COMMAND_ID=NEWFI-260907-A-PORTAL200-APPLY4DESIGN7 STATUS=OK RC=0 HOST=pve01 MODE=read-only -COMPONENT=newfi-portal200-apply4-exact-old-carrier-recovery +COMPONENT=newfi-portal200-apply4-static-ast-recovery REFERENCE_REGISTER_CHECK=OK REFERENCE_SHA256=5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66 ERROR_REGISTER_CHECK=OK ERROR_REGISTER_SHA256=3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0 -COMMAND_SHA256=743ae461d12f19d0da870df76bde76c8a943c1729a9a04890736219912177071 +COMMAND_SHA256=f496660f98e2d5bd57395b705f69565e9b9b4fa3823630155bbe079f561a2876 DUPLICATE_FAILED_COMMAND_BLOCKED=false EXECUTION_STARTED=true CHANGE_DECLARED=false @@ -24,1622 +24,27 @@ MUTATION_OUTCOME=NO_MUTATION SANITIZED=yes SECRETS_INCLUDED=no PRIVATE_ADDRESSES_INCLUDED=no -RAW_EVIDENCE_SHA256=26388e4b0b4f7fd3220098a4be02fcc57b475838436c8374cae49aec73fea1ac -SANITIZED_OUTPUT_SHA256=778936e6f9b7e0ce7697c2b9feb8203c185a24c19d7b76e2f58d05caa12ee143 +RAW_EVIDENCE_SHA256=b0e750afcc8c28e33530e823fa9ca879ddd22ce66e758296aeb27ffd61aa5d1a +SANITIZED_OUTPUT_SHA256=b0e750afcc8c28e33530e823fa9ca879ddd22ce66e758296aeb27ffd61aa5d1a OUTPUT_BEGIN -LOCAL_APPLY3_EXACT_CARRIER_RECOVERY_BEGIN=true -COMMAND_ID=NEWFI-260907-A-PORTAL200-APPLY4DESIGN6 -MUTATION_SCOPE=NONE_READ_ONLY -LOCAL_MUTATIONS_PERFORMED=NO -TARGET_BASENAME=newfi-54-portal200-apply3.cmd -TARGET_OLD_COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3 -TARGET_MATCH_COUNT=1 -TARGET_FILE_SHA256=573c5c94fe59ed072e094281668ce9847dc4a9882019a2b89d8ba5f190d92176 -TARGET_FILE_BYTES=17209 -TARGET_FILE_CONTAINS_OLD_CID=True -DECODED_LAYER_COUNT=2 -CANDIDATE_LAYER_COUNT=2 -CANDIDATE_LAYER_1_SOURCE=carrier::b64::zlib -CANDIDATE_LAYER_1_SHA256=97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184 -CANDIDATE_LAYER_1_BYTES=34155 -CANDIDATE_LAYER_1_RELEVANT_BEGIN -import subprocess -import sys -import xml.etree.ElementTree as ET - -CID = "NEWFI-260907-PORTAL200-APPLY3" -BRANCH = "implementation/newfi-closed-mvp-20260906" -OLD_HEAD = "06bab5acd124e768f70305b3d7eca2f595871890" -WT = pathlib.Path("/root/newfi-portal/.worktrees/implementation-newfi-closed-mvp-20260906") -CAND_ROOT = pathlib.Path("/root/newfi-portal200-candidate/src/addons/Newfi/Portal") -PORTAL_REL = pathlib.Path("src/addons/Newfi/Portal") -GIT_PORTAL = WT / PORTAL_REL -EXPECTED_NEW = { - "Pub/Controller/Portal.php": (363, "619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d"), - "Service/PortalData.php": (5213, "4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d"), - "_data/templates.xml": (10977, "1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861"), -} -EXPECTED_OLD = { - "Pub/Controller/Portal.php": (247, "53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3"), - "_data/templates.xml": (1461, "c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf"), -EXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)] -BACKUP = f"/root/newfi-portal200-backup-{CID}" -class PortalError(RuntimeError): - pass -def emit(key, value=None): - if value is None: -function srcMeta(string $path): ?array { - if (!is_file($path)) { return null; } - return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)]; -$portal = $root . '/src/addons/Newfi/Portal'; -$templates = $db->fetchAll( - "SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, " . - "LOCATE('data-newfi-portal200', template) > 0 AS html_marker, " . - "LOCATE('--newfi-portal200', template) > 0 AS css_marker " . - "FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title" -); -$result = [ - 'hostname' => gethostname(), - 'application' => [ - 'boardActive' => (string)$db->fetchOne("SELECT option_value FROM xf_option WHERE option_id='boardActive'"), - 'boardUrl' => (string)$db->fetchOne("SELECT option_value FROM xf_option WHERE option_id='boardUrl'"), - 'indexRoute' => (string)$db->fetchOne("SELECT option_value FROM xf_option WHERE option_id='indexRoute'"), - 'defaultStyleId' => (string)$db->fetchOne("SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'"), - 'error_count' => (int)$db->fetchOne("SELECT COUNT(*) FROM xf_error_log"), - 'error_max_id' => (int)$db->fetchOne("SELECT COALESCE(MAX(error_id),0) FROM xf_error_log") - ], - 'addon' => $db->fetchRow( - "SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action " . - "FROM xf_addon WHERE addon_id='Newfi/Portal'" - ), - 'source' => [ - 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'), - 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'), - '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml') - 'templates' => $templates -]; -echo "PORTAL200_INSPECT_JSON_BEGIN\n"; -echo json_encode( - $result, - JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE -echo "\nPORTAL200_INSPECT_JSON_END\n"; -?>''' -IMPORT_PHP = r'''addOnManager()->getById('Newfi/Portal'); -if (!$addOn || !$addOn->isInstalled()) { - fwrite(STDERR, "ADDON_NOT_INSTALLED\n"); - exit(30); -$addOn->preRebuild(); -$dataManager = $app->addOnDataManager(); -$imported = []; -foreach ($dataManager->getDataTypeClasses() as $dataType) { -$addOn->postDataImport(); -$repo = $app->repository(\XF\Repository\StyleRepository::class); -$repo->updateAllStylesLastModifiedDateLater(); -$addOn->postRebuild(); -echo "PORTAL200_IMPORT_JSON=" . json_encode( - ['result'=>'PASS','containers'=>$imported], - JSON_UNESCAPED_SLASHES -) . "\n"; -ERRORS_PHP_TEMPLATE = r'''(bool)($section['hasItems'] ?? false), - 'status'=>(string)($section['status'] ?? '') - ]; - } - $result['html_marker'] = strpos($html, 'data-newfi-portal200="1"') !== false; - $result['locked_resources'] = strpos($html, 'data-portal-locked="resources"') !== false; - $result['locked_media'] = strpos($html, 'data-portal-locked="media"') !== false; - $result['html_bytes'] = strlen($html); - if (!$result['html_marker']) { fwrite(STDERR, "HTML_MARKER_MISSING\n"); exit(61); } - if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) { - fwrite(STDERR, "GUEST_PERMISSION_CONTRACT_CHANGED\n"); exit(62); - if (!$result['locked_resources'] || !$result['locked_media']) { - fwrite(STDERR, "PUBLIC_SECTION_UNAVAILABLE\n"); exit(64); -} else if ($state === 'old') { - $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []); - $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false; - $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false; - if ($result['html_marker'] || !$result['old_copy']) { - fwrite(STDERR, "OLD_RENDER_CONTRACT\n"); exit(65); -} else { - exit(66); -echo "PORTAL200_RUNTIME_JSON=" . json_encode( -HTTP_VERIFY_PY = r'''import html,json,re,subprocess,sys,urllib.parse -mode=sys.argv[1] - out.append((k,v)) - if found: - forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment)) - forced_css_status,forced_body=fetch(forced) - forced_css_marker='--newfi-portal200' in forced_body -result={ - 'status':status, - 'bytes':len(body.encode()), - 'html_marker':'data-newfi-portal200="1"' in body, - 'css_marker':'--newfi-portal200' in normal_css_text, - 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body, - 'css_count':len(hrefs), - 'css_status':normal_status, - 'forced_css_status':forced_css_status, - 'forced_css_marker':forced_css_marker, -print('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':'))) -if status!=expected_status: raise SystemExit(45) -if mode=='prod': - if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46) -elif mode=='staging': - if any(x!=200 for x in normal_status): raise SystemExit(46) -else: - raise SystemExit(50) -if mode=='prod' and state=='new': -''' -def parse_inspect(text): - b = "PORTAL200_INSPECT_JSON_BEGIN\n" - e = "\nPORTAL200_INSPECT_JSON_END" - if b not in text or e not in text: - fail("INSPECT_MARKERS") - try: - return json.loads(text.split(b, 1)[1].split(e, 1)[0]) - except Exception: - fail("INSPECT_JSON") - a.get("boardActive") == expected[0] - and a.get("boardUrl") == expected[1] - and a.get("indexRoute") == "portal/" - and a.get("defaultStyleId") == "4" - and addon.get("addon_id") == "Newfi/Portal" - and int(addon.get("version_id", 0)) == 1000010 - and addon.get("version_string") == "1.0.0 Alpha 1" - and int(addon.get("active", 0)) == 1 - and int(addon.get("is_processing", 1)) == 0 - ) -def new_errors(target, baseline): - raw = qga( - target, -def assert_no_new_errors(target, baseline, phase): - rows = new_errors(target, baseline) - if rows: - emit( - "PORTAL200_NEW_XF_ERRORS", - { - "target": target, - "phase": phase, - "count": len(rows), - "ids": [r.get("error_id") for r in rows], - }, - ) - fail("NEW_XF_ERRORS") -def guest_backup(target): - script = f'''set -eu -B={shlex.quote(BACKUP)} -P=/var/www/forums/newfi/public/src/addons/Newfi/Portal -test ! -e "$B" -install -d -m 700 "$B/Pub/Controller" "$B/_data" -cp -p -- "$P/Pub/Controller/Portal.php" "$B/Pub/Controller/Portal.php" -cp -p -- "$P/_data/templates.xml" "$B/_data/templates.xml" -test ! -e "$P/Service/PortalData.php" -printf 'PORTAL200_BACKUP=PASS\n' - out = qga(target, ["/bin/bash", "-lc", script], timeout=90) - if "PORTAL200_BACKUP=PASS" not in out: - fail("BACKUP_FAILED") -def guest_write(target, rel, data): - dest = f"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}" - parent = str(pathlib.PurePosixPath(dest).parent) - script = ( - "set -eu; umask 022; " - + "install -d -m 755 " - + shlex.quote(parent) - + "; T=$(mktemp " - + shlex.quote(parent + "/.portal200.XXXXXX") - + "); cat >\"$T\"; chmod 0644 \"$T\"; chown root:root \"$T\"; " - + "mv -f -- \"$T\" " - + shlex.quote(dest) - + "; printf 'PORTAL200_WRITE=PASS\\n'" - out = qga( - ["/bin/bash", "-lc", script], - input_bytes=data, - timeout=90, - if "PORTAL200_WRITE=PASS" not in out: - fail("WRITE_FAILED") -def install_candidate(target): - for rel in [ - "Pub/Controller/Portal.php", - "Service/PortalData.php", - "_data/templates.xml", - ]: - guest_write(target, rel, (CAND_ROOT / rel).read_bytes()) -def restore_old(target): -test -f "$B/Pub/Controller/Portal.php" -test -f "$B/_data/templates.xml" -cp -p -- "$B/Pub/Controller/Portal.php" "$P/Pub/Controller/Portal.php" -cp -p -- "$B/_data/templates.xml" "$P/_data/templates.xml" -rm -f -- "$P/Service/PortalData.php" -rmdir -- "$P/Service" 2>/dev/null || true -printf 'PORTAL200_SOURCE_RESTORE=PASS\n' - if "PORTAL200_SOURCE_RESTORE=PASS" not in out: - fail("SOURCE_RESTORE_FAILED") -def import_addon(target): - ["/usr/bin/php8.3"], - input_bytes=IMPORT_PHP.encode(), - timeout=240, - if "PORTAL200_IMPORT_JSON=" not in out: - fail("IMPORT_FAILED") -def runtime_verify(target, state): - src = RUNTIME_VERIFY_PHP_TEMPLATE.replace("__STATE__", state) - out = qga(target, ["/usr/bin/php8.3"], input_bytes=src.encode(), timeout=120) - line = next( - (x for x in out.splitlines() if x.startswith("PORTAL200_RUNTIME_JSON=")), - None, - if not line: - fail("RUNTIME_MARKER_MISSING") - data = json.loads(line.split("=", 1)[1]) - fail("RUNTIME_JSON") - emit("PORTAL200_RUNTIME", {"target": target, **data}) - return data -def http_verify(target, state): - argv = ["/usr/bin/python3", "-", target, state] - if target == "prod": - try: - p = subprocess.run( - fail("HTTP_PROD_TIMEOUT") - out = p.stdout.decode("utf-8", "replace") - err = p.stderr.decode("utf-8", "replace") - line = next( - (x for x in out.splitlines() if x.startswith("PORTAL200_HTTP_JSON=")), - None, - data = None - if line: - try: - data = json.loads(line.split("=", 1)[1]) - emit("PORTAL200_HTTP", {"target": target, "state": state, **data}) - except Exception: - fail("HTTP_JSON") - if p.returncode != 0: - emit( - "PORTAL200_HTTP_EXEC", - { - "target": target, - "state": state, - "rc": p.returncode, - "stderr": err[:800], - }, - ) - fail("HTTP_PROD_RC") - input_bytes=HTTP_VERIFY_PY.encode(), - timeout=180, - (x for x in out.splitlines() if x.startswith("PORTAL200_HTTP_JSON=")), - fail("HTTP_MARKER_MISSING") - fail("HTTP_JSON") - emit("PORTAL200_HTTP", {"target": target, "state": state, **data}) -def verify_new(target, baseline): - data = inspect(target) - if not app_contract(data, target): - fail("APP_CONTRACT_NEW") - if not source_is_new(data): - timeout=40, - if "status: running" not in p.stdout.decode(): - fail("VM211_NOT_RUNNING") - emit("PORTAL200_CLUSTER", "PASS") -def result_contract(status, changes, rollback_started, rollback_restored): - emit( - "HOMELAB_RESULT_CONTRACT", - { - "version": 1, - "command_id": CID, -emit("NEWFI200_BEGIN") -emit("COMMAND_ID", CID) -emit("PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE", 22) -emit( - "PORTAL200_FLOW", - "candidate->staging-apply->rollback-proof->staging-reapply->git-push->production", -) -staging_backup = False -staging_mutated = False -staging_final_new = False -prod_backup = False -prod_mutated = False -git_files_changed = False -s_old_templates = None -p_old_templates = None -try: - compile(HTTP_VERIFY_PY, "", "exec") - for rel, (size, sha) in EXPECTED_NEW.items(): - p = CAND_ROOT / rel - if not p.is_file() or file_meta(p) != (size, sha): - fail("CANDIDATE_IDENTITY") - tree = ET.parse(CAND_ROOT / "_data/templates.xml") - titles = sorted(x.attrib.get("title") for x in tree.getroot().findall("template")) - if titles != ["newfi_portal", "newfi_portal.less"]: - fail("CANDIDATE_TEMPLATE_SET") - service = (CAND_ROOT / "Service/PortalData.php").read_text("utf-8") - if any( - token in service - for token in [ - "\\XF::db(", - "->save(", - "->delete(", - "INSERT ", - "DELETE ", - "REPLACE ", - ] - ): - fail("CANDIDATE_MUTATION_SCAN") - emit("PORTAL200_CANDIDATE_IDENTITY", "PASS") - "PORTAL200_ORCHESTRATION_FIXES", - "staging_503_without_css_links": True, - "failure_status": "FAIL", - "prod_http_via_pve01_public_route": True, - "css_href_regex_fixed": True, - }, - remote = remote_out.split()[0] if remote_out else "" - if branch != BRANCH or head != OLD_HEAD or remote != OLD_HEAD or status: - fail("GIT_PRESTATE") - "PORTAL200_GIT_PRESTATE", - {"branch": branch, "head": head, "remote": remote, "clean": True}, - cluster_guard() - lint_php("staging", INSPECT_PHP, "INSPECT_STAGING") - lint_php("staging", IMPORT_PHP, "IMPORT_STAGING") - lint_php("staging", RUNTIME_VERIFY_PHP_TEMPLATE.replace("__STATE__", "new"), "RUNTIME_STAGING") - lint_php("prod", INSPECT_PHP, "INSPECT_PROD") - lint_php("prod", IMPORT_PHP, "IMPORT_PROD") - lint_php("prod", RUNTIME_VERIFY_PHP_TEMPLATE.replace("__STATE__", "new"), "RUNTIME_PROD") - emit("PORTAL200_RUNTIME_HELPER_LINT", "PASS") - s0 = inspect("staging") - p0 = inspect("prod") - if not app_contract(s0, "staging") or not app_contract(p0, "prod"): - fail("APP_PRESTATE") - if not source_is_old(s0) or not source_is_old(p0): - fail("SOURCE_PRESTATE") - if s0.get("templates") != p0.get("templates"): - s_error = int(s0["application"]["error_max_id"]) - p_error = int(p0["application"]["error_max_id"]) - s_old_templates = s0["templates"] - p_old_templates = p0["templates"] - emit("PORTAL200_ERROR_BASELINE", {"staging": s_error, "prod": p_error}) - http_verify("staging", "old") - runtime_verify("prod", "old") - http_verify("prod", "old") - assert_no_new_errors("staging", s_error, "preflight") - assert_no_new_errors("prod", p_error, "preflight") - emit("PORTAL200_PREFLIGHT", "PASS") - guest_backup("staging") - staging_backup = True - staging_mutated = True - install_candidate("staging") - import_addon("staging") - verify_new("staging", s_error) - emit("PORTAL200_STAGING_FIRST_APPLY", "PASS") - restore_old("staging") - verify_old("staging", s_error, s_old_templates) - staging_mutated = False - emit("PORTAL200_STAGING_ROLLBACK_PROOF", "PASS") - staging_final_new = True - emit("PORTAL200_STAGING_FINAL", "PASS") - for rel in EXPECTED_NEW: - dest = GIT_PORTAL / rel - old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None - dest.parent.mkdir(parents=True, exist_ok=True) - shutil.copyfile(CAND_ROOT / rel, dest) - git_files_changed = True - if remote_head != new_head: - fail("GIT_REMOTE_HEAD") - push_confirmed = True - "PORTAL200_GIT_PROMOTION", - "old_head": OLD_HEAD, - "new_head": new_head, - "remote_head": remote_head, - "result": "PASS", - fail("PROD_ERROR_DRIFT") - guest_backup("prod") - prod_backup = True - prod_mutated = True - install_candidate("prod") - import_addon("prod") - verify_new("prod", p_error) - emit("PORTAL200_PRODUCTION", "PASS") - sf = verify_new("staging", s_error) - pf = verify_new("prod", p_error) - if sf["source"] != pf["source"]: - fail("FINAL_SOURCE_PARITY") - if sf["templates"] != pf["templates"]: - fail("FINAL_TEMPLATE_PARITY") - if git("rev-parse", "HEAD") != new_head: - if git("status", "--porcelain=v1", "--untracked-files=all"): - fail("FINAL_GIT_DIRTY") - "PORTAL200_FINAL_EVIDENCE", - "commit": new_head, - "source_files": 3, - "templates": 2, - "staging_error_baseline": s_error, - "prod_error_baseline": p_error, - emit("PORTAL200_PORTAL", "PASS") - emit("PORTAL200_STAGING", "PASS") - emit("PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS", 21) - result_contract("OK", True, False, None) - emit("NEWFI200_END") - raise SystemExit(0) -except PortalError as exc: - code = str(exc) - emit("PORTAL200_FAILURE", code) - if push_confirmed: - if prod_mutated and prod_backup: - failure_rollback_started = True - restore_old("prod") - import_addon("prod") - verify_old("prod", p_error, p_old_templates, strict_errors=False) - prod_mutated = False - failure_rollback_restored = False - emit("PORTAL200_FAILURE_PROD_ROLLBACK", "PASS_PARTIAL_OVERALL_STATE_CHANGED") - emit("PORTAL200_FAILURE_PROD_ROLLBACK", "FAIL") - else: - if staging_mutated and staging_backup: - restore_old("staging") - import_addon("staging") - verify_old("staging", s_error, s_old_templates, strict_errors=False) - staging_mutated = False - staging_final_new = False - failure_rollback_restored = True - emit("PORTAL200_FAILURE_STAGING_ROLLBACK", "PASS") - emit("PORTAL200_FAILURE_STAGING_ROLLBACK", "FAIL") - if git_files_changed and not commit_created: - for rel, old in old_git_bytes.items(): - dest = GIT_PORTAL / rel - if old is None: - if dest.exists(): - dest.unlink() - try: - service_dir.rmdir() - except OSError: - pass - emit("PORTAL200_FAILURE_GIT_WORKTREE_RESTORE", "PASS") - emit("PORTAL200_FAILURE_GIT_WORKTREE_RESTORE", "FAIL") - if commit_created: - "PORTAL200_FAILURE_GIT_HISTORY", - "COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED", - persisted = bool(push_confirmed or commit_created or staging_final_new or prod_mutated) - if failure_rollback_started and failure_rollback_restored is None: - failure_rollback_restored = False - result_contract( - "FAIL", - persisted, - failure_rollback_started, - failure_rollback_restored, - emit("PORTAL200_ROADMAP_REMAINS", 22) - raise SystemExit(1) -except Exception: - emit("PORTAL200_FAILURE", "UNEXPECTED_EXCEPTION") - bool(push_confirmed or commit_created or staging_final_new or prod_mutated), - False, -CANDIDATE_LAYER_1_RELEVANT_END -CANDIDATE_LAYER_1_FULL_BEGIN -import hashlib -import json -import pathlib -import shlex -import shutil -import subprocess -import sys -import xml.etree.ElementTree as ET - -CID = "NEWFI-260907-PORTAL200-APPLY3" -BRANCH = "implementation/newfi-closed-mvp-20260906" -OLD_HEAD = "06bab5acd124e768f70305b3d7eca2f595871890" -WT = pathlib.Path("/root/newfi-portal/.worktrees/implementation-newfi-closed-mvp-20260906") -CAND_ROOT = pathlib.Path("/root/newfi-portal200-candidate/src/addons/Newfi/Portal") -PORTAL_REL = pathlib.Path("src/addons/Newfi/Portal") -GIT_PORTAL = WT / PORTAL_REL -EXPECTED_NEW = { - "Pub/Controller/Portal.php": (363, "619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d"), - "Service/PortalData.php": (5213, "4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d"), - "_data/templates.xml": (10977, "1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861"), -} -EXPECTED_OLD = { - "Pub/Controller/Portal.php": (247, "53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3"), - "_data/templates.xml": (1461, "c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf"), -} -EXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)] -BACKUP = f"/root/newfi-portal200-backup-{CID}" - - -class PortalError(RuntimeError): - pass - - -def emit(key, value=None): - if value is None: - print(key, flush=True) - elif isinstance(value, (dict, list, tuple, bool, int, float)): - print( - key + "=" + json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")), - flush=True, - ) - else: - print(key + "=" + str(value), flush=True) - - -def fail(code): - raise PortalError(code) - - -def sha_bytes(data): - return hashlib.sha256(data).hexdigest() - - -def file_meta(path): - data = pathlib.Path(path).read_bytes() - return len(data), sha_bytes(data) - - -def run(cmd, *, input_bytes=None, timeout=180, ok=(0,)): - try: - p = subprocess.run( - cmd, - input=input_bytes, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - timeout=timeout, - check=False, - ) - except subprocess.TimeoutExpired: - fail("SUBPROCESS_TIMEOUT") - if p.returncode not in ok: - fail("SUBPROCESS_RC") - return p - - -def git(*args, ok=(0,), timeout=120): - p = run(["/usr/bin/git", "-C", str(WT), *args], timeout=timeout, ok=ok) - return p.stdout.decode("utf-8", "replace").strip() - - -def qga(target, argv, *, input_bytes=None, timeout=180): - if target == "prod": - cmd = [ - "/usr/sbin/qm", - "guest", - "exec", - "210", - "--synchronous", - "1", - "--timeout", - str(timeout), - ] - if input_bytes is not None: - cmd += ["--pass-stdin", "1"] - cmd += ["--", *argv] - elif target == "staging": - q = [ - "/usr/sbin/qm", - "guest", - "exec", - "211", - "--synchronous", - "1", - "--timeout", - str(timeout), - ] - if input_bytes is not None: - q += ["--pass-stdin", "1"] - q += ["--", *argv] - remote = ( - "test \"$(id -u):$(hostname -s)\" = 0:pve03 || exit 72; " - "for f in /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md /etc/pve/31_HOMELAB_REFERENCE.md; do " - "test -r \"$f\" && test -s \"$f\" && cat -- \"$f\" >/dev/null || exit 73; done; exec " - + " ".join(shlex.quote(x) for x in q) - ) - cmd = [ - "/usr/bin/ssh", - "-T", - "-o", - "BatchMode=yes", - "-o", - "StrictHostKeyChecking=yes", - "-o", - "UpdateHostKeys=no", - "-o", - "ConnectTimeout=10", - "-o", - "ConnectionAttempts=1", - "root@pve03", - remote, - ] - else: - fail("BAD_TARGET") - - p = run(cmd, input_bytes=input_bytes, timeout=timeout + 30) - try: - data = json.loads(p.stdout.decode("utf-8")) - except Exception: - fail("QGA_JSON") - if data.get("exited") not in (1, True) or data.get("exitcode") != 0: - fail("QGA_GUEST_RC") - if data.get("out-truncated") or data.get("err-truncated"): - fail("QGA_TRUNCATED") - return data.get("out-data", "") - - -INSPECT_PHP = r'''db(); -function srcMeta(string $path): ?array { - if (!is_file($path)) { return null; } - return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)]; -} -$portal = $root . '/src/addons/Newfi/Portal'; -$templates = $db->fetchAll( - "SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, " . - "LOCATE('data-newfi-portal200', template) > 0 AS html_marker, " . - "LOCATE('--newfi-portal200', template) > 0 AS css_marker " . - "FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title" -); -$result = [ - 'hostname' => gethostname(), - 'application' => [ - 'boardActive' => (string)$db->fetchOne("SELECT option_value FROM xf_option WHERE option_id='boardActive'"), - 'boardUrl' => (string)$db->fetchOne("SELECT option_value FROM xf_option WHERE option_id='boardUrl'"), - 'indexRoute' => (string)$db->fetchOne("SELECT option_value FROM xf_option WHERE option_id='indexRoute'"), - 'defaultStyleId' => (string)$db->fetchOne("SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'"), - 'error_count' => (int)$db->fetchOne("SELECT COUNT(*) FROM xf_error_log"), - 'error_max_id' => (int)$db->fetchOne("SELECT COALESCE(MAX(error_id),0) FROM xf_error_log") - ], - 'addon' => $db->fetchRow( - "SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action " . - "FROM xf_addon WHERE addon_id='Newfi/Portal'" - ), - 'source' => [ - 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'), - 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'), - '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml') - ], - 'templates' => $templates -]; -echo "PORTAL200_INSPECT_JSON_BEGIN\n"; -echo json_encode( - $result, - JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE -); -echo "\nPORTAL200_INSPECT_JSON_END\n"; -?>''' - - -IMPORT_PHP = r'''addOnManager()->getById('Newfi/Portal'); -if (!$addOn || !$addOn->isInstalled()) { - fwrite(STDERR, "ADDON_NOT_INSTALLED\n"); - exit(30); -} -$addOn->preRebuild(); -$dataManager = $app->addOnDataManager(); -$imported = []; -foreach ($dataManager->getDataTypeClasses() as $dataType) { - $handler = $dataManager->getDataTypeHandler($dataType); - $xml = $handler->openTypeFile($addOn->getDataDirectory()); - if (!$xml || !$xml->count()) { - $handler->deleteAddOnData($addOn->addon_id); - continue; - } - $start = 0; - $guard = 0; - while (true) { - $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0); - if (!$last) { break; } - if ($last <= $start || ++$guard > 100000) { - fwrite(STDERR, "IMPORT_PROGRESS_INVALID\n"); - exit(31); - } - $start = $last; - } - $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml); - \XF::triggerRunOnce(); - $imported[] = $handler->getContainerTag(); -} -$addOn->postDataImport(); -$repo = $app->repository(\XF\Repository\StyleRepository::class); -$repo->updateAllStylesLastModifiedDateLater(); -$addOn->postRebuild(); -echo "PORTAL200_IMPORT_JSON=" . json_encode( - ['result'=>'PASS','containers'=>$imported], - JSON_UNESCAPED_SLASHES -) . "\n"; -?>''' - - -ERRORS_PHP_TEMPLATE = r'''db(); -$b=%d; -$rows=$db->fetchAll( - "SELECT error_id,exception_type,message,filename,line FROM xf_error_log WHERE error_id>? ORDER BY error_id", - [$b] -); -echo json_encode( - $rows, - JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE -); -?>''' - - -RUNTIME_VERIFY_PHP_TEMPLATE = r'''$state]; -if ($state === 'new') { - $service = new \Newfi\Portal\Service\PortalData($app); - $portal = $service->build(); - $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]); - $result['sections'] = []; - foreach (['forums','resources','media','events','dogs'] as $key) { - $section = $portal[$key] ?? null; - if (!is_array($section)) { fwrite(STDERR, "SECTION_MISSING\n"); exit(60); } - $result['sections'][$key] = [ - 'available'=>(bool)($section['available'] ?? false), - 'hasItems'=>(bool)($section['hasItems'] ?? false), - 'status'=>(string)($section['status'] ?? '') - ]; - } - $result['html_marker'] = strpos($html, 'data-newfi-portal200="1"') !== false; - $result['locked_resources'] = strpos($html, 'data-portal-locked="resources"') !== false; - $result['locked_media'] = strpos($html, 'data-portal-locked="media"') !== false; - $result['html_bytes'] = strlen($html); - if (!$result['html_marker']) { fwrite(STDERR, "HTML_MARKER_MISSING\n"); exit(61); } - if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) { - fwrite(STDERR, "GUEST_PERMISSION_CONTRACT_CHANGED\n"); exit(62); - } - if (!$result['locked_resources'] || !$result['locked_media']) { - fwrite(STDERR, "LOCKED_COPY_MISSING\n"); exit(63); - } - if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) { - fwrite(STDERR, "PUBLIC_SECTION_UNAVAILABLE\n"); exit(64); - } -} else if ($state === 'old') { - $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []); - $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false; - $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false; - $result['html_bytes'] = strlen($html); - if ($result['html_marker'] || !$result['old_copy']) { - fwrite(STDERR, "OLD_RENDER_CONTRACT\n"); exit(65); - } -} else { - exit(66); -} -echo "PORTAL200_RUNTIME_JSON=" . json_encode( - $result, - JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE -) . "\n"; -?>''' - - -HTTP_VERIFY_PY = r'''import html,json,re,subprocess,sys,urllib.parse -mode=sys.argv[1] -state=sys.argv[2] -if mode=='staging': - host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503 -elif mode=='prod': - host='newfi.ru'; scheme='https'; port='443'; expected_status=200 -else: - raise SystemExit(40) -def fetch(url): - cmd=[ - '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10' - ] - if mode=='staging': - cmd += ['--resolve',f'{host}:{port}:127.0.0.1'] - cmd += [ - '-o','-','-w','\n__PORTAL_STATUS__%{http_code}',url - ] - p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False) - if p.returncode!=0: raise SystemExit(41) - text=p.stdout.decode('utf-8','replace') - marker='\n__PORTAL_STATUS__' - if marker not in text: raise SystemExit(42) - body,status=text.rsplit(marker,1) - return int(status.strip()),body -root=f'{scheme}://{host}/' -status,body=fetch(root) -hrefs=[] -for raw in re.findall(r"href=[\"']([^\"']*css\.php[^\"']*)[\"']",body,re.I): - raw=html.unescape(raw) - hrefs.append(urllib.parse.urljoin(root,raw)) -normal_css=[] -normal_status=[] -for u in hrefs: - s,b=fetch(u) - normal_status.append(s) - normal_css.append(b) -normal_css_text='\n'.join(normal_css) -forced_css_marker=None -forced_css_status=None -if mode=='staging' and state=='new' and hrefs: - u=urllib.parse.urlsplit(hrefs[0]) - pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True) - out=[] - found=False - for k,v in pairs: - if k=='css': - found=True - parts=[x for x in v.split(',') if x] - if 'public:newfi_portal.less' not in parts: - parts.append('public:newfi_portal.less') - v=','.join(parts) - out.append((k,v)) - if found: - forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment)) - forced_css_status,forced_body=fetch(forced) - forced_css_marker='--newfi-portal200' in forced_body -result={ - 'status':status, - 'bytes':len(body.encode()), - 'html_marker':'data-newfi-portal200="1"' in body, - 'css_marker':'--newfi-portal200' in normal_css_text, - 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body, - 'css_count':len(hrefs), - 'css_status':normal_status, - 'forced_css_status':forced_css_status, - 'forced_css_marker':forced_css_marker, -} -print('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':'))) -if status!=expected_status: raise SystemExit(45) -if mode=='prod': - if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46) -elif mode=='staging': - if any(x!=200 for x in normal_status): raise SystemExit(46) -else: - raise SystemExit(50) -if mode=='prod' and state=='new': - if not result['html_marker'] or not result['css_marker']: raise SystemExit(47) -elif mode=='prod' and state=='old': - if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48) -elif mode=='staging' and state=='new': - if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49) -elif mode=='staging' and state=='old': - pass -else: - raise SystemExit(50) -''' - - -def parse_inspect(text): - b = "PORTAL200_INSPECT_JSON_BEGIN\n" - e = "\nPORTAL200_INSPECT_JSON_END" - if b not in text or e not in text: - fail("INSPECT_MARKERS") - try: - return json.loads(text.split(b, 1)[1].split(e, 1)[0]) - except Exception: - fail("INSPECT_JSON") - - -def inspect(target): - return parse_inspect( - qga(target, ["/usr/bin/php8.3"], input_bytes=INSPECT_PHP.encode(), timeout=120) - ) - - -def source_is_old(data): - src = data["source"] - for rel, (size, sha) in EXPECTED_OLD.items(): - row = src.get(rel) - if not row or row.get("bytes") != size or row.get("sha256") != sha: - return False - return src.get("Service/PortalData.php") is None - - -def source_is_new(data): - src = data["source"] - for rel, (size, sha) in EXPECTED_NEW.items(): - row = src.get(rel) - if not row or row.get("bytes") != size or row.get("sha256") != sha: - return False - return True - - -def template_state_new(data): - rows = data.get("templates", []) - rows0 = [r for r in rows if int(r.get("style_id", -1)) == 0] - titles = [r.get("title") for r in rows0] - by = {r.get("title"): r for r in rows0} - return ( - titles == ["newfi_portal", "newfi_portal.less"] - and int(by["newfi_portal"].get("html_marker", 0)) == 1 - and int(by["newfi_portal.less"].get("css_marker", 0)) == 1 - ) - - -def app_contract(data, target): - a = data.get("application", {}) - addon = data.get("addon") or {} - expected = { - "staging": ("0", "http://newfi-staging.gram1.ru"), - "prod": ("1", "https://newfi.ru"), - }[target] - return ( - a.get("boardActive") == expected[0] - and a.get("boardUrl") == expected[1] - and a.get("indexRoute") == "portal/" - and a.get("defaultStyleId") == "4" - and addon.get("addon_id") == "Newfi/Portal" - and int(addon.get("version_id", 0)) == 1000010 - and addon.get("version_string") == "1.0.0 Alpha 1" - and int(addon.get("active", 0)) == 1 - and int(addon.get("is_processing", 1)) == 0 - ) - - -def new_errors(target, baseline): - raw = qga( - target, - ["/usr/bin/php8.3"], - input_bytes=(ERRORS_PHP_TEMPLATE % int(baseline)).encode(), - timeout=90, - ) - try: - return json.loads(raw) - except Exception: - fail("ERROR_LOG_JSON") - - -def assert_no_new_errors(target, baseline, phase): - rows = new_errors(target, baseline) - if rows: - emit( - "PORTAL200_NEW_XF_ERRORS", - { - "target": target, - "phase": phase, - "count": len(rows), - "ids": [r.get("error_id") for r in rows], - }, - ) - fail("NEW_XF_ERRORS") - - -def guest_backup(target): - script = f'''set -eu -B={shlex.quote(BACKUP)} -P=/var/www/forums/newfi/public/src/addons/Newfi/Portal -test ! -e "$B" -install -d -m 700 "$B/Pub/Controller" "$B/_data" -cp -p -- "$P/Pub/Controller/Portal.php" "$B/Pub/Controller/Portal.php" -cp -p -- "$P/_data/templates.xml" "$B/_data/templates.xml" -test ! -e "$P/Service/PortalData.php" -printf 'PORTAL200_BACKUP=PASS\n' -''' - out = qga(target, ["/bin/bash", "-lc", script], timeout=90) - if "PORTAL200_BACKUP=PASS" not in out: - fail("BACKUP_FAILED") - - -def guest_write(target, rel, data): - dest = f"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}" - parent = str(pathlib.PurePosixPath(dest).parent) - script = ( - "set -eu; umask 022; " - + "install -d -m 755 " - + shlex.quote(parent) - + "; T=$(mktemp " - + shlex.quote(parent + "/.portal200.XXXXXX") - + "); cat >\"$T\"; chmod 0644 \"$T\"; chown root:root \"$T\"; " - + "mv -f -- \"$T\" " - + shlex.quote(dest) - + "; printf 'PORTAL200_WRITE=PASS\\n'" - ) - out = qga( - target, - ["/bin/bash", "-lc", script], - input_bytes=data, - timeout=90, - ) - if "PORTAL200_WRITE=PASS" not in out: - fail("WRITE_FAILED") - - -def install_candidate(target): - for rel in [ - "Pub/Controller/Portal.php", - "Service/PortalData.php", - "_data/templates.xml", - ]: - guest_write(target, rel, (CAND_ROOT / rel).read_bytes()) - - -def restore_old(target): - script = f'''set -eu -B={shlex.quote(BACKUP)} -P=/var/www/forums/newfi/public/src/addons/Newfi/Portal -test -f "$B/Pub/Controller/Portal.php" -test -f "$B/_data/templates.xml" -cp -p -- "$B/Pub/Controller/Portal.php" "$P/Pub/Controller/Portal.php" -cp -p -- "$B/_data/templates.xml" "$P/_data/templates.xml" -rm -f -- "$P/Service/PortalData.php" -rmdir -- "$P/Service" 2>/dev/null || true -printf 'PORTAL200_SOURCE_RESTORE=PASS\n' -''' - out = qga(target, ["/bin/bash", "-lc", script], timeout=90) - if "PORTAL200_SOURCE_RESTORE=PASS" not in out: - fail("SOURCE_RESTORE_FAILED") - - -def import_addon(target): - out = qga( - target, - ["/usr/bin/php8.3"], - input_bytes=IMPORT_PHP.encode(), - timeout=240, - ) - if "PORTAL200_IMPORT_JSON=" not in out: - fail("IMPORT_FAILED") - - -def runtime_verify(target, state): - src = RUNTIME_VERIFY_PHP_TEMPLATE.replace("__STATE__", state) - out = qga(target, ["/usr/bin/php8.3"], input_bytes=src.encode(), timeout=120) - line = next( - (x for x in out.splitlines() if x.startswith("PORTAL200_RUNTIME_JSON=")), - None, - ) - if not line: - fail("RUNTIME_MARKER_MISSING") - try: - data = json.loads(line.split("=", 1)[1]) - except Exception: - fail("RUNTIME_JSON") - emit("PORTAL200_RUNTIME", {"target": target, **data}) - return data - - -def http_verify(target, state): - argv = ["/usr/bin/python3", "-", target, state] - if target == "prod": - try: - p = subprocess.run( - argv, - input=HTTP_VERIFY_PY.encode(), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - timeout=180, - check=False, - ) - except subprocess.TimeoutExpired: - fail("HTTP_PROD_TIMEOUT") - out = p.stdout.decode("utf-8", "replace") - err = p.stderr.decode("utf-8", "replace") - line = next( - (x for x in out.splitlines() if x.startswith("PORTAL200_HTTP_JSON=")), - None, - ) - data = None - if line: - try: - data = json.loads(line.split("=", 1)[1]) - emit("PORTAL200_HTTP", {"target": target, "state": state, **data}) - except Exception: - fail("HTTP_JSON") - if p.returncode != 0: - emit( - "PORTAL200_HTTP_EXEC", - { - "target": target, - "state": state, - "rc": p.returncode, - "stderr": err[:800], - }, - ) - fail("HTTP_PROD_RC") - if data is None: - fail("HTTP_MARKER_MISSING") - return data - - out = qga( - target, - argv, - input_bytes=HTTP_VERIFY_PY.encode(), - timeout=180, - ) - line = next( - (x for x in out.splitlines() if x.startswith("PORTAL200_HTTP_JSON=")), - None, - ) - if not line: - fail("HTTP_MARKER_MISSING") - try: - data = json.loads(line.split("=", 1)[1]) - except Exception: - fail("HTTP_JSON") - emit("PORTAL200_HTTP", {"target": target, "state": state, **data}) - return data - - -def verify_new(target, baseline): - data = inspect(target) - if not app_contract(data, target): - fail("APP_CONTRACT_NEW") - if not source_is_new(data): - fail("SOURCE_NEW_MISMATCH") - if not template_state_new(data): - fail("TEMPLATE_NEW_MISMATCH") - runtime_verify(target, "new") - http_verify(target, "new") - assert_no_new_errors(target, baseline, "verify-new") - return inspect(target) - - -def verify_old(target, baseline, old_templates, strict_errors=True): - data = inspect(target) - if not app_contract(data, target): - fail("APP_CONTRACT_OLD") - if not source_is_old(data): - fail("SOURCE_OLD_MISMATCH") - if data.get("templates") != old_templates: - fail("TEMPLATE_ROLLBACK_MISMATCH") - if target == "prod": - runtime_verify(target, "old") - http_verify(target, "old") - if strict_errors: - assert_no_new_errors(target, baseline, "verify-old") - return inspect(target) - - -def lint_php(target, source, label): - out = qga( - target, - ["/usr/bin/php8.3", "-l"], - input_bytes=source.encode(), - timeout=90, - ) - if "No syntax errors detected" not in out: - fail("PHP_LINT_" + label) - - -def cluster_guard(): - p = run(["/usr/sbin/qm", "status", "210"], timeout=30) - if "status: running" not in p.stdout.decode(): - fail("VM210_NOT_RUNNING") - p = run( - [ - "/usr/bin/ssh", - "-T", - "-o", - "BatchMode=yes", - "-o", - "StrictHostKeyChecking=yes", - "-o", - "UpdateHostKeys=no", - "-o", - "ConnectTimeout=10", - "root@pve03", - "/usr/sbin/qm status 211", - ], - timeout=40, - ) - if "status: running" not in p.stdout.decode(): - fail("VM211_NOT_RUNNING") - emit("PORTAL200_CLUSTER", "PASS") - - -def result_contract(status, changes, rollback_started, rollback_restored): - emit( - "HOMELAB_RESULT_CONTRACT", - { - "version": 1, - "command_id": CID, - "status": status, - "changes_made": bool(changes), - "rollback_started": bool(rollback_started), - "rollback_restored": rollback_restored, - }, - ) - - -emit("NEWFI200_BEGIN") -emit("COMMAND_ID", CID) -emit("PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE", 22) -emit( - "PORTAL200_FLOW", - "candidate->staging-apply->rollback-proof->staging-reapply->git-push->production", -) - -staging_backup = False -staging_mutated = False -staging_final_new = False -prod_backup = False -prod_mutated = False -git_files_changed = False -commit_created = False -push_confirmed = False -failure_rollback_started = False -failure_rollback_restored = None -old_git_bytes = {} -new_head = None -s_error = None -p_error = None -s_old_templates = None -p_old_templates = None - -try: - compile(HTTP_VERIFY_PY, "", "exec") - for rel, (size, sha) in EXPECTED_NEW.items(): - p = CAND_ROOT / rel - if not p.is_file() or file_meta(p) != (size, sha): - fail("CANDIDATE_IDENTITY") - tree = ET.parse(CAND_ROOT / "_data/templates.xml") - titles = sorted(x.attrib.get("title") for x in tree.getroot().findall("template")) - if titles != ["newfi_portal", "newfi_portal.less"]: - fail("CANDIDATE_TEMPLATE_SET") - service = (CAND_ROOT / "Service/PortalData.php").read_text("utf-8") - if any( - token in service - for token in [ - "\\XF::db(", - "->save(", - "->delete(", - "INSERT ", - "UPDATE ", - "DELETE ", - "REPLACE ", - ] - ): - fail("CANDIDATE_MUTATION_SCAN") - emit("PORTAL200_CANDIDATE_IDENTITY", "PASS") - emit( - "PORTAL200_ORCHESTRATION_FIXES", - { - "staging_503_without_css_links": True, - "failure_status": "FAIL", - "prod_http_via_pve01_public_route": True, - "css_href_regex_fixed": True, - }, - ) - - branch = git("branch", "--show-current") - head = git("rev-parse", "HEAD") - status = git("status", "--porcelain=v1", "--untracked-files=all") - remote_out = git("ls-remote", "origin", f"refs/heads/{BRANCH}") - remote = remote_out.split()[0] if remote_out else "" - if branch != BRANCH or head != OLD_HEAD or remote != OLD_HEAD or status: - fail("GIT_PRESTATE") - emit( - "PORTAL200_GIT_PRESTATE", - {"branch": branch, "head": head, "remote": remote, "clean": True}, - ) - cluster_guard() - - lint_php("staging", INSPECT_PHP, "INSPECT_STAGING") - lint_php("staging", IMPORT_PHP, "IMPORT_STAGING") - lint_php("staging", RUNTIME_VERIFY_PHP_TEMPLATE.replace("__STATE__", "new"), "RUNTIME_STAGING") - lint_php("prod", INSPECT_PHP, "INSPECT_PROD") - lint_php("prod", IMPORT_PHP, "IMPORT_PROD") - lint_php("prod", RUNTIME_VERIFY_PHP_TEMPLATE.replace("__STATE__", "new"), "RUNTIME_PROD") - emit("PORTAL200_RUNTIME_HELPER_LINT", "PASS") - - s0 = inspect("staging") - p0 = inspect("prod") - if not app_contract(s0, "staging") or not app_contract(p0, "prod"): - fail("APP_PRESTATE") - if not source_is_old(s0) or not source_is_old(p0): - fail("SOURCE_PRESTATE") - if s0.get("templates") != p0.get("templates"): - fail("TEMPLATE_PRESTATE_PARITY") - s_error = int(s0["application"]["error_max_id"]) - p_error = int(p0["application"]["error_max_id"]) - s_old_templates = s0["templates"] - p_old_templates = p0["templates"] - emit("PORTAL200_ERROR_BASELINE", {"staging": s_error, "prod": p_error}) - http_verify("staging", "old") - runtime_verify("prod", "old") - http_verify("prod", "old") - assert_no_new_errors("staging", s_error, "preflight") - assert_no_new_errors("prod", p_error, "preflight") - emit("PORTAL200_PREFLIGHT", "PASS") - - guest_backup("staging") - staging_backup = True - staging_mutated = True - install_candidate("staging") - import_addon("staging") - verify_new("staging", s_error) - emit("PORTAL200_STAGING_FIRST_APPLY", "PASS") - - restore_old("staging") - import_addon("staging") - verify_old("staging", s_error, s_old_templates) - staging_mutated = False - emit("PORTAL200_STAGING_ROLLBACK_PROOF", "PASS") - - staging_mutated = True - install_candidate("staging") - import_addon("staging") - verify_new("staging", s_error) - staging_final_new = True - emit("PORTAL200_STAGING_FINAL", "PASS") - - for rel in EXPECTED_NEW: - dest = GIT_PORTAL / rel - old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None - dest.parent.mkdir(parents=True, exist_ok=True) - shutil.copyfile(CAND_ROOT / rel, dest) - git_files_changed = True - - if git("diff", "--check"): - fail("GIT_DIFF_CHECK") - status_lines = git("status", "--porcelain=v1", "--untracked-files=all").splitlines() - actual_paths = sorted(line[3:] for line in status_lines if len(line) >= 4) - if actual_paths != sorted(EXPECTED_GIT_PATHS): - fail("GIT_SCOPE") - git("add", "--", *EXPECTED_GIT_PATHS) - staged = sorted(x for x in git("diff", "--cached", "--name-only").splitlines() if x) - if staged != sorted(EXPECTED_GIT_PATHS): - fail("GIT_STAGED_SCOPE") - git("commit", "-m", f"feat: build data-backed Newfi portal [{CID}]") - commit_created = True - new_head = git("rev-parse", "HEAD") - if new_head == OLD_HEAD: - fail("GIT_COMMIT_HEAD") - if git("status", "--porcelain=v1", "--untracked-files=all"): - fail("GIT_POSTCOMMIT_DIRTY") - git("push", "origin", f"HEAD:refs/heads/{BRANCH}", timeout=180) - remote_out = git("ls-remote", "origin", f"refs/heads/{BRANCH}") - remote_head = remote_out.split()[0] if remote_out else "" - if remote_head != new_head: - fail("GIT_REMOTE_HEAD") - push_confirmed = True - emit( - "PORTAL200_GIT_PROMOTION", - { - "old_head": OLD_HEAD, - "new_head": new_head, - "remote_head": remote_head, - "result": "PASS", - }, - ) - - p_pre = inspect("prod") - if not source_is_old(p_pre) or p_pre.get("templates") != p_old_templates: - fail("PROD_DRIFT_BEFORE_APPLY") - if int(p_pre["application"]["error_max_id"]) != p_error: - fail("PROD_ERROR_DRIFT") - guest_backup("prod") - prod_backup = True - prod_mutated = True - install_candidate("prod") - import_addon("prod") - verify_new("prod", p_error) - emit("PORTAL200_PRODUCTION", "PASS") - - sf = verify_new("staging", s_error) - pf = verify_new("prod", p_error) - if sf["source"] != pf["source"]: - fail("FINAL_SOURCE_PARITY") - if sf["templates"] != pf["templates"]: - fail("FINAL_TEMPLATE_PARITY") - if git("rev-parse", "HEAD") != new_head: - fail("FINAL_LOCAL_HEAD") - remote_out = git("ls-remote", "origin", f"refs/heads/{BRANCH}") - if not remote_out or remote_out.split()[0] != new_head: - fail("FINAL_REMOTE_HEAD") - if git("status", "--porcelain=v1", "--untracked-files=all"): - fail("FINAL_GIT_DIRTY") - cluster_guard() - emit( - "PORTAL200_FINAL_EVIDENCE", - { - "commit": new_head, - "source_files": 3, - "templates": 2, - "staging_error_baseline": s_error, - "prod_error_baseline": p_error, - }, - ) - emit("PORTAL200_PORTAL", "PASS") - emit("PORTAL200_STAGING", "PASS") - emit("PORTAL200_PRODUCTION", "PASS") - emit("PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS", 21) - result_contract("OK", True, False, None) - emit("NEWFI200_END") - raise SystemExit(0) - -except PortalError as exc: - code = str(exc) - emit("PORTAL200_FAILURE", code) - - if push_confirmed: - if prod_mutated and prod_backup: - failure_rollback_started = True - try: - restore_old("prod") - import_addon("prod") - verify_old("prod", p_error, p_old_templates, strict_errors=False) - prod_mutated = False - failure_rollback_restored = False - emit("PORTAL200_FAILURE_PROD_ROLLBACK", "PASS_PARTIAL_OVERALL_STATE_CHANGED") - except Exception: - failure_rollback_restored = False - emit("PORTAL200_FAILURE_PROD_ROLLBACK", "FAIL") - else: - if staging_mutated and staging_backup: - failure_rollback_started = True - try: - restore_old("staging") - import_addon("staging") - verify_old("staging", s_error, s_old_templates, strict_errors=False) - staging_mutated = False - staging_final_new = False - failure_rollback_restored = True - emit("PORTAL200_FAILURE_STAGING_ROLLBACK", "PASS") - except Exception: - failure_rollback_restored = False - emit("PORTAL200_FAILURE_STAGING_ROLLBACK", "FAIL") - - if git_files_changed and not commit_created: - try: - for rel, old in old_git_bytes.items(): - dest = GIT_PORTAL / rel - if old is None: - if dest.exists(): - dest.unlink() - else: - dest.write_bytes(old) - service_dir = GIT_PORTAL / "Service" - try: - service_dir.rmdir() - except OSError: - pass - emit("PORTAL200_FAILURE_GIT_WORKTREE_RESTORE", "PASS") - except Exception: - emit("PORTAL200_FAILURE_GIT_WORKTREE_RESTORE", "FAIL") - - if commit_created: - emit( - "PORTAL200_FAILURE_GIT_HISTORY", - "COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED", - ) - - persisted = bool(push_confirmed or commit_created or staging_final_new or prod_mutated) - if failure_rollback_started and failure_rollback_restored is None: - failure_rollback_restored = False - result_contract( - "FAIL", - persisted, - failure_rollback_started, - failure_rollback_restored, - ) - emit("PORTAL200_ROADMAP_REMAINS", 22) - emit("NEWFI200_END") - raise SystemExit(1) - -except Exception: - emit("PORTAL200_FAILURE", "UNEXPECTED_EXCEPTION") - result_contract( - "FAIL", - bool(push_confirmed or commit_created or staging_final_new or prod_mutated), - False, - None, - ) - emit("PORTAL200_ROADMAP_REMAINS", 22) - emit("NEWFI200_END") - raise SystemExit(1) - -CANDIDATE_LAYER_1_FULL_END -CANDIDATE_LAYER_2_SOURCE=carrier -CANDIDATE_LAYER_2_SHA256=573c5c94fe59ed072e094281668ce9847dc4a9882019a2b89d8ba5f190d92176 -CANDIDATE_LAYER_2_BYTES=17209 -CANDIDATE_LAYER_2_RELEVANT_BEGIN -exit /b %RC% -# NEWFI54_POWERSHELL_BODY -$ErrorActionPreference='Stop' -$plinkPath='C:\Program Files\PuTTY\plink.exe' -$remotePath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_REMOTE.txt' -$stdoutPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STDOUT.txt' -$stderrPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STDERR.txt' -$statusPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STATUS.txt' - -foreach($p in @($remotePath,$stdoutPath,$stderrPath,$statusPath)) { - Remove-Item -LiteralPath $p -Force -ErrorAction SilentlyContinue -} -if(-not (Test-Path -LiteralPath $plinkPath -PathType Leaf)) { - [IO.File]::WriteAllText($statusPath,'STOP=PLINK_MISSING'+[Environment]::NewLine) - exit 2 -$remote=@' -(set -eu; PATH=/usr/sbin:/usr/bin:/sbin:/bin; LC_ALL=C; export PATH LC_ALL; printf 'TARGET=pve01 STEP=NEWFI54 PORTAL200_APPLY3\n'; case $(id -u):$(hostname -s) in 0:pve01) ;; *) exit 2;; esac; test -f /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && test -r /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && test -s /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && cat -- /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md >/dev/null || exit 2; test -f /etc/pve/31_HOMELAB_REFERENCE.md && test -r /etc/pve/31_HOMELAB_REFERENCE.md && test -s /etc/pve/31_HOMELAB_REFERENCE.md && cat -- /etc/pve/31_HOMELAB_REFERENCE.md >/dev/null || exit 2; printf '\033[31mERROR_REGISTER_CHECK=OK\033[0m\n\033[31mREFERENCE_CHECK=OK\033[0m\n'; umask 077; unset BASH_ENV ENV PYTHONPATH PYTHONHOME; test -f /usr/local/sbin/homelab-chat-run && test ! -L /usr/local/sbin/homelab-chat-run && test -x /usr/local/sbin/homelab-chat-run && test -x /usr/bin/python3 || exit 2; test -d /root/newfi-portal200-candidate || exit 2; d=/root/newfi-run-NEWFI-260907-PORTAL200-APPLY3; mkdir -m 700 -- $d 2>/dev/null || exit 2; printf 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\nSTATE=RESERVED_BEFORE_EXECUTION\n' >$d/attempt.txt; if /usr/local/sbin/homelab-chat-run NEWFI-260907-PORTAL200-APPLY3 change newfi-portal200-deploy -- /usr/bin/python3 -I -B -c 'import base64,hashlib,sys,zlib;d=zlib.decompress(base64.b64decode(sys.argv[1]));assert hashlib.sha256(d).hexdigest()==sys.argv[2];exec(d)' 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 97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184 >$d/wrapper.log 2>&1; then rc=0; else rc=$?; fi; printf 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\n' >$d/receipt.txt; printf 'WRAPPER_RC=' >>$d/receipt.txt; echo $rc >>$d/receipt.txt; printf 'PUBLICATION=GITEA_RUNTIME_EXPECTED\n' >>$d/receipt.txt; exit $rc) -'@ -$remote=$remote.Replace("`r`n","`n").TrimEnd("`n")+"`n" -[IO.File]::WriteAllText($remotePath,$remote,[Text.UTF8Encoding]::new($false)) -$h=[Security.Cryptography.SHA256]::Create() -try { - $actual=([BitConverter]::ToString($h.ComputeHash([IO.File]::ReadAllBytes($remotePath)))).Replace('-','').ToLowerInvariant() -} finally {$h.Dispose()} -$p=Start-Process -FilePath $plinkPath -ArgumentList $args -NoNewWindow -Wait -PassThru -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -$rc=$p.ExitCode -$status=@( - 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3', - 'PLINK_RC='+$rc, - 'REMOTE_RESULT=VERIFY_IN_GITEA', - 'LOCAL_STDOUT='+$stdoutPath, - 'LOCAL_STDERR='+$stderrPath -) -join [Environment]::NewLine -[IO.File]::WriteAllText($statusPath,$status+[Environment]::NewLine,[Text.UTF8Encoding]::new($false)) -exit $rc -CANDIDATE_LAYER_2_RELEVANT_END -CANDIDATE_LAYER_2_FULL_BEGIN -@echo off -setlocal DisableDelayedExpansion -set "SELF=%~f0" -"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "$ErrorActionPreference='Stop'; $t=[IO.File]::ReadAllText($env:SELF).Replace([string][char]13,''); $m='# NEWFI54_POWERSHELL_BODY'+[char]10; $i=$t.IndexOf($m); if($i -lt 0){exit 2}; $b=$t.Substring($i+$m.Length); $h=[Security.Cryptography.SHA256]::Create(); $d=([BitConverter]::ToString($h.ComputeHash([Text.Encoding]::UTF8.GetBytes($b)))).Replace('-','').ToLowerInvariant(); $h.Dispose(); if($d -ne 'fe66d389a08cceef3f1b8d656e926cc75a85f71d1e44b8b8b29ee9fa4f785dfc'){exit 2}; & ([ScriptBlock]::Create($b));" -set "RC=%ERRORLEVEL%" -exit /b %RC% -# NEWFI54_POWERSHELL_BODY -$ErrorActionPreference='Stop' -$plinkPath='C:\Program Files\PuTTY\plink.exe' -$remotePath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_REMOTE.txt' -$stdoutPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STDOUT.txt' -$stderrPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STDERR.txt' -$statusPath=Join-Path $env:TEMP 'NEWFI54_PORTAL200_APPLY3_STATUS.txt' - -foreach($p in @($remotePath,$stdoutPath,$stderrPath,$statusPath)) { - Remove-Item -LiteralPath $p -Force -ErrorAction SilentlyContinue -} - -if(-not (Test-Path -LiteralPath $plinkPath -PathType Leaf)) { - [IO.File]::WriteAllText($statusPath,'STOP=PLINK_MISSING'+[Environment]::NewLine) - exit 2 -} -if(@(Get-Process -Name 'pageant*' -ErrorAction SilentlyContinue).Count -lt 1) { - [IO.File]::WriteAllText($statusPath,'STOP=PAGEANT_NOT_RUNNING'+[Environment]::NewLine) - exit 2 -} - -$remote=@' -(set -eu; PATH=/usr/sbin:/usr/bin:/sbin:/bin; LC_ALL=C; export PATH LC_ALL; printf 'TARGET=pve01 STEP=NEWFI54 PORTAL200_APPLY3\n'; case $(id -u):$(hostname -s) in 0:pve01) ;; *) exit 2;; esac; test -f /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && test -r /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && test -s /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md && cat -- /etc/pve/HOMELAB_ASSISTANT_ERROR_REGISTER.md >/dev/null || exit 2; test -f /etc/pve/31_HOMELAB_REFERENCE.md && test -r /etc/pve/31_HOMELAB_REFERENCE.md && test -s /etc/pve/31_HOMELAB_REFERENCE.md && cat -- /etc/pve/31_HOMELAB_REFERENCE.md >/dev/null || exit 2; printf '\033[31mERROR_REGISTER_CHECK=OK\033[0m\n\033[31mREFERENCE_CHECK=OK\033[0m\n'; umask 077; unset BASH_ENV ENV PYTHONPATH PYTHONHOME; test -f /usr/local/sbin/homelab-chat-run && test ! -L /usr/local/sbin/homelab-chat-run && test -x /usr/local/sbin/homelab-chat-run && test -x /usr/bin/python3 || exit 2; test -d /root/newfi-portal200-candidate || exit 2; d=/root/newfi-run-NEWFI-260907-PORTAL200-APPLY3; mkdir -m 700 -- $d 2>/dev/null || exit 2; printf 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\nSTATE=RESERVED_BEFORE_EXECUTION\n' >$d/attempt.txt; if /usr/local/sbin/homelab-chat-run NEWFI-260907-PORTAL200-APPLY3 change newfi-portal200-deploy -- /usr/bin/python3 -I -B -c 'import base64,hashlib,sys,zlib;d=zlib.decompress(base64.b64decode(sys.argv[1]));assert hashlib.sha256(d).hexdigest()==sys.argv[2];exec(d)' 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 97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184 >$d/wrapper.log 2>&1; then rc=0; else rc=$?; fi; printf 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3\n' >$d/receipt.txt; printf 'WRAPPER_RC=' >>$d/receipt.txt; echo $rc >>$d/receipt.txt; printf 'PUBLICATION=GITEA_RUNTIME_EXPECTED\n' >>$d/receipt.txt; exit $rc) -'@ -$remote=$remote.Replace("`r`n","`n").TrimEnd("`n")+"`n" -[IO.File]::WriteAllText($remotePath,$remote,[Text.UTF8Encoding]::new($false)) - -$h=[Security.Cryptography.SHA256]::Create() -try { - $actual=([BitConverter]::ToString($h.ComputeHash([IO.File]::ReadAllBytes($remotePath)))).Replace('-','').ToLowerInvariant() -} finally {$h.Dispose()} - -if($actual -ne '71da2a66224075527d8aa04c1281fdb7a2b9f078d76c349fc05b48a4b803a4c3') { - [IO.File]::WriteAllText($statusPath,'STOP=REMOTE_HASH_MISMATCH'+[Environment]::NewLine) - exit 2 -} - -$args=@('-batch','-agent','-ssh','-P','22','-m',$remotePath,'root@[PRIVATE_IP]') -$p=Start-Process -FilePath $plinkPath -ArgumentList $args -NoNewWindow -Wait -PassThru -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -$rc=$p.ExitCode - -$status=@( - 'COMMAND_ID=NEWFI-260907-PORTAL200-APPLY3', - 'PLINK_RC='+$rc, - 'REMOTE_RESULT=VERIFY_IN_GITEA', - 'LOCAL_STDOUT='+$stdoutPath, - 'LOCAL_STDERR='+$stderrPath -) -join [Environment]::NewLine -[IO.File]::WriteAllText($statusPath,$status+[Environment]::NewLine,[Text.UTF8Encoding]::new($false)) - -exit $rc - -CANDIDATE_LAYER_2_FULL_END -RECOVERED_VERSION_IDS= -RECOVERED_VERSION_STRINGS== -RECOVERED_SHA256_COUNT=8 -RECOVERED_SHA256=619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d,4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d,1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861,53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3,c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf,fe66d389a08cceef3f1b8d656e926cc75a85f71d1e44b8b8b29ee9fa4f785dfc,97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184,71da2a66224075527d8aa04c1281fdb7a2b9f078d76c349fc05b48a4b803a4c3 -RECOVERY_RESULT=EXACT_OLD_APPLY3_CARRIER_DECODED -LOCAL_APPLY3_EXACT_CARRIER_RECOVERY_END=true +AST_RECOVERY_BEGIN=true +OLD_CARRIER_SHA256=573c5c94fe59ed072e094281668ce9847dc4a9882019a2b89d8ba5f190d92176 +OLD_INNER_SHA256=97a1807a275e5d309591f7a7e044d7d56ba5b2f4c6a068df6353bae147707184 +OLD_INNER_BYTES=34155 +TOP_LEVEL_LITERAL_ASSIGNMENTS={"BRANCH":"implementation/newfi-closed-mvp-20260906","CID":"NEWFI-260907-PORTAL200-APPLY3","ERRORS_PHP_TEMPLATE":"db();\n$b=%d;\n$rows=$db->fetchAll(\n \"SELECT error_id,exception_type,message,filename,line FROM xf_error_log WHERE error_id>? ORDER BY error_id\",\n [$b]\n);\necho json_encode(\n $rows,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\n?>","EXPECTED_NEW":{"Pub/Controller/Portal.php":[363,"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d"],"Service/PortalData.php":[5213,"4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d"],"_data/templates.xml":[10977,"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861"]},"EXPECTED_OLD":{"Pub/Controller/Portal.php":[247,"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3"],"_data/templates.xml":[1461,"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf"]},"HTTP_VERIFY_PY":"import html,json,re,subprocess,sys,urllib.parse\nmode=sys.argv[1]\nstate=sys.argv[2]\nif mode=='staging':\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\nelif mode=='prod':\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\nelse:\n raise SystemExit(40)\ndef fetch(url):\n cmd=[\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\n ]\n if mode=='staging':\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\n cmd += [\n '-o','-','-w','\\n__PORTAL_STATUS__%{http_code}',url\n ]\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\n if p.returncode!=0: raise SystemExit(41)\n text=p.stdout.decode('utf-8','replace')\n marker='\\n__PORTAL_STATUS__'\n if marker not in text: raise SystemExit(42)\n body,status=text.rsplit(marker,1)\n return int(status.strip()),body\nroot=f'{scheme}://{host}/'\nstatus,body=fetch(root)\nhrefs=[]\nfor raw in re.findall(r\"href=[\\\"']([^\\\"']*css\\.php[^\\\"']*)[\\\"']\",body,re.I):\n raw=html.unescape(raw)\n hrefs.append(urllib.parse.urljoin(root,raw))\nnormal_css=[]\nnormal_status=[]\nfor u in hrefs:\n s,b=fetch(u)\n normal_status.append(s)\n normal_css.append(b)\nnormal_css_text='\\n'.join(normal_css)\nforced_css_marker=None\nforced_css_status=None\nif mode=='staging' and state=='new' and hrefs:\n u=urllib.parse.urlsplit(hrefs[0])\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\n out=[]\n found=False\n for k,v in pairs:\n if k=='css':\n found=True\n parts=[x for x in v.split(',') if x]\n if 'public:newfi_portal.less' not in parts:\n parts.append('public:newfi_portal.less')\n v=','.join(parts)\n out.append((k,v))\n if found:\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\n forced_css_status,forced_body=fetch(forced)\n forced_css_marker='--newfi-portal200' in forced_body\nresult={\n 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\n}\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\nif status!=expected_status: raise SystemExit(45)\nif mode=='prod':\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\nelif mode=='staging':\n if any(x!=200 for x in normal_status): raise SystemExit(46)\nelse:\n raise SystemExit(50)\nif mode=='prod' and state=='new':\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\nelif mode=='prod' and state=='old':\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\nelif mode=='staging' and state=='new':\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\nelif mode=='staging' and state=='old':\n pass\nelse:\n raise SystemExit(50)\n","IMPORT_PHP":"addOnManager()->getById('Newfi/Portal');\nif (!$addOn || !$addOn->isInstalled()) {\n fwrite(STDERR, \"ADDON_NOT_INSTALLED\\n\");\n exit(30);\n}\n$addOn->preRebuild();\n$dataManager = $app->addOnDataManager();\n$imported = [];\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\n $handler = $dataManager->getDataTypeHandler($dataType);\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\n if (!$xml || !$xml->count()) {\n $handler->deleteAddOnData($addOn->addon_id);\n continue;\n }\n $start = 0;\n $guard = 0;\n while (true) {\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\n if (!$last) { break; }\n if ($last <= $start || ++$guard > 100000) {\n fwrite(STDERR, \"IMPORT_PROGRESS_INVALID\\n\");\n exit(31);\n }\n $start = $last;\n }\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\n \\XF::triggerRunOnce();\n $imported[] = $handler->getContainerTag();\n}\n$addOn->postDataImport();\n$repo = $app->repository(\\XF\\Repository\\StyleRepository::class);\n$repo->updateAllStylesLastModifiedDateLater();\n$addOn->postRebuild();\necho \"PORTAL200_IMPORT_JSON=\" . json_encode(\n ['result'=>'PASS','containers'=>$imported],\n JSON_UNESCAPED_SLASHES\n) . \"\\n\";\n?>","INSPECT_PHP":"db();\nfunction srcMeta(string $path): ?array {\n if (!is_file($path)) { return null; }\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\n}\n$portal = $root . '/src/addons/Newfi/Portal';\n$templates = $db->fetchAll(\n \"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \" .\n \"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \" .\n \"LOCATE('--newfi-portal200', template) > 0 AS css_marker \" .\n \"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\"\n);\n$result = [\n 'hostname' => gethostname(),\n 'application' => [\n 'boardActive' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\"),\n 'boardUrl' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\"),\n 'indexRoute' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\"),\n 'defaultStyleId' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\"),\n 'error_count' => (int)$db->fetchOne(\"SELECT COUNT(*) FROM xf_error_log\"),\n 'error_max_id' => (int)$db->fetchOne(\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\")\n ],\n 'addon' => $db->fetchRow(\n \"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \" .\n \"FROM xf_addon WHERE addon_id='Newfi/Portal'\"\n ),\n 'source' => [\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n ],\n 'templates' => $templates\n];\necho \"PORTAL200_INSPECT_JSON_BEGIN\\n\";\necho json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\necho \"\\nPORTAL200_INSPECT_JSON_END\\n\";\n?>","OLD_HEAD":"06bab5acd124e768f70305b3d7eca2f595871890","RUNTIME_VERIFY_PHP_TEMPLATE":"$state];\nif ($state === 'new') {\n $service = new \\Newfi\\Portal\\Service\\PortalData($app);\n $portal = $service->build();\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\n $result['sections'] = [];\n foreach (['forums','resources','media','events','dogs'] as $key) {\n $section = $portal[$key] ?? null;\n if (!is_array($section)) { fwrite(STDERR, \"SECTION_MISSING\\n\"); exit(60); }\n $result['sections'][$key] = [\n 'available'=>(bool)($section['available'] ?? false),\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\n 'status'=>(string)($section['status'] ?? '')\n ];\n }\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\"1\"') !== false;\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\"resources\"') !== false;\n $result['locked_media'] = strpos($html, 'data-portal-locked=\"media\"') !== false;\n $result['html_bytes'] = strlen($html);\n if (!$result['html_marker']) { fwrite(STDERR, \"HTML_MARKER_MISSING\\n\"); exit(61); }\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\n fwrite(STDERR, \"GUEST_PERMISSION_CONTRACT_CHANGED\\n\"); exit(62);\n }\n if (!$result['locked_resources'] || !$result['locked_media']) {\n fwrite(STDERR, \"LOCKED_COPY_MISSING\\n\"); exit(63);\n }\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\n fwrite(STDERR, \"PUBLIC_SECTION_UNAVAILABLE\\n\"); exit(64);\n }\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\n $result['html_bytes'] = strlen($html);\n if ($result['html_marker'] || !$result['old_copy']) {\n fwrite(STDERR, \"OLD_RENDER_CONTRACT\\n\"); exit(65);\n }\n} else {\n exit(66);\n}\necho \"PORTAL200_RUNTIME_JSON=\" . json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n) . \"\\n\";\n?>","commit_created":false,"failure_rollback_started":false,"git_files_changed":false,"old_git_bytes":{},"prod_backup":false,"prod_mutated":false,"push_confirmed":false,"staging_backup":false,"staging_final_new":false,"staging_mutated":false} +CONST_CID=NEWFI-260907-PORTAL200-APPLY3 +CONST_BRANCH=implementation/newfi-closed-mvp-20260906 +CONST_OLD_HEAD=06bab5acd124e768f70305b3d7eca2f595871890 +CONST_WT=UNPROVEN +CONST_CAND_ROOT=UNPROVEN +RELEVANT_CONSTANTS=[{"lineno":10,"segment":"\"NEWFI-260907-PORTAL200-APPLY3\"","value":"NEWFI-260907-PORTAL200-APPLY3"},{"lineno":147,"segment":"r'''db();\nfunction srcMeta(string $path): ?array {\n if (!is_file($path)) { return null; }\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\n}\n$portal = $root . '/src/addons/Newfi/Portal';\n$templates = $db->fetchAll(\n \"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \" .\n \"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \" .\n \"LOCATE('--newfi-portal200', template) > 0 AS css_marker \" .\n \"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\"\n);\n$result = [\n 'hostname' => gethostname(),\n 'application' => [\n 'boardActive' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\"),\n 'boardUrl' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\"),\n 'indexRoute' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\"),\n 'defaultStyleId' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\"),\n 'error_count' => (int)$db->fetchOne(\"SELECT COUNT(*) FROM xf_error_log\"),\n 'error_max_id' => (int)$db->fetchOne(\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\")\n ],\n 'addon' => $db->fetchRow(\n \"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \" .\n \"FROM xf_addon WHERE addon_id='Newfi/Portal'\"\n ),\n 'source' => [\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n ],\n 'templates' => $templates\n];\necho \"PORTAL200_INSPECT_JSON_BEGIN\\n\";\necho json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\necho \"\\nPORTAL200_INSPECT_JSO","value":"db();\nfunction srcMeta(string $path): ?array {\n if (!is_file($path)) { return null; }\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\n}\n$portal = $root . '/src/addons/Newfi/Portal';\n$templates = $db->fetchAll(\n \"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \" .\n \"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \" .\n \"LOCATE('--newfi-portal200', template) > 0 AS css_marker \" .\n \"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\"\n);\n$result = [\n 'hostname' => gethostname(),\n 'application' => [\n 'boardActive' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\"),\n 'boardUrl' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\"),\n 'indexRoute' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\"),\n 'defaultStyleId' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\"),\n 'error_count' => (int)$db->fetchOne(\"SELECT COUNT(*) FROM xf_error_log\"),\n 'error_max_id' => (int)$db->fetchOne(\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\")\n ],\n 'addon' => $db->fetchRow(\n \"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \" .\n \"FROM xf_addon WHERE addon_id='Newfi/Portal'\"\n ),\n 'source' => [\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n ],\n 'templates' => $templates\n];\necho \"PORTAL200_INSPECT_JSON_BEGIN\\n\";\necho json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\necho \"\\nPORTAL200_INSPECT_JSON_END\\n\";\n?>"},{"lineno":194,"segment":"r'''addOnManager()->getById('Newfi/Portal');\nif (!$addOn || !$addOn->isInstalled()) {\n fwrite(STDERR, \"ADDON_NOT_INSTALLED\\n\");\n exit(30);\n}\n$addOn->preRebuild();\n$dataManager = $app->addOnDataManager();\n$imported = [];\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\n $handler = $dataManager->getDataTypeHandler($dataType);\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\n if (!$xml || !$xml->count()) {\n $handler->deleteAddOnData($addOn->addon_id);\n continue;\n }\n $start = 0;\n $guard = 0;\n while (true) {\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\n if (!$last) { break; }\n if ($last <= $start || ++$guard > 100000) {\n fwrite(STDERR, \"IMPORT_PROGRESS_INVALID\\n\");\n exit(31);\n }\n $start = $last;\n }\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\n \\XF::triggerRunOnce();\n $imported[] = $handler->getContainerTag();\n}\n$addOn->postDataImport();\n$repo = $app->repository(\\XF\\Repository\\StyleRepository::class);\n$repo->updateAllStylesLastModifiedDateLater();\n$addOn->postRebuild();\necho \"PORTAL200_IMPORT_JSON=\" . json_encode(\n ['result'=>'PASS','containers'=>$imported],\n JSON_UNESCAPED_SLASHES\n) . \"\\n\";\n?>'''","value":"addOnManager()->getById('Newfi/Portal');\nif (!$addOn || !$addOn->isInstalled()) {\n fwrite(STDERR, \"ADDON_NOT_INSTALLED\\n\");\n exit(30);\n}\n$addOn->preRebuild();\n$dataManager = $app->addOnDataManager();\n$imported = [];\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\n $handler = $dataManager->getDataTypeHandler($dataType);\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\n if (!$xml || !$xml->count()) {\n $handler->deleteAddOnData($addOn->addon_id);\n continue;\n }\n $start = 0;\n $guard = 0;\n while (true) {\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\n if (!$last) { break; }\n if ($last <= $start || ++$guard > 100000) {\n fwrite(STDERR, \"IMPORT_PROGRESS_INVALID\\n\");\n exit(31);\n }\n $start = $last;\n }\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\n \\XF::triggerRunOnce();\n $imported[] = $handler->getContainerTag();\n}\n$addOn->postDataImport();\n$repo = $app->repository(\\XF\\Repository\\StyleRepository::class);\n$repo->updateAllStylesLastModifiedDateLater();\n$addOn->postRebuild();\necho \"PORTAL200_IMPORT_JSON=\" . json_encode(\n ['result'=>'PASS','containers'=>$imported],\n JSON_UNESCAPED_SLASHES\n) . \"\\n\";\n?>"},{"lineno":258,"segment":"r'''$state];\nif ($state === 'new') {\n $service = new \\Newfi\\Portal\\Service\\PortalData($app);\n $portal = $service->build();\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\n $result['sections'] = [];\n foreach (['forums','resources','media','events','dogs'] as $key) {\n $section = $portal[$key] ?? null;\n if (!is_array($section)) { fwrite(STDERR, \"SECTION_MISSING\\n\"); exit(60); }\n $result['sections'][$key] = [\n 'available'=>(bool)($section['available'] ?? false),\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\n 'status'=>(string)($section['status'] ?? '')\n ];\n }\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\"1\"') !== false;\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\"resources\"') !== false;\n $result['locked_media'] = strpos($html, 'data-portal-locked=\"media\"') !== false;\n $result['html_bytes'] = strlen($html);\n if (!$result['html_marker']) { fwrite(STDERR, \"HTML_MARKER_MISSING\\n\"); exit(61); }\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\n fwrite(STDERR, \"GUEST_PERMISSION_CONTRACT_CHANGED\\n\"); exit(62);\n }\n if (!$result['locked_resources'] || !$result['locked_media']) {\n fwrite(STDERR, \"LOCKED_COPY_MISSING\\n\"); exit(63);\n }\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\n fwrite(STDERR, \"PUBLIC_SECTION_UNAVAILABLE\\n\"); exit(64);\n }\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['o","value":"$state];\nif ($state === 'new') {\n $service = new \\Newfi\\Portal\\Service\\PortalData($app);\n $portal = $service->build();\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\n $result['sections'] = [];\n foreach (['forums','resources','media','events','dogs'] as $key) {\n $section = $portal[$key] ?? null;\n if (!is_array($section)) { fwrite(STDERR, \"SECTION_MISSING\\n\"); exit(60); }\n $result['sections'][$key] = [\n 'available'=>(bool)($section['available'] ?? false),\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\n 'status'=>(string)($section['status'] ?? '')\n ];\n }\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\"1\"') !== false;\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\"resources\"') !== false;\n $result['locked_media'] = strpos($html, 'data-portal-locked=\"media\"') !== false;\n $result['html_bytes'] = strlen($html);\n if (!$result['html_marker']) { fwrite(STDERR, \"HTML_MARKER_MISSING\\n\"); exit(61); }\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\n fwrite(STDERR, \"GUEST_PERMISSION_CONTRACT_CHANGED\\n\"); exit(62);\n }\n if (!$result['locked_resources'] || !$result['locked_media']) {\n fwrite(STDERR, \"LOCKED_COPY_MISSING\\n\"); exit(63);\n }\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\n fwrite(STDERR, \"PUBLIC_SECTION_UNAVAILABLE\\n\"); exit(64);\n }\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\n $result['html_bytes'] = strlen($html);\n if ($result['html_marker'] || !$result['old_copy']) {\n fwrite(STDERR, \"OLD_RENDER_CONTRACT\\n\"); exit(65);\n }\n} else {\n exit(66);\n}\necho \"PORTAL200_RUNTIME_JSON=\" . json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n) . \"\\n\";\n?>"},{"lineno":311,"segment":"r'''import html,json,re,subprocess,sys,urllib.parse\nmode=sys.argv[1]\nstate=sys.argv[2]\nif mode=='staging':\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\nelif mode=='prod':\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\nelse:\n raise SystemExit(40)\ndef fetch(url):\n cmd=[\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\n ]\n if mode=='staging':\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\n cmd += [\n '-o','-','-w','\\n__PORTAL_STATUS__%{http_code}',url\n ]\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\n if p.returncode!=0: raise SystemExit(41)\n text=p.stdout.decode('utf-8','replace')\n marker='\\n__PORTAL_STATUS__'\n if marker not in text: raise SystemExit(42)\n body,status=text.rsplit(marker,1)\n return int(status.strip()),body\nroot=f'{scheme}://{host}/'\nstatus,body=fetch(root)\nhrefs=[]\nfor raw in re.findall(r\"href=[\\\"']([^\\\"']*css\\.php[^\\\"']*)[\\\"']\",body,re.I):\n raw=html.unescape(raw)\n hrefs.append(urllib.parse.urljoin(root,raw))\nnormal_css=[]\nnormal_status=[]\nfor u in hrefs:\n s,b=fetch(u)\n normal_status.append(s)\n normal_css.append(b)\nnormal_css_text='\\n'.join(normal_css)\nforced_css_marker=None\nforced_css_status=None\nif mode=='staging' and state=='new' and hrefs:\n u=urllib.parse.urlsplit(hrefs[0])\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\n out=[]\n found=False\n for k,v in pairs:\n if k=='css':\n found=True\n parts=[x for x in v.split(',') if x]\n if 'public:newfi_portal.less' not in parts:\n parts.append('public:newfi_portal.less')\n v=','.join(parts)\n out.append((k,v))\n if found:\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\n forced_css_status,forced_body=fetch(forced)\n forced_css_marker='--newfi-portal200' in ","value":"import html,json,re,subprocess,sys,urllib.parse\nmode=sys.argv[1]\nstate=sys.argv[2]\nif mode=='staging':\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\nelif mode=='prod':\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\nelse:\n raise SystemExit(40)\ndef fetch(url):\n cmd=[\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\n ]\n if mode=='staging':\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\n cmd += [\n '-o','-','-w','\\n__PORTAL_STATUS__%{http_code}',url\n ]\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\n if p.returncode!=0: raise SystemExit(41)\n text=p.stdout.decode('utf-8','replace')\n marker='\\n__PORTAL_STATUS__'\n if marker not in text: raise SystemExit(42)\n body,status=text.rsplit(marker,1)\n return int(status.strip()),body\nroot=f'{scheme}://{host}/'\nstatus,body=fetch(root)\nhrefs=[]\nfor raw in re.findall(r\"href=[\\\"']([^\\\"']*css\\.php[^\\\"']*)[\\\"']\",body,re.I):\n raw=html.unescape(raw)\n hrefs.append(urllib.parse.urljoin(root,raw))\nnormal_css=[]\nnormal_status=[]\nfor u in hrefs:\n s,b=fetch(u)\n normal_status.append(s)\n normal_css.append(b)\nnormal_css_text='\\n'.join(normal_css)\nforced_css_marker=None\nforced_css_status=None\nif mode=='staging' and state=='new' and hrefs:\n u=urllib.parse.urlsplit(hrefs[0])\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\n out=[]\n found=False\n for k,v in pairs:\n if k=='css':\n found=True\n parts=[x for x in v.split(',') if x]\n if 'public:newfi_portal.less' not in parts:\n parts.append('public:newfi_portal.less')\n v=','.join(parts)\n out.append((k,v))\n if found:\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\n forced_css_status,forced_body=fetch(forced)\n forced_css_marker='--newfi-portal200' in forced_body\nresult={\n 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\n}\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\nif status!=expected_status: raise SystemExit(45)\nif mode=='prod':\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\nelif mode=='staging':\n if any(x!=200 for x in normal_status): raise SystemExit(46)\nelse:\n raise SystemExit(50)\nif mode=='prod' and state=='new':\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\nelif mode=='prod' and state=='old':\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\nelif mode=='staging' and state=='new':\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\nelif mode=='staging' and state=='old':\n pass\nelse:\n raise SystemExit(50)\n"},{"lineno":14,"segment":"\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\"","value":"/root/newfi-portal200-candidate/src/addons/Newfi/Portal"},{"lineno":20,"segment":"\"_data/templates.xml\"","value":"_data/templates.xml"},{"lineno":24,"segment":"\"_data/templates.xml\"","value":"_data/templates.xml"},{"lineno":27,"segment":"/root/newfi-portal200-backup-","value":"/root/newfi-portal200-backup-"},{"lineno":401,"segment":"\"PORTAL200_INSPECT_JSON_BEGIN\\n\"","value":"PORTAL200_INSPECT_JSON_BEGIN\n"},{"lineno":402,"segment":"\"\\nPORTAL200_INSPECT_JSON_END\"","value":"\nPORTAL200_INSPECT_JSON_END"},{"lineno":734,"segment":"\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\"","value":"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE"},{"lineno":736,"segment":"\"PORTAL200_FLOW\"","value":"PORTAL200_FLOW"},{"lineno":737,"segment":"\"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\"","value":"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production"},{"lineno":497,"segment":"\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest ! -e \"$B\"\ninstall -d -m 700 \"$B/Pub/Controller\" \"$B/_data\"\ncp -p -- \"$P/Pub/Controller/Portal.php\" \"$B/Pub/Controller/Portal.php\"\ncp -p -- \"$P/_data/templates.xml\" \"$B/_data/templates.xml\"\ntest ! -e \"$P/Service/PortalData.php\"\nprintf 'PORTAL200_BACKUP=PASS\\n'\n","value":"\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest ! -e \"$B\"\ninstall -d -m 700 \"$B/Pub/Controller\" \"$B/_data\"\ncp -p -- \"$P/Pub/Controller/Portal.php\" \"$B/Pub/Controller/Portal.php\"\ncp -p -- \"$P/_data/templates.xml\" \"$B/_data/templates.xml\"\ntest ! -e \"$P/Service/PortalData.php\"\nprintf 'PORTAL200_BACKUP=PASS\n'\n"},{"lineno":507,"segment":"\"PORTAL200_BACKUP=PASS\"","value":"PORTAL200_BACKUP=PASS"},{"lineno":523,"segment":"\"; printf 'PORTAL200_WRITE=PASS\\\\n'\"","value":"; printf 'PORTAL200_WRITE=PASS\\n'"},{"lineno":531,"segment":"\"PORTAL200_WRITE=PASS\"","value":"PORTAL200_WRITE=PASS"},{"lineno":539,"segment":"\"_data/templates.xml\"","value":"_data/templates.xml"},{"lineno":546,"segment":"\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest -f \"$B/Pub/Controller/Portal.php\"\ntest -f \"$B/_data/templates.xml\"\ncp -p -- \"$B/Pub/Controller/Portal.php\" \"$P/Pub/Controller/Portal.php\"\ncp -p -- \"$B/_data/templates.xml\" \"$P/_data/templates.xml\"\nrm -f -- \"$P/Service/PortalData.php\"\nrmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\n","value":"\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest -f \"$B/Pub/Controller/Portal.php\"\ntest -f \"$B/_data/templates.xml\"\ncp -p -- \"$B/Pub/Controller/Portal.php\" \"$P/Pub/Controller/Portal.php\"\ncp -p -- \"$B/_data/templates.xml\" \"$P/_data/templates.xml\"\nrm -f -- \"$P/Service/PortalData.php\"\nrmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\n'\n"},{"lineno":557,"segment":"\"PORTAL200_SOURCE_RESTORE=PASS\"","value":"PORTAL200_SOURCE_RESTORE=PASS"},{"lineno":568,"segment":"\"PORTAL200_IMPORT_JSON=\"","value":"PORTAL200_IMPORT_JSON="},{"lineno":585,"segment":"\"PORTAL200_RUNTIME\"","value":"PORTAL200_RUNTIME"},{"lineno":647,"segment":"\"PORTAL200_HTTP\"","value":"PORTAL200_HTTP"},{"lineno":715,"segment":"\"PORTAL200_CLUSTER\"","value":"PORTAL200_CLUSTER"},{"lineno":758,"segment":"\"\"","value":""},{"lineno":781,"segment":"\"PORTAL200_CANDIDATE_IDENTITY\"","value":"PORTAL200_CANDIDATE_IDENTITY"},{"lineno":783,"segment":"\"PORTAL200_ORCHESTRATION_FIXES\"","value":"PORTAL200_ORCHESTRATION_FIXES"},{"lineno":800,"segment":"\"PORTAL200_GIT_PRESTATE\"","value":"PORTAL200_GIT_PRESTATE"},{"lineno":811,"segment":"\"PORTAL200_RUNTIME_HELPER_LINT\"","value":"PORTAL200_RUNTIME_HELPER_LINT"},{"lineno":825,"segment":"\"PORTAL200_ERROR_BASELINE\"","value":"PORTAL200_ERROR_BASELINE"},{"lineno":831,"segment":"\"PORTAL200_PREFLIGHT\"","value":"PORTAL200_PREFLIGHT"},{"lineno":839,"segment":"\"PORTAL200_STAGING_FIRST_APPLY\"","value":"PORTAL200_STAGING_FIRST_APPLY"},{"lineno":845,"segment":"\"PORTAL200_STAGING_ROLLBACK_PROOF\"","value":"PORTAL200_STAGING_ROLLBACK_PROOF"},{"lineno":852,"segment":"\"PORTAL200_STAGING_FINAL\"","value":"PORTAL200_STAGING_FINAL"},{"lineno":885,"segment":"\"PORTAL200_GIT_PROMOTION\"","value":"PORTAL200_GIT_PROMOTION"},{"lineno":905,"segment":"\"PORTAL200_PRODUCTION\"","value":"PORTAL200_PRODUCTION"},{"lineno":922,"segment":"\"PORTAL200_FINAL_EVIDENCE\"","value":"PORTAL200_FINAL_EVIDENCE"},{"lineno":931,"segment":"\"PORTAL200_PORTAL\"","value":"PORTAL200_PORTAL"},{"lineno":932,"segment":"\"PORTAL200_STAGING\"","value":"PORTAL200_STAGING"},{"lineno":933,"segment":"\"PORTAL200_PRODUCTION\"","value":"PORTAL200_PRODUCTION"},{"lineno":934,"segment":"\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\"","value":"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS"},{"lineno":460,"segment":"1000010","value":1000010},{"lineno":484,"segment":"\"PORTAL200_NEW_XF_ERRORS\"","value":"PORTAL200_NEW_XF_ERRORS"},{"lineno":763,"segment":"\"_data/templates.xml\"","value":"_data/templates.xml"},{"lineno":766,"segment":"\"CANDIDATE_TEMPLATE_SET\"","value":"CANDIDATE_TEMPLATE_SET"},{"lineno":780,"segment":"\"CANDIDATE_MUTATION_SCAN\"","value":"CANDIDATE_MUTATION_SCAN"},{"lineno":941,"segment":"\"PORTAL200_FAILURE\"","value":"PORTAL200_FAILURE"},{"lineno":1004,"segment":"\"PORTAL200_ROADMAP_REMAINS\"","value":"PORTAL200_ROADMAP_REMAINS"},{"lineno":1009,"segment":"\"PORTAL200_FAILURE\"","value":"PORTAL200_FAILURE"},{"lineno":1016,"segment":"\"PORTAL200_ROADMAP_REMAINS\"","value":"PORTAL200_ROADMAP_REMAINS"},{"lineno":461,"segment":"\"version_string\"","value":"version_string"},{"lineno":618,"segment":"\"PORTAL200_HTTP_EXEC\"","value":"PORTAL200_HTTP_EXEC"},{"lineno":762,"segment":"\"CANDIDATE_IDENTITY\"","value":"CANDIDATE_IDENTITY"},{"lineno":460,"segment":"\"version_id\"","value":"version_id"},{"lineno":576,"segment":"\"PORTAL200_RUNTIME_JSON=\"","value":"PORTAL200_RUNTIME_JSON="},{"lineno":613,"segment":"\"PORTAL200_HTTP\"","value":"PORTAL200_HTTP"},{"lineno":638,"segment":"\"PORTAL200_HTTP_JSON=\"","value":"PORTAL200_HTTP_JSON="},{"lineno":991,"segment":"\"PORTAL200_FAILURE_GIT_HISTORY\"","value":"PORTAL200_FAILURE_GIT_HISTORY"},{"lineno":606,"segment":"\"PORTAL200_HTTP_JSON=\"","value":"PORTAL200_HTTP_JSON="},{"lineno":952,"segment":"\"PORTAL200_FAILURE_PROD_ROLLBACK\"","value":"PORTAL200_FAILURE_PROD_ROLLBACK"},{"lineno":966,"segment":"\"PORTAL200_FAILURE_STAGING_ROLLBACK\"","value":"PORTAL200_FAILURE_STAGING_ROLLBACK"},{"lineno":985,"segment":"\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\"","value":"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE"},{"lineno":955,"segment":"\"PORTAL200_FAILURE_PROD_ROLLBACK\"","value":"PORTAL200_FAILURE_PROD_ROLLBACK"},{"lineno":969,"segment":"\"PORTAL200_FAILURE_STAGING_ROLLBACK\"","value":"PORTAL200_FAILURE_STAGING_ROLLBACK"},{"lineno":987,"segment":"\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\"","value":"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE"},{"lineno":519,"segment":"\"/.portal200.XXXXXX\"","value":"/.portal200.XXXXXX"}] +RELEVANT_CALLS=[{"args":["/root/newfi-portal200-candidate/src/addons/Newfi/Portal"],"kwargs":{},"lineno":14,"name":"pathlib.Path","source":"pathlib.Path(\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\")"},{"args":["src/addons/Newfi/Portal"],"kwargs":{},"lineno":15,"name":"pathlib.Path","source":"pathlib.Path(\"src/addons/Newfi/Portal\")"},{"args":["PORTAL200_FLOW","candidate->staging-apply->rollback-proof->staging-reapply->git-push->production"],"kwargs":{},"lineno":735,"name":"emit","source":"emit(\n \"PORTAL200_FLOW\",\n \"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\",\n)"},{"args":["[\"/usr/bin/git\", \"-C\", str(WT), *args]"],"kwargs":{"ok":"ok","timeout":"timeout"},"lineno":77,"name":"run","source":"run([\"/usr/bin/git\", \"-C\", str(WT), *args], timeout=timeout, ok=ok)"},{"args":["cmd"],"kwargs":{"input_bytes":"input_bytes","timeout":"timeout + 30"},"lineno":135,"name":"run","source":"run(cmd, input_bytes=input_bytes, timeout=timeout + 30)"},{"args":["__STATE__","state"],"kwargs":{},"lineno":573,"name":"RUNTIME_VERIFY_PHP_TEMPLATE.replace","source":"RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", state)"},{"args":["target","new"],"kwargs":{},"lineno":659,"name":"runtime_verify","source":"runtime_verify(target, \"new\")"},{"args":[["/usr/sbin/qm","status","210"]],"kwargs":{"timeout":30},"lineno":693,"name":"run","source":"run([\"/usr/sbin/qm\", \"status\", \"210\"], timeout=30)"},{"args":[["/usr/bin/ssh","-T","-o","BatchMode=yes","-o","StrictHostKeyChecking=yes","-o","UpdateHostKeys=no","-o","ConnectTimeout=10","root@pve03","/usr/sbin/qm status 211"]],"kwargs":{"timeout":40},"lineno":696,"name":"run","source":"run(\n [\n \"/usr/bin/ssh\",\n \"-T\",\n \"-o\",\n \"BatchMode=yes\",\n \"-o\",\n \"StrictHostKeyChecking=yes\",\n \"-o\",\n \"UpdateHostKeys=no\",\n \"-o\",\n \"ConnectTimeout=10\",\n \"root@pve03\",\n \"/usr/sbin/qm status 211\",\n ],\n timeout=40,\n )"},{"args":["CAND_ROOT / \"_data/templates.xml\""],"kwargs":{},"lineno":763,"name":"ET.parse","source":"ET.parse(CAND_ROOT / \"_data/templates.xml\")"},{"args":["utf-8"],"kwargs":{},"lineno":767,"name":"read_text","source":"(CAND_ROOT / \"Service/PortalData.php\").read_text(\"utf-8\")"},{"args":["PORTAL200_CANDIDATE_IDENTITY","PASS"],"kwargs":{},"lineno":781,"name":"emit","source":"emit(\"PORTAL200_CANDIDATE_IDENTITY\", \"PASS\")"},{"args":["prod","old"],"kwargs":{},"lineno":827,"name":"runtime_verify","source":"runtime_verify(\"prod\", \"old\")"},{"args":["staging"],"kwargs":{},"lineno":836,"name":"install_candidate","source":"install_candidate(\"staging\")"},{"args":["staging"],"kwargs":{},"lineno":848,"name":"install_candidate","source":"install_candidate(\"staging\")"},{"args":["prod"],"kwargs":{},"lineno":902,"name":"install_candidate","source":"install_candidate(\"prod\")"},{"args":["cmd"],"kwargs":{"check":false,"input":"input_bytes","stderr":"subprocess.PIPE","stdout":"subprocess.PIPE","timeout":"timeout"},"lineno":61,"name":"subprocess.run","source":"subprocess.run(\n cmd,\n input=input_bytes,\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n timeout=timeout,\n check=False,\n )"},{"args":["target","rel","(CAND_ROOT / rel).read_bytes()"],"kwargs":{},"lineno":541,"name":"guest_write","source":"guest_write(target, rel, (CAND_ROOT / rel).read_bytes())"},{"args":["target","old"],"kwargs":{},"lineno":674,"name":"runtime_verify","source":"runtime_verify(target, \"old\")"},{"args":["CANDIDATE_TEMPLATE_SET"],"kwargs":{},"lineno":766,"name":"fail","source":"fail(\"CANDIDATE_TEMPLATE_SET\")"},{"args":["CANDIDATE_MUTATION_SCAN"],"kwargs":{},"lineno":780,"name":"fail","source":"fail(\"CANDIDATE_MUTATION_SCAN\")"},{"args":["__STATE__","new"],"kwargs":{},"lineno":807,"name":"RUNTIME_VERIFY_PHP_TEMPLATE.replace","source":"RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\")"},{"args":["__STATE__","new"],"kwargs":{},"lineno":810,"name":"RUNTIME_VERIFY_PHP_TEMPLATE.replace","source":"RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\")"},{"args":["CAND_ROOT / rel","dest"],"kwargs":{},"lineno":858,"name":"shutil.copyfile","source":"shutil.copyfile(CAND_ROOT / rel, dest)"},{"args":["addon.get(\"version_id\", 0)"],"kwargs":{},"lineno":460,"name":"int","source":"int(addon.get(\"version_id\", 0))"},{"args":["version_string"],"kwargs":{},"lineno":461,"name":"addon.get","source":"addon.get(\"version_string\")"},{"args":[],"kwargs":{},"lineno":541,"name":"read_bytes","source":"(CAND_ROOT / rel).read_bytes()"},{"args":["argv"],"kwargs":{"check":false,"input":"HTTP_VERIFY_PY.encode()","stderr":"subprocess.PIPE","stdout":"subprocess.PIPE","timeout":180},"lineno":593,"name":"subprocess.run","source":"subprocess.run(\n argv,\n input=HTTP_VERIFY_PY.encode(),\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n timeout=180,\n check=False,\n )"},{"args":["CANDIDATE_IDENTITY"],"kwargs":{},"lineno":762,"name":"fail","source":"fail(\"CANDIDATE_IDENTITY\")"},{"args":["version_id",0],"kwargs":{},"lineno":460,"name":"addon.get","source":"addon.get(\"version_id\", 0)"},{"args":["old"],"kwargs":{},"lineno":979,"name":"dest.write_bytes","source":"dest.write_bytes(old)"}] +RELEVANT_SOURCE_CONTEXTS=[{"line":10,"text":"import subprocess\nimport sys\nimport xml.etree.ElementTree as ET\n\nCID = \"NEWFI-260907-PORTAL200-APPLY3\"\nBRANCH = \"implementation/newfi-closed-mvp-20260906\"\nOLD_HEAD = \"06bab5acd124e768f70305b3d7eca2f595871890\"\nWT = pathlib.Path(\"/root/newfi-portal/.worktrees/implementation-newfi-closed-mvp-20260906\")\nCAND_ROOT = pathlib.Path(\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\")\nPORTAL_REL = pathlib.Path(\"src/addons/Newfi/Portal\")\nGIT_PORTAL = WT / PORTAL_REL\nEXPECTED_NEW = {\n \"Pub/Controller/Portal.php\": (363, \"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\"),"},{"line":14,"text":"CID = \"NEWFI-260907-PORTAL200-APPLY3\"\nBRANCH = \"implementation/newfi-closed-mvp-20260906\"\nOLD_HEAD = \"06bab5acd124e768f70305b3d7eca2f595871890\"\nWT = pathlib.Path(\"/root/newfi-portal/.worktrees/implementation-newfi-closed-mvp-20260906\")\nCAND_ROOT = pathlib.Path(\"/root/newfi-portal200-candidate/src/addons/Newfi/Portal\")\nPORTAL_REL = pathlib.Path(\"src/addons/Newfi/Portal\")\nGIT_PORTAL = WT / PORTAL_REL\nEXPECTED_NEW = {\n \"Pub/Controller/Portal.php\": (363, \"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\"),\n \"Service/PortalData.php\": (5213, \"4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d\"),\n \"_data/templates.xml\": (10977, \"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\"),\n}\nEXPECTED_OLD = {"},{"line":20,"text":"GIT_PORTAL = WT / PORTAL_REL\nEXPECTED_NEW = {\n \"Pub/Controller/Portal.php\": (363, \"619d489f7cc3b461479c702fa8825097ae22d3c6cfeb911e9f14ea1de9c36d6d\"),\n \"Service/PortalData.php\": (5213, \"4da03f26294ab6d163a1a746dee4bb6a82d7d4f39e66b24246bc0eb4c455dd1d\"),\n \"_data/templates.xml\": (10977, \"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\"),\n}\nEXPECTED_OLD = {\n \"Pub/Controller/Portal.php\": (247, \"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\"),\n \"_data/templates.xml\": (1461, \"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\"),\n}\nEXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)]\nBACKUP = f\"/root/newfi-portal200-backup-{CID}\"\n"},{"line":24,"text":" \"_data/templates.xml\": (10977, \"1650c3d868d4bc47703f1b75f1f179cd25309b2d3b20a2b848e81a4423c00861\"),\n}\nEXPECTED_OLD = {\n \"Pub/Controller/Portal.php\": (247, \"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\"),\n \"_data/templates.xml\": (1461, \"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\"),\n}\nEXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)]\nBACKUP = f\"/root/newfi-portal200-backup-{CID}\"\n\n\nclass PortalError(RuntimeError):\n pass\n"},{"line":27,"text":" \"Pub/Controller/Portal.php\": (247, \"53909e753ea5a011550fd280ad1c342a74ca9949be092dfbd95244243cae92f3\"),\n \"_data/templates.xml\": (1461, \"c29c04f8b937284e821c87957653e87c2f6ac014fac30648a0f6b18e84392adf\"),\n}\nEXPECTED_GIT_PATHS = [str(PORTAL_REL / p) for p in sorted(EXPECTED_NEW)]\nBACKUP = f\"/root/newfi-portal200-backup-{CID}\"\n\n\nclass PortalError(RuntimeError):\n pass\n\n\ndef emit(key, value=None):\n if value is None:"},{"line":160,"text":"}\n$portal = $root . '/src/addons/Newfi/Portal';\n$templates = $db->fetchAll(\n \"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \" .\n \"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \" .\n \"LOCATE('--newfi-portal200', template) > 0 AS css_marker \" .\n \"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\"\n);\n$result = [\n 'hostname' => gethostname(),\n 'application' => [\n 'boardActive' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\"),\n 'boardUrl' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\"),"},{"line":161,"text":"$portal = $root . '/src/addons/Newfi/Portal';\n$templates = $db->fetchAll(\n \"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \" .\n \"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \" .\n \"LOCATE('--newfi-portal200', template) > 0 AS css_marker \" .\n \"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\"\n);\n$result = [\n 'hostname' => gethostname(),\n 'application' => [\n 'boardActive' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\"),\n 'boardUrl' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\"),\n 'indexRoute' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\"),"},{"line":175,"text":" 'error_count' => (int)$db->fetchOne(\"SELECT COUNT(*) FROM xf_error_log\"),\n 'error_max_id' => (int)$db->fetchOne(\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\")\n ],\n 'addon' => $db->fetchRow(\n \"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \" .\n \"FROM xf_addon WHERE addon_id='Newfi/Portal'\"\n ),\n 'source' => [\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n ],\n 'templates' => $templates"},{"line":181,"text":" ),\n 'source' => [\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n ],\n 'templates' => $templates\n];\necho \"PORTAL200_INSPECT_JSON_BEGIN\\n\";\necho json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);"},{"line":185,"text":" '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n ],\n 'templates' => $templates\n];\necho \"PORTAL200_INSPECT_JSON_BEGIN\\n\";\necho json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\necho \"\\nPORTAL200_INSPECT_JSON_END\\n\";\n?>'''\n\n"},{"line":190,"text":"echo json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\necho \"\\nPORTAL200_INSPECT_JSON_END\\n\";\n?>'''\n\n\nIMPORT_PHP = r'''postDataImport();\n$repo = $app->repository(\\XF\\Repository\\StyleRepository::class);\n$repo->updateAllStylesLastModifiedDateLater();\n$addOn->postRebuild();\necho \"PORTAL200_IMPORT_JSON=\" . json_encode(\n ['result'=>'PASS','containers'=>$imported],\n JSON_UNESCAPED_SLASHES\n) . \"\\n\";\n?>'''\n\n\nERRORS_PHP_TEMPLATE = r'''(bool)($section['hasItems'] ?? false),\n 'status'=>(string)($section['status'] ?? '')\n ];\n }\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\"1\"') !== false;\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\"resources\"') !== false;\n $result['locked_media'] = strpos($html, 'data-portal-locked=\"media\"') !== false;\n $result['html_bytes'] = strlen($html);\n if (!$result['html_marker']) { fwrite(STDERR, \"HTML_MARKER_MISSING\\n\"); exit(61); }\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\n fwrite(STDERR, \"GUEST_PERMISSION_CONTRACT_CHANGED\\n\"); exit(62);\n }\n if (!$result['locked_resources'] || !$result['locked_media']) {"},{"line":288,"text":" if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\n fwrite(STDERR, \"GUEST_PERMISSION_CONTRACT_CHANGED\\n\"); exit(62);\n }\n if (!$result['locked_resources'] || !$result['locked_media']) {\n fwrite(STDERR, \"LOCKED_COPY_MISSING\\n\"); exit(63);\n }\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\n fwrite(STDERR, \"PUBLIC_SECTION_UNAVAILABLE\\n\"); exit(64);\n }\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;"},{"line":295,"text":" fwrite(STDERR, \"PUBLIC_SECTION_UNAVAILABLE\\n\"); exit(64);\n }\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\n $result['html_bytes'] = strlen($html);\n if ($result['html_marker'] || !$result['old_copy']) {\n fwrite(STDERR, \"OLD_RENDER_CONTRACT\\n\"); exit(65);\n }\n} else {\n exit(66);\n}"},{"line":296,"text":" }\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\n $result['html_bytes'] = strlen($html);\n if ($result['html_marker'] || !$result['old_copy']) {\n fwrite(STDERR, \"OLD_RENDER_CONTRACT\\n\"); exit(65);\n }\n} else {\n exit(66);\n}\necho \"PORTAL200_RUNTIME_JSON=\" . json_encode("},{"line":298,"text":" $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\n $result['html_bytes'] = strlen($html);\n if ($result['html_marker'] || !$result['old_copy']) {\n fwrite(STDERR, \"OLD_RENDER_CONTRACT\\n\"); exit(65);\n }\n} else {\n exit(66);\n}\necho \"PORTAL200_RUNTIME_JSON=\" . json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE"},{"line":304,"text":" }\n} else {\n exit(66);\n}\necho \"PORTAL200_RUNTIME_JSON=\" . json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n) . \"\\n\";\n?>'''\n\n\nHTTP_VERIFY_PY = r'''import html,json,re,subprocess,sys,urllib.parse\nmode=sys.argv[1]"},{"line":367,"text":" out.append((k,v))\n if found:\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\n forced_css_status,forced_body=fetch(forced)\n forced_css_marker='--newfi-portal200' in forced_body\nresult={\n 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,"},{"line":371,"text":" forced_css_marker='--newfi-portal200' in forced_body\nresult={\n 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\n}\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))"},{"line":372,"text":"result={\n 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\n}\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\nif status!=expected_status: raise SystemExit(45)"},{"line":373,"text":" 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\n}\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\nif status!=expected_status: raise SystemExit(45)\nif mode=='prod':"},{"line":379,"text":" 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\n}\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\nif status!=expected_status: raise SystemExit(45)\nif mode=='prod':\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\nelif mode=='staging':\n if any(x!=200 for x in normal_status): raise SystemExit(46)\nelse:\n raise SystemExit(50)\nif mode=='prod' and state=='new':"},{"line":390,"text":" raise SystemExit(50)\nif mode=='prod' and state=='new':\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\nelif mode=='prod' and state=='old':\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\nelif mode=='staging' and state=='new':\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\nelif mode=='staging' and state=='old':\n pass\nelse:\n raise SystemExit(50)\n'''\n"},{"line":401,"text":"'''\n\n\ndef parse_inspect(text):\n b = \"PORTAL200_INSPECT_JSON_BEGIN\\n\"\n e = \"\\nPORTAL200_INSPECT_JSON_END\"\n if b not in text or e not in text:\n fail(\"INSPECT_MARKERS\")\n try:\n return json.loads(text.split(b, 1)[1].split(e, 1)[0])\n except Exception:\n fail(\"INSPECT_JSON\")\n"},{"line":402,"text":"\n\ndef parse_inspect(text):\n b = \"PORTAL200_INSPECT_JSON_BEGIN\\n\"\n e = \"\\nPORTAL200_INSPECT_JSON_END\"\n if b not in text or e not in text:\n fail(\"INSPECT_MARKERS\")\n try:\n return json.loads(text.split(b, 1)[1].split(e, 1)[0])\n except Exception:\n fail(\"INSPECT_JSON\")\n\n"},{"line":460,"text":" and a.get(\"boardUrl\") == expected[1]\n and a.get(\"indexRoute\") == \"portal/\"\n and a.get(\"defaultStyleId\") == \"4\"\n and addon.get(\"addon_id\") == \"Newfi/Portal\"\n and int(addon.get(\"version_id\", 0)) == 1000010\n and addon.get(\"version_string\") == \"1.0.0 Alpha 1\"\n and int(addon.get(\"active\", 0)) == 1\n and int(addon.get(\"is_processing\", 1)) == 0\n )\n\n\ndef new_errors(target, baseline):\n raw = qga("},{"line":461,"text":" and a.get(\"indexRoute\") == \"portal/\"\n and a.get(\"defaultStyleId\") == \"4\"\n and addon.get(\"addon_id\") == \"Newfi/Portal\"\n and int(addon.get(\"version_id\", 0)) == 1000010\n and addon.get(\"version_string\") == \"1.0.0 Alpha 1\"\n and int(addon.get(\"active\", 0)) == 1\n and int(addon.get(\"is_processing\", 1)) == 0\n )\n\n\ndef new_errors(target, baseline):\n raw = qga(\n target,"},{"line":484,"text":"def assert_no_new_errors(target, baseline, phase):\n rows = new_errors(target, baseline)\n if rows:\n emit(\n \"PORTAL200_NEW_XF_ERRORS\",\n {\n \"target\": target,\n \"phase\": phase,\n \"count\": len(rows),\n \"ids\": [r.get(\"error_id\") for r in rows],\n },\n )\n fail(\"NEW_XF_ERRORS\")"},{"line":502,"text":"P=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest ! -e \"$B\"\ninstall -d -m 700 \"$B/Pub/Controller\" \"$B/_data\"\ncp -p -- \"$P/Pub/Controller/Portal.php\" \"$B/Pub/Controller/Portal.php\"\ncp -p -- \"$P/_data/templates.xml\" \"$B/_data/templates.xml\"\ntest ! -e \"$P/Service/PortalData.php\"\nprintf 'PORTAL200_BACKUP=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_BACKUP=PASS\" not in out:\n fail(\"BACKUP_FAILED\")\n\n"},{"line":504,"text":"install -d -m 700 \"$B/Pub/Controller\" \"$B/_data\"\ncp -p -- \"$P/Pub/Controller/Portal.php\" \"$B/Pub/Controller/Portal.php\"\ncp -p -- \"$P/_data/templates.xml\" \"$B/_data/templates.xml\"\ntest ! -e \"$P/Service/PortalData.php\"\nprintf 'PORTAL200_BACKUP=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_BACKUP=PASS\" not in out:\n fail(\"BACKUP_FAILED\")\n\n\ndef guest_write(target, rel, data):\n dest = f\"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}\""},{"line":507,"text":"test ! -e \"$P/Service/PortalData.php\"\nprintf 'PORTAL200_BACKUP=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_BACKUP=PASS\" not in out:\n fail(\"BACKUP_FAILED\")\n\n\ndef guest_write(target, rel, data):\n dest = f\"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}\"\n parent = str(pathlib.PurePosixPath(dest).parent)\n script = (\n \"set -eu; umask 022; \""},{"line":519,"text":" \"set -eu; umask 022; \"\n + \"install -d -m 755 \"\n + shlex.quote(parent)\n + \"; T=$(mktemp \"\n + shlex.quote(parent + \"/.portal200.XXXXXX\")\n + \"); cat >\\\"$T\\\"; chmod 0644 \\\"$T\\\"; chown root:root \\\"$T\\\"; \"\n + \"mv -f -- \\\"$T\\\" \"\n + shlex.quote(dest)\n + \"; printf 'PORTAL200_WRITE=PASS\\\\n'\"\n )\n out = qga(\n target,\n [\"/bin/bash\", \"-lc\", script],"},{"line":523,"text":" + shlex.quote(parent + \"/.portal200.XXXXXX\")\n + \"); cat >\\\"$T\\\"; chmod 0644 \\\"$T\\\"; chown root:root \\\"$T\\\"; \"\n + \"mv -f -- \\\"$T\\\" \"\n + shlex.quote(dest)\n + \"; printf 'PORTAL200_WRITE=PASS\\\\n'\"\n )\n out = qga(\n target,\n [\"/bin/bash\", \"-lc\", script],\n input_bytes=data,\n timeout=90,\n )\n if \"PORTAL200_WRITE=PASS\" not in out:"},{"line":531,"text":" [\"/bin/bash\", \"-lc\", script],\n input_bytes=data,\n timeout=90,\n )\n if \"PORTAL200_WRITE=PASS\" not in out:\n fail(\"WRITE_FAILED\")\n\n\ndef install_candidate(target):\n for rel in [\n \"Pub/Controller/Portal.php\",\n \"Service/PortalData.php\",\n \"_data/templates.xml\","},{"line":532,"text":" input_bytes=data,\n timeout=90,\n )\n if \"PORTAL200_WRITE=PASS\" not in out:\n fail(\"WRITE_FAILED\")\n\n\ndef install_candidate(target):\n for rel in [\n \"Pub/Controller/Portal.php\",\n \"Service/PortalData.php\",\n \"_data/templates.xml\",\n ]:"},{"line":535,"text":" if \"PORTAL200_WRITE=PASS\" not in out:\n fail(\"WRITE_FAILED\")\n\n\ndef install_candidate(target):\n for rel in [\n \"Pub/Controller/Portal.php\",\n \"Service/PortalData.php\",\n \"_data/templates.xml\",\n ]:\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\n\n"},{"line":539,"text":"def install_candidate(target):\n for rel in [\n \"Pub/Controller/Portal.php\",\n \"Service/PortalData.php\",\n \"_data/templates.xml\",\n ]:\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\n\n\ndef restore_old(target):\n script = f'''set -eu\nB={shlex.quote(BACKUP)}\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal"},{"line":541,"text":" \"Pub/Controller/Portal.php\",\n \"Service/PortalData.php\",\n \"_data/templates.xml\",\n ]:\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())\n\n\ndef restore_old(target):\n script = f'''set -eu\nB={shlex.quote(BACKUP)}\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest -f \"$B/Pub/Controller/Portal.php\"\ntest -f \"$B/_data/templates.xml\""},{"line":549,"text":" script = f'''set -eu\nB={shlex.quote(BACKUP)}\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest -f \"$B/Pub/Controller/Portal.php\"\ntest -f \"$B/_data/templates.xml\"\ncp -p -- \"$B/Pub/Controller/Portal.php\" \"$P/Pub/Controller/Portal.php\"\ncp -p -- \"$B/_data/templates.xml\" \"$P/_data/templates.xml\"\nrm -f -- \"$P/Service/PortalData.php\"\nrmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_SOURCE_RESTORE=PASS\" not in out:"},{"line":551,"text":"P=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest -f \"$B/Pub/Controller/Portal.php\"\ntest -f \"$B/_data/templates.xml\"\ncp -p -- \"$B/Pub/Controller/Portal.php\" \"$P/Pub/Controller/Portal.php\"\ncp -p -- \"$B/_data/templates.xml\" \"$P/_data/templates.xml\"\nrm -f -- \"$P/Service/PortalData.php\"\nrmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_SOURCE_RESTORE=PASS\" not in out:\n fail(\"SOURCE_RESTORE_FAILED\")\n"},{"line":554,"text":"cp -p -- \"$B/Pub/Controller/Portal.php\" \"$P/Pub/Controller/Portal.php\"\ncp -p -- \"$B/_data/templates.xml\" \"$P/_data/templates.xml\"\nrm -f -- \"$P/Service/PortalData.php\"\nrmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_SOURCE_RESTORE=PASS\" not in out:\n fail(\"SOURCE_RESTORE_FAILED\")\n\n\ndef import_addon(target):\n out = qga("},{"line":557,"text":"rmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_SOURCE_RESTORE=PASS\" not in out:\n fail(\"SOURCE_RESTORE_FAILED\")\n\n\ndef import_addon(target):\n out = qga(\n target,\n [\"/usr/bin/php8.3\"],\n input_bytes=IMPORT_PHP.encode(),"},{"line":568,"text":" [\"/usr/bin/php8.3\"],\n input_bytes=IMPORT_PHP.encode(),\n timeout=240,\n )\n if \"PORTAL200_IMPORT_JSON=\" not in out:\n fail(\"IMPORT_FAILED\")\n\n\ndef runtime_verify(target, state):\n src = RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", state)\n out = qga(target, [\"/usr/bin/php8.3\"], input_bytes=src.encode(), timeout=120)\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_RUNTIME_JSON=\")),"},{"line":576,"text":"def runtime_verify(target, state):\n src = RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", state)\n out = qga(target, [\"/usr/bin/php8.3\"], input_bytes=src.encode(), timeout=120)\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_RUNTIME_JSON=\")),\n None,\n )\n if not line:\n fail(\"RUNTIME_MARKER_MISSING\")\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n except Exception:\n fail(\"RUNTIME_JSON\")"},{"line":585,"text":" try:\n data = json.loads(line.split(\"=\", 1)[1])\n except Exception:\n fail(\"RUNTIME_JSON\")\n emit(\"PORTAL200_RUNTIME\", {\"target\": target, **data})\n return data\n\n\ndef http_verify(target, state):\n argv = [\"/usr/bin/python3\", \"-\", target, state]\n if target == \"prod\":\n try:\n p = subprocess.run("},{"line":606,"text":" fail(\"HTTP_PROD_TIMEOUT\")\n out = p.stdout.decode(\"utf-8\", \"replace\")\n err = p.stderr.decode(\"utf-8\", \"replace\")\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_HTTP_JSON=\")),\n None,\n )\n data = None\n if line:\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\n except Exception:"},{"line":613,"text":" data = None\n if line:\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\n except Exception:\n fail(\"HTTP_JSON\")\n if p.returncode != 0:\n emit(\n \"PORTAL200_HTTP_EXEC\",\n {\n \"target\": target,\n \"state\": state,"},{"line":618,"text":" except Exception:\n fail(\"HTTP_JSON\")\n if p.returncode != 0:\n emit(\n \"PORTAL200_HTTP_EXEC\",\n {\n \"target\": target,\n \"state\": state,\n \"rc\": p.returncode,\n \"stderr\": err[:800],\n },\n )\n fail(\"HTTP_PROD_RC\")"},{"line":638,"text":" input_bytes=HTTP_VERIFY_PY.encode(),\n timeout=180,\n )\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_HTTP_JSON=\")),\n None,\n )\n if not line:\n fail(\"HTTP_MARKER_MISSING\")\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n except Exception:\n fail(\"HTTP_JSON\")"},{"line":647,"text":" try:\n data = json.loads(line.split(\"=\", 1)[1])\n except Exception:\n fail(\"HTTP_JSON\")\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\n return data\n\n\ndef verify_new(target, baseline):\n data = inspect(target)\n if not app_contract(data, target):\n fail(\"APP_CONTRACT_NEW\")\n if not source_is_new(data):"},{"line":715,"text":" timeout=40,\n )\n if \"status: running\" not in p.stdout.decode():\n fail(\"VM211_NOT_RUNNING\")\n emit(\"PORTAL200_CLUSTER\", \"PASS\")\n\n\ndef result_contract(status, changes, rollback_started, rollback_restored):\n emit(\n \"HOMELAB_RESULT_CONTRACT\",\n {\n \"version\": 1,\n \"command_id\": CID,"},{"line":734,"text":"\n\nemit(\"NEWFI200_BEGIN\")\nemit(\"COMMAND_ID\", CID)\nemit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\", 22)\nemit(\n \"PORTAL200_FLOW\",\n \"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\",\n)\n\nstaging_backup = False\nstaging_mutated = False\nstaging_final_new = False"},{"line":736,"text":"emit(\"NEWFI200_BEGIN\")\nemit(\"COMMAND_ID\", CID)\nemit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\", 22)\nemit(\n \"PORTAL200_FLOW\",\n \"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\",\n)\n\nstaging_backup = False\nstaging_mutated = False\nstaging_final_new = False\nprod_backup = False\nprod_mutated = False"},{"line":737,"text":"emit(\"COMMAND_ID\", CID)\nemit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_BEFORE\", 22)\nemit(\n \"PORTAL200_FLOW\",\n \"candidate->staging-apply->rollback-proof->staging-reapply->git-push->production\",\n)\n\nstaging_backup = False\nstaging_mutated = False\nstaging_final_new = False\nprod_backup = False\nprod_mutated = False\ngit_files_changed = False"},{"line":758,"text":"s_old_templates = None\np_old_templates = None\n\ntry:\n compile(HTTP_VERIFY_PY, \"\", \"exec\")\n for rel, (size, sha) in EXPECTED_NEW.items():\n p = CAND_ROOT / rel\n if not p.is_file() or file_meta(p) != (size, sha):\n fail(\"CANDIDATE_IDENTITY\")\n tree = ET.parse(CAND_ROOT / \"_data/templates.xml\")\n titles = sorted(x.attrib.get(\"title\") for x in tree.getroot().findall(\"template\"))\n if titles != [\"newfi_portal\", \"newfi_portal.less\"]:\n fail(\"CANDIDATE_TEMPLATE_SET\")"},{"line":760,"text":"\ntry:\n compile(HTTP_VERIFY_PY, \"\", \"exec\")\n for rel, (size, sha) in EXPECTED_NEW.items():\n p = CAND_ROOT / rel\n if not p.is_file() or file_meta(p) != (size, sha):\n fail(\"CANDIDATE_IDENTITY\")\n tree = ET.parse(CAND_ROOT / \"_data/templates.xml\")\n titles = sorted(x.attrib.get(\"title\") for x in tree.getroot().findall(\"template\"))\n if titles != [\"newfi_portal\", \"newfi_portal.less\"]:\n fail(\"CANDIDATE_TEMPLATE_SET\")\n service = (CAND_ROOT / \"Service/PortalData.php\").read_text(\"utf-8\")\n if any("},{"line":762,"text":" compile(HTTP_VERIFY_PY, \"\", \"exec\")\n for rel, (size, sha) in EXPECTED_NEW.items():\n p = CAND_ROOT / rel\n if not p.is_file() or file_meta(p) != (size, sha):\n fail(\"CANDIDATE_IDENTITY\")\n tree = ET.parse(CAND_ROOT / \"_data/templates.xml\")\n titles = sorted(x.attrib.get(\"title\") for x in tree.getroot().findall(\"template\"))\n if titles != [\"newfi_portal\", \"newfi_portal.less\"]:\n fail(\"CANDIDATE_TEMPLATE_SET\")\n service = (CAND_ROOT / \"Service/PortalData.php\").read_text(\"utf-8\")\n if any(\n token in service\n for token in ["},{"line":763,"text":" for rel, (size, sha) in EXPECTED_NEW.items():\n p = CAND_ROOT / rel\n if not p.is_file() or file_meta(p) != (size, sha):\n fail(\"CANDIDATE_IDENTITY\")\n tree = ET.parse(CAND_ROOT / \"_data/templates.xml\")\n titles = sorted(x.attrib.get(\"title\") for x in tree.getroot().findall(\"template\"))\n if titles != [\"newfi_portal\", \"newfi_portal.less\"]:\n fail(\"CANDIDATE_TEMPLATE_SET\")\n service = (CAND_ROOT / \"Service/PortalData.php\").read_text(\"utf-8\")\n if any(\n token in service\n for token in [\n \"\\\\XF::db(\","},{"line":766,"text":" fail(\"CANDIDATE_IDENTITY\")\n tree = ET.parse(CAND_ROOT / \"_data/templates.xml\")\n titles = sorted(x.attrib.get(\"title\") for x in tree.getroot().findall(\"template\"))\n if titles != [\"newfi_portal\", \"newfi_portal.less\"]:\n fail(\"CANDIDATE_TEMPLATE_SET\")\n service = (CAND_ROOT / \"Service/PortalData.php\").read_text(\"utf-8\")\n if any(\n token in service\n for token in [\n \"\\\\XF::db(\",\n \"->save(\",\n \"->delete(\",\n \"INSERT \","},{"line":767,"text":" tree = ET.parse(CAND_ROOT / \"_data/templates.xml\")\n titles = sorted(x.attrib.get(\"title\") for x in tree.getroot().findall(\"template\"))\n if titles != [\"newfi_portal\", \"newfi_portal.less\"]:\n fail(\"CANDIDATE_TEMPLATE_SET\")\n service = (CAND_ROOT / \"Service/PortalData.php\").read_text(\"utf-8\")\n if any(\n token in service\n for token in [\n \"\\\\XF::db(\",\n \"->save(\",\n \"->delete(\",\n \"INSERT \",\n \"UPDATE \","},{"line":780,"text":" \"DELETE \",\n \"REPLACE \",\n ]\n ):\n fail(\"CANDIDATE_MUTATION_SCAN\")\n emit(\"PORTAL200_CANDIDATE_IDENTITY\", \"PASS\")\n emit(\n \"PORTAL200_ORCHESTRATION_FIXES\",\n {\n \"staging_503_without_css_links\": True,\n \"failure_status\": \"FAIL\",\n \"prod_http_via_pve01_public_route\": True,\n \"css_href_regex_fixed\": True,"},{"line":781,"text":" \"REPLACE \",\n ]\n ):\n fail(\"CANDIDATE_MUTATION_SCAN\")\n emit(\"PORTAL200_CANDIDATE_IDENTITY\", \"PASS\")\n emit(\n \"PORTAL200_ORCHESTRATION_FIXES\",\n {\n \"staging_503_without_css_links\": True,\n \"failure_status\": \"FAIL\",\n \"prod_http_via_pve01_public_route\": True,\n \"css_href_regex_fixed\": True,\n },"},{"line":783,"text":" ):\n fail(\"CANDIDATE_MUTATION_SCAN\")\n emit(\"PORTAL200_CANDIDATE_IDENTITY\", \"PASS\")\n emit(\n \"PORTAL200_ORCHESTRATION_FIXES\",\n {\n \"staging_503_without_css_links\": True,\n \"failure_status\": \"FAIL\",\n \"prod_http_via_pve01_public_route\": True,\n \"css_href_regex_fixed\": True,\n },\n )\n"},{"line":800,"text":" remote = remote_out.split()[0] if remote_out else \"\"\n if branch != BRANCH or head != OLD_HEAD or remote != OLD_HEAD or status:\n fail(\"GIT_PRESTATE\")\n emit(\n \"PORTAL200_GIT_PRESTATE\",\n {\"branch\": branch, \"head\": head, \"remote\": remote, \"clean\": True},\n )\n cluster_guard()\n\n lint_php(\"staging\", INSPECT_PHP, \"INSPECT_STAGING\")\n lint_php(\"staging\", IMPORT_PHP, \"IMPORT_STAGING\")\n lint_php(\"staging\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\"), \"RUNTIME_STAGING\")\n lint_php(\"prod\", INSPECT_PHP, \"INSPECT_PROD\")"},{"line":811,"text":" lint_php(\"staging\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\"), \"RUNTIME_STAGING\")\n lint_php(\"prod\", INSPECT_PHP, \"INSPECT_PROD\")\n lint_php(\"prod\", IMPORT_PHP, \"IMPORT_PROD\")\n lint_php(\"prod\", RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", \"new\"), \"RUNTIME_PROD\")\n emit(\"PORTAL200_RUNTIME_HELPER_LINT\", \"PASS\")\n\n s0 = inspect(\"staging\")\n p0 = inspect(\"prod\")\n if not app_contract(s0, \"staging\") or not app_contract(p0, \"prod\"):\n fail(\"APP_PRESTATE\")\n if not source_is_old(s0) or not source_is_old(p0):\n fail(\"SOURCE_PRESTATE\")\n if s0.get(\"templates\") != p0.get(\"templates\"):"},{"line":825,"text":" s_error = int(s0[\"application\"][\"error_max_id\"])\n p_error = int(p0[\"application\"][\"error_max_id\"])\n s_old_templates = s0[\"templates\"]\n p_old_templates = p0[\"templates\"]\n emit(\"PORTAL200_ERROR_BASELINE\", {\"staging\": s_error, \"prod\": p_error})\n http_verify(\"staging\", \"old\")\n runtime_verify(\"prod\", \"old\")\n http_verify(\"prod\", \"old\")\n assert_no_new_errors(\"staging\", s_error, \"preflight\")\n assert_no_new_errors(\"prod\", p_error, \"preflight\")\n emit(\"PORTAL200_PREFLIGHT\", \"PASS\")\n\n guest_backup(\"staging\")"},{"line":831,"text":" runtime_verify(\"prod\", \"old\")\n http_verify(\"prod\", \"old\")\n assert_no_new_errors(\"staging\", s_error, \"preflight\")\n assert_no_new_errors(\"prod\", p_error, \"preflight\")\n emit(\"PORTAL200_PREFLIGHT\", \"PASS\")\n\n guest_backup(\"staging\")\n staging_backup = True\n staging_mutated = True\n install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n emit(\"PORTAL200_STAGING_FIRST_APPLY\", \"PASS\")"},{"line":836,"text":"\n guest_backup(\"staging\")\n staging_backup = True\n staging_mutated = True\n install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n emit(\"PORTAL200_STAGING_FIRST_APPLY\", \"PASS\")\n\n restore_old(\"staging\")\n import_addon(\"staging\")\n verify_old(\"staging\", s_error, s_old_templates)\n staging_mutated = False"},{"line":839,"text":" staging_mutated = True\n install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n emit(\"PORTAL200_STAGING_FIRST_APPLY\", \"PASS\")\n\n restore_old(\"staging\")\n import_addon(\"staging\")\n verify_old(\"staging\", s_error, s_old_templates)\n staging_mutated = False\n emit(\"PORTAL200_STAGING_ROLLBACK_PROOF\", \"PASS\")\n\n staging_mutated = True"},{"line":845,"text":" restore_old(\"staging\")\n import_addon(\"staging\")\n verify_old(\"staging\", s_error, s_old_templates)\n staging_mutated = False\n emit(\"PORTAL200_STAGING_ROLLBACK_PROOF\", \"PASS\")\n\n staging_mutated = True\n install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n staging_final_new = True\n emit(\"PORTAL200_STAGING_FINAL\", \"PASS\")\n"},{"line":848,"text":" staging_mutated = False\n emit(\"PORTAL200_STAGING_ROLLBACK_PROOF\", \"PASS\")\n\n staging_mutated = True\n install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n staging_final_new = True\n emit(\"PORTAL200_STAGING_FINAL\", \"PASS\")\n\n for rel in EXPECTED_NEW:\n dest = GIT_PORTAL / rel\n old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None"},{"line":852,"text":" install_candidate(\"staging\")\n import_addon(\"staging\")\n verify_new(\"staging\", s_error)\n staging_final_new = True\n emit(\"PORTAL200_STAGING_FINAL\", \"PASS\")\n\n for rel in EXPECTED_NEW:\n dest = GIT_PORTAL / rel\n old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None\n dest.parent.mkdir(parents=True, exist_ok=True)\n shutil.copyfile(CAND_ROOT / rel, dest)\n git_files_changed = True\n"},{"line":858,"text":" for rel in EXPECTED_NEW:\n dest = GIT_PORTAL / rel\n old_git_bytes[str(rel)] = dest.read_bytes() if dest.exists() else None\n dest.parent.mkdir(parents=True, exist_ok=True)\n shutil.copyfile(CAND_ROOT / rel, dest)\n git_files_changed = True\n\n if git(\"diff\", \"--check\"):\n fail(\"GIT_DIFF_CHECK\")\n status_lines = git(\"status\", \"--porcelain=v1\", \"--untracked-files=all\").splitlines()\n actual_paths = sorted(line[3:] for line in status_lines if len(line) >= 4)\n if actual_paths != sorted(EXPECTED_GIT_PATHS):\n fail(\"GIT_SCOPE\")"},{"line":885,"text":" if remote_head != new_head:\n fail(\"GIT_REMOTE_HEAD\")\n push_confirmed = True\n emit(\n \"PORTAL200_GIT_PROMOTION\",\n {\n \"old_head\": OLD_HEAD,\n \"new_head\": new_head,\n \"remote_head\": remote_head,\n \"result\": \"PASS\",\n },\n )\n"},{"line":902,"text":" fail(\"PROD_ERROR_DRIFT\")\n guest_backup(\"prod\")\n prod_backup = True\n prod_mutated = True\n install_candidate(\"prod\")\n import_addon(\"prod\")\n verify_new(\"prod\", p_error)\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n\n sf = verify_new(\"staging\", s_error)\n pf = verify_new(\"prod\", p_error)\n if sf[\"source\"] != pf[\"source\"]:\n fail(\"FINAL_SOURCE_PARITY\")"},{"line":905,"text":" prod_mutated = True\n install_candidate(\"prod\")\n import_addon(\"prod\")\n verify_new(\"prod\", p_error)\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n\n sf = verify_new(\"staging\", s_error)\n pf = verify_new(\"prod\", p_error)\n if sf[\"source\"] != pf[\"source\"]:\n fail(\"FINAL_SOURCE_PARITY\")\n if sf[\"templates\"] != pf[\"templates\"]:\n fail(\"FINAL_TEMPLATE_PARITY\")\n if git(\"rev-parse\", \"HEAD\") != new_head:"},{"line":922,"text":" if git(\"status\", \"--porcelain=v1\", \"--untracked-files=all\"):\n fail(\"FINAL_GIT_DIRTY\")\n cluster_guard()\n emit(\n \"PORTAL200_FINAL_EVIDENCE\",\n {\n \"commit\": new_head,\n \"source_files\": 3,\n \"templates\": 2,\n \"staging_error_baseline\": s_error,\n \"prod_error_baseline\": p_error,\n },\n )"},{"line":931,"text":" \"staging_error_baseline\": s_error,\n \"prod_error_baseline\": p_error,\n },\n )\n emit(\"PORTAL200_PORTAL\", \"PASS\")\n emit(\"PORTAL200_STAGING\", \"PASS\")\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n emit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\", 21)\n result_contract(\"OK\", True, False, None)\n emit(\"NEWFI200_END\")\n raise SystemExit(0)\n\nexcept PortalError as exc:"},{"line":932,"text":" \"prod_error_baseline\": p_error,\n },\n )\n emit(\"PORTAL200_PORTAL\", \"PASS\")\n emit(\"PORTAL200_STAGING\", \"PASS\")\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n emit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\", 21)\n result_contract(\"OK\", True, False, None)\n emit(\"NEWFI200_END\")\n raise SystemExit(0)\n\nexcept PortalError as exc:\n code = str(exc)"},{"line":933,"text":" },\n )\n emit(\"PORTAL200_PORTAL\", \"PASS\")\n emit(\"PORTAL200_STAGING\", \"PASS\")\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n emit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\", 21)\n result_contract(\"OK\", True, False, None)\n emit(\"NEWFI200_END\")\n raise SystemExit(0)\n\nexcept PortalError as exc:\n code = str(exc)\n emit(\"PORTAL200_FAILURE\", code)"},{"line":934,"text":" )\n emit(\"PORTAL200_PORTAL\", \"PASS\")\n emit(\"PORTAL200_STAGING\", \"PASS\")\n emit(\"PORTAL200_PRODUCTION\", \"PASS\")\n emit(\"PORTAL200_REMAINING_FULL_PRODUCT_COMMANDS_AFTER_PASS\", 21)\n result_contract(\"OK\", True, False, None)\n emit(\"NEWFI200_END\")\n raise SystemExit(0)\n\nexcept PortalError as exc:\n code = str(exc)\n emit(\"PORTAL200_FAILURE\", code)\n"},{"line":941,"text":" raise SystemExit(0)\n\nexcept PortalError as exc:\n code = str(exc)\n emit(\"PORTAL200_FAILURE\", code)\n\n if push_confirmed:\n if prod_mutated and prod_backup:\n failure_rollback_started = True\n try:\n restore_old(\"prod\")\n import_addon(\"prod\")\n verify_old(\"prod\", p_error, p_old_templates, strict_errors=False)"},{"line":952,"text":" import_addon(\"prod\")\n verify_old(\"prod\", p_error, p_old_templates, strict_errors=False)\n prod_mutated = False\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_PROD_ROLLBACK\", \"PASS_PARTIAL_OVERALL_STATE_CHANGED\")\n except Exception:\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_PROD_ROLLBACK\", \"FAIL\")\n else:\n if staging_mutated and staging_backup:\n failure_rollback_started = True\n try:\n restore_old(\"staging\")"},{"line":955,"text":" failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_PROD_ROLLBACK\", \"PASS_PARTIAL_OVERALL_STATE_CHANGED\")\n except Exception:\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_PROD_ROLLBACK\", \"FAIL\")\n else:\n if staging_mutated and staging_backup:\n failure_rollback_started = True\n try:\n restore_old(\"staging\")\n import_addon(\"staging\")\n verify_old(\"staging\", s_error, s_old_templates, strict_errors=False)\n staging_mutated = False"},{"line":966,"text":" verify_old(\"staging\", s_error, s_old_templates, strict_errors=False)\n staging_mutated = False\n staging_final_new = False\n failure_rollback_restored = True\n emit(\"PORTAL200_FAILURE_STAGING_ROLLBACK\", \"PASS\")\n except Exception:\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_STAGING_ROLLBACK\", \"FAIL\")\n\n if git_files_changed and not commit_created:\n try:\n for rel, old in old_git_bytes.items():\n dest = GIT_PORTAL / rel"},{"line":969,"text":" failure_rollback_restored = True\n emit(\"PORTAL200_FAILURE_STAGING_ROLLBACK\", \"PASS\")\n except Exception:\n failure_rollback_restored = False\n emit(\"PORTAL200_FAILURE_STAGING_ROLLBACK\", \"FAIL\")\n\n if git_files_changed and not commit_created:\n try:\n for rel, old in old_git_bytes.items():\n dest = GIT_PORTAL / rel\n if old is None:\n if dest.exists():\n dest.unlink()"},{"line":979,"text":" if old is None:\n if dest.exists():\n dest.unlink()\n else:\n dest.write_bytes(old)\n service_dir = GIT_PORTAL / \"Service\"\n try:\n service_dir.rmdir()\n except OSError:\n pass\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"PASS\")\n except Exception:\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"FAIL\")"},{"line":985,"text":" try:\n service_dir.rmdir()\n except OSError:\n pass\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"PASS\")\n except Exception:\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"FAIL\")\n\n if commit_created:\n emit(\n \"PORTAL200_FAILURE_GIT_HISTORY\",\n \"COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED\",\n )"},{"line":987,"text":" except OSError:\n pass\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"PASS\")\n except Exception:\n emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"FAIL\")\n\n if commit_created:\n emit(\n \"PORTAL200_FAILURE_GIT_HISTORY\",\n \"COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED\",\n )\n\n persisted = bool(push_confirmed or commit_created or staging_final_new or prod_mutated)"},{"line":991,"text":" emit(\"PORTAL200_FAILURE_GIT_WORKTREE_RESTORE\", \"FAIL\")\n\n if commit_created:\n emit(\n \"PORTAL200_FAILURE_GIT_HISTORY\",\n \"COMMIT_CREATED_NO_DESTRUCTIVE_RESET_FRESH_ID_REQUIRED\",\n )\n\n persisted = bool(push_confirmed or commit_created or staging_final_new or prod_mutated)\n if failure_rollback_started and failure_rollback_restored is None:\n failure_rollback_restored = False\n result_contract(\n \"FAIL\","},{"line":1004,"text":" persisted,\n failure_rollback_started,\n failure_rollback_restored,\n )\n emit(\"PORTAL200_ROADMAP_REMAINS\", 22)\n emit(\"NEWFI200_END\")\n raise SystemExit(1)\n\nexcept Exception:\n emit(\"PORTAL200_FAILURE\", \"UNEXPECTED_EXCEPTION\")\n result_contract(\n \"FAIL\",\n bool(push_confirmed or commit_created or staging_final_new or prod_mutated),"},{"line":1009,"text":" emit(\"NEWFI200_END\")\n raise SystemExit(1)\n\nexcept Exception:\n emit(\"PORTAL200_FAILURE\", \"UNEXPECTED_EXCEPTION\")\n result_contract(\n \"FAIL\",\n bool(push_confirmed or commit_created or staging_final_new or prod_mutated),\n False,\n None,\n )\n emit(\"PORTAL200_ROADMAP_REMAINS\", 22)\n emit(\"NEWFI200_END\")"},{"line":1016,"text":" bool(push_confirmed or commit_created or staging_final_new or prod_mutated),\n False,\n None,\n )\n emit(\"PORTAL200_ROADMAP_REMAINS\", 22)\n emit(\"NEWFI200_END\")\n raise SystemExit(1)"}] +RELEVANT_FUNCTIONS=[{"lineno":400,"name":"parse_inspect","sha256":"7b72b62a0ff4c617b7175627d8630294160a6c129f3acfb0d109b4c72c2e8fe2","source":"def parse_inspect(text):\n b = \"PORTAL200_INSPECT_JSON_BEGIN\\n\"\n e = \"\\nPORTAL200_INSPECT_JSON_END\"\n if b not in text or e not in text:\n fail(\"INSPECT_MARKERS\")\n try:\n return json.loads(text.split(b, 1)[1].split(e, 1)[0])\n except Exception:\n fail(\"INSPECT_JSON\")"},{"lineno":447,"name":"app_contract","sha256":"a3a06560de8f2db42e0c3bafc27ad29ee985fd5bf13b01c527a283731ed7904c","source":"def app_contract(data, target):\n a = data.get(\"application\", {})\n addon = data.get(\"addon\") or {}\n expected = {\n \"staging\": (\"0\", \"http://newfi-staging.gram1.ru\"),\n \"prod\": (\"1\", \"https://newfi.ru\"),\n }[target]\n return (\n a.get(\"boardActive\") == expected[0]\n and a.get(\"boardUrl\") == expected[1]\n and a.get(\"indexRoute\") == \"portal/\"\n and a.get(\"defaultStyleId\") == \"4\"\n and addon.get(\"addon_id\") == \"Newfi/Portal\"\n and int(addon.get(\"version_id\", 0)) == 1000010\n and addon.get(\"version_string\") == \"1.0.0 Alpha 1\"\n and int(addon.get(\"active\", 0)) == 1\n and int(addon.get(\"is_processing\", 1)) == 0\n )"},{"lineno":480,"name":"assert_no_new_errors","sha256":"801839c1705804fed1a507709263d2882a332f0276b42a7c5439204b99428dfb","source":"def assert_no_new_errors(target, baseline, phase):\n rows = new_errors(target, baseline)\n if rows:\n emit(\n \"PORTAL200_NEW_XF_ERRORS\",\n {\n \"target\": target,\n \"phase\": phase,\n \"count\": len(rows),\n \"ids\": [r.get(\"error_id\") for r in rows],\n },\n )\n fail(\"NEW_XF_ERRORS\")"},{"lineno":495,"name":"guest_backup","sha256":"3e0d88922bf3d3420512f69cd0e50232e0325391cebb99845ed1c8c206ec08a1","source":"def guest_backup(target):\n script = f'''set -eu\nB={shlex.quote(BACKUP)}\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest ! -e \"$B\"\ninstall -d -m 700 \"$B/Pub/Controller\" \"$B/_data\"\ncp -p -- \"$P/Pub/Controller/Portal.php\" \"$B/Pub/Controller/Portal.php\"\ncp -p -- \"$P/_data/templates.xml\" \"$B/_data/templates.xml\"\ntest ! -e \"$P/Service/PortalData.php\"\nprintf 'PORTAL200_BACKUP=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_BACKUP=PASS\" not in out:\n fail(\"BACKUP_FAILED\")"},{"lineno":511,"name":"guest_write","sha256":"f5f8ed7e6553eadf19e219ed9cab5dd6d7b9ad72dca3926e5368ad663f9b2881","source":"def guest_write(target, rel, data):\n dest = f\"/var/www/forums/newfi/public/src/addons/Newfi/Portal/{rel}\"\n parent = str(pathlib.PurePosixPath(dest).parent)\n script = (\n \"set -eu; umask 022; \"\n + \"install -d -m 755 \"\n + shlex.quote(parent)\n + \"; T=$(mktemp \"\n + shlex.quote(parent + \"/.portal200.XXXXXX\")\n + \"); cat >\\\"$T\\\"; chmod 0644 \\\"$T\\\"; chown root:root \\\"$T\\\"; \"\n + \"mv -f -- \\\"$T\\\" \"\n + shlex.quote(dest)\n + \"; printf 'PORTAL200_WRITE=PASS\\\\n'\"\n )\n out = qga(\n target,\n [\"/bin/bash\", \"-lc\", script],\n input_bytes=data,\n timeout=90,\n )\n if \"PORTAL200_WRITE=PASS\" not in out:\n fail(\"WRITE_FAILED\")"},{"lineno":535,"name":"install_candidate","sha256":"b46ecffec3c0972b1306ffb057164c1223e08f26bd3121bfde7f624ffad66903","source":"def install_candidate(target):\n for rel in [\n \"Pub/Controller/Portal.php\",\n \"Service/PortalData.php\",\n \"_data/templates.xml\",\n ]:\n guest_write(target, rel, (CAND_ROOT / rel).read_bytes())"},{"lineno":544,"name":"restore_old","sha256":"eafa8284ff101e7e3c189922361201ce5684ce970e90e2c933c39b65e25e21da","source":"def restore_old(target):\n script = f'''set -eu\nB={shlex.quote(BACKUP)}\nP=/var/www/forums/newfi/public/src/addons/Newfi/Portal\ntest -f \"$B/Pub/Controller/Portal.php\"\ntest -f \"$B/_data/templates.xml\"\ncp -p -- \"$B/Pub/Controller/Portal.php\" \"$P/Pub/Controller/Portal.php\"\ncp -p -- \"$B/_data/templates.xml\" \"$P/_data/templates.xml\"\nrm -f -- \"$P/Service/PortalData.php\"\nrmdir -- \"$P/Service\" 2>/dev/null || true\nprintf 'PORTAL200_SOURCE_RESTORE=PASS\\n'\n'''\n out = qga(target, [\"/bin/bash\", \"-lc\", script], timeout=90)\n if \"PORTAL200_SOURCE_RESTORE=PASS\" not in out:\n fail(\"SOURCE_RESTORE_FAILED\")"},{"lineno":561,"name":"import_addon","sha256":"8af6f854e1917ee8e02cfe345f730da0b4b6fbe1b44fdb73ced52c8a3eeebb50","source":"def import_addon(target):\n out = qga(\n target,\n [\"/usr/bin/php8.3\"],\n input_bytes=IMPORT_PHP.encode(),\n timeout=240,\n )\n if \"PORTAL200_IMPORT_JSON=\" not in out:\n fail(\"IMPORT_FAILED\")"},{"lineno":572,"name":"runtime_verify","sha256":"cbe3a884afd8bb310ce9d055ddaf74a9923ac55e975ad5a9c3fbb0b910776497","source":"def runtime_verify(target, state):\n src = RUNTIME_VERIFY_PHP_TEMPLATE.replace(\"__STATE__\", state)\n out = qga(target, [\"/usr/bin/php8.3\"], input_bytes=src.encode(), timeout=120)\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_RUNTIME_JSON=\")),\n None,\n )\n if not line:\n fail(\"RUNTIME_MARKER_MISSING\")\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n except Exception:\n fail(\"RUNTIME_JSON\")\n emit(\"PORTAL200_RUNTIME\", {\"target\": target, **data})\n return data"},{"lineno":589,"name":"http_verify","sha256":"75eb5fbec1862b5d926cbc91519c284218a4bb05db9edf616422fa187b8348a6","source":"def http_verify(target, state):\n argv = [\"/usr/bin/python3\", \"-\", target, state]\n if target == \"prod\":\n try:\n p = subprocess.run(\n argv,\n input=HTTP_VERIFY_PY.encode(),\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n timeout=180,\n check=False,\n )\n except subprocess.TimeoutExpired:\n fail(\"HTTP_PROD_TIMEOUT\")\n out = p.stdout.decode(\"utf-8\", \"replace\")\n err = p.stderr.decode(\"utf-8\", \"replace\")\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_HTTP_JSON=\")),\n None,\n )\n data = None\n if line:\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\n except Exception:\n fail(\"HTTP_JSON\")\n if p.returncode != 0:\n emit(\n \"PORTAL200_HTTP_EXEC\",\n {\n \"target\": target,\n \"state\": state,\n \"rc\": p.returncode,\n \"stderr\": err[:800],\n },\n )\n fail(\"HTTP_PROD_RC\")\n if data is None:\n fail(\"HTTP_MARKER_MISSING\")\n return data\n\n out = qga(\n target,\n argv,\n input_bytes=HTTP_VERIFY_PY.encode(),\n timeout=180,\n )\n line = next(\n (x for x in out.splitlines() if x.startswith(\"PORTAL200_HTTP_JSON=\")),\n None,\n )\n if not line:\n fail(\"HTTP_MARKER_MISSING\")\n try:\n data = json.loads(line.split(\"=\", 1)[1])\n except Exception:\n fail(\"HTTP_JSON\")\n emit(\"PORTAL200_HTTP\", {\"target\": target, \"state\": state, **data})\n return data"},{"lineno":692,"name":"cluster_guard","sha256":"bd6547918004a2f74ae4ac8cc7d7caf80b4ced2bcf57cc98ddc454561121dcac","source":"def cluster_guard():\n p = run([\"/usr/sbin/qm\", \"status\", \"210\"], timeout=30)\n if \"status: running\" not in p.stdout.decode():\n fail(\"VM210_NOT_RUNNING\")\n p = run(\n [\n \"/usr/bin/ssh\",\n \"-T\",\n \"-o\",\n \"BatchMode=yes\",\n \"-o\",\n \"StrictHostKeyChecking=yes\",\n \"-o\",\n \"UpdateHostKeys=no\",\n \"-o\",\n \"ConnectTimeout=10\",\n \"root@pve03\",\n \"/usr/sbin/qm status 211\",\n ],\n timeout=40,\n )\n if \"status: running\" not in p.stdout.decode():\n fail(\"VM211_NOT_RUNNING\")\n emit(\"PORTAL200_CLUSTER\", \"PASS\")"}] +LITERAL_PAYLOADS=[{"bytes":2008,"lineno":147,"names":["INSPECT_PHP"],"sha256":"8de3d66a7adfca4c179db55bb5c4e956f6750fb41e9e058b449efeb214cbd7f1","value":"db();\nfunction srcMeta(string $path): ?array {\n if (!is_file($path)) { return null; }\n return ['bytes' => filesize($path), 'sha256' => hash_file('sha256', $path)];\n}\n$portal = $root . '/src/addons/Newfi/Portal';\n$templates = $db->fetchAll(\n \"SELECT title, style_id, LENGTH(template) AS bytes, SHA2(template,256) AS sha256, \" .\n \"LOCATE('data-newfi-portal200', template) > 0 AS html_marker, \" .\n \"LOCATE('--newfi-portal200', template) > 0 AS css_marker \" .\n \"FROM xf_template WHERE addon_id='Newfi/Portal' ORDER BY style_id,title\"\n);\n$result = [\n 'hostname' => gethostname(),\n 'application' => [\n 'boardActive' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardActive'\"),\n 'boardUrl' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='boardUrl'\"),\n 'indexRoute' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='indexRoute'\"),\n 'defaultStyleId' => (string)$db->fetchOne(\"SELECT option_value FROM xf_option WHERE option_id='defaultStyleId'\"),\n 'error_count' => (int)$db->fetchOne(\"SELECT COUNT(*) FROM xf_error_log\"),\n 'error_max_id' => (int)$db->fetchOne(\"SELECT COALESCE(MAX(error_id),0) FROM xf_error_log\")\n ],\n 'addon' => $db->fetchRow(\n \"SELECT addon_id,version_id,version_string,active,is_processing,last_pending_action \" .\n \"FROM xf_addon WHERE addon_id='Newfi/Portal'\"\n ),\n 'source' => [\n 'Pub/Controller/Portal.php' => srcMeta($portal . '/Pub/Controller/Portal.php'),\n 'Service/PortalData.php' => srcMeta($portal . '/Service/PortalData.php'),\n '_data/templates.xml' => srcMeta($portal . '/_data/templates.xml')\n ],\n 'templates' => $templates\n];\necho \"PORTAL200_INSPECT_JSON_BEGIN\\n\";\necho json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\necho \"\\nPORTAL200_INSPECT_JSON_END\\n\";\n?>"},{"bytes":1429,"lineno":194,"names":["IMPORT_PHP"],"sha256":"8f4109ed896f29035cfa5534751507efff2786d4d6d18840adf30b84771a18e8","value":"addOnManager()->getById('Newfi/Portal');\nif (!$addOn || !$addOn->isInstalled()) {\n fwrite(STDERR, \"ADDON_NOT_INSTALLED\\n\");\n exit(30);\n}\n$addOn->preRebuild();\n$dataManager = $app->addOnDataManager();\n$imported = [];\nforeach ($dataManager->getDataTypeClasses() as $dataType) {\n $handler = $dataManager->getDataTypeHandler($dataType);\n $xml = $handler->openTypeFile($addOn->getDataDirectory());\n if (!$xml || !$xml->count()) {\n $handler->deleteAddOnData($addOn->addon_id);\n continue;\n }\n $start = 0;\n $guard = 0;\n while (true) {\n $last = $handler->importAddOnData($addOn->addon_id, $xml, $start, 0);\n if (!$last) { break; }\n if ($last <= $start || ++$guard > 100000) {\n fwrite(STDERR, \"IMPORT_PROGRESS_INVALID\\n\");\n exit(31);\n }\n $start = $last;\n }\n $handler->deleteOrphanedAddOnData($addOn->addon_id, $xml);\n \\XF::triggerRunOnce();\n $imported[] = $handler->getContainerTag();\n}\n$addOn->postDataImport();\n$repo = $app->repository(\\XF\\Repository\\StyleRepository::class);\n$repo->updateAllStylesLastModifiedDateLater();\n$addOn->postRebuild();\necho \"PORTAL200_IMPORT_JSON=\" . json_encode(\n ['result'=>'PASS','containers'=>$imported],\n JSON_UNESCAPED_SLASHES\n) . \"\\n\";\n?>"},{"bytes":410,"lineno":240,"names":["ERRORS_PHP_TEMPLATE"],"sha256":"ee412cafa99d7d809c8b682db8ebb0bc2958ab0cbc9e37537e22a78f10489635","value":"db();\n$b=%d;\n$rows=$db->fetchAll(\n \"SELECT error_id,exception_type,message,filename,line FROM xf_error_log WHERE error_id>? ORDER BY error_id\",\n [$b]\n);\necho json_encode(\n $rows,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n);\n?>"},{"bytes":2501,"lineno":258,"names":["RUNTIME_VERIFY_PHP_TEMPLATE"],"sha256":"884c03648203992cf6d5b96be8342243739b0412fdff55f4a6b64cf96aad3313","value":"$state];\nif ($state === 'new') {\n $service = new \\Newfi\\Portal\\Service\\PortalData($app);\n $portal = $service->build();\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', ['portal'=>$portal]);\n $result['sections'] = [];\n foreach (['forums','resources','media','events','dogs'] as $key) {\n $section = $portal[$key] ?? null;\n if (!is_array($section)) { fwrite(STDERR, \"SECTION_MISSING\\n\"); exit(60); }\n $result['sections'][$key] = [\n 'available'=>(bool)($section['available'] ?? false),\n 'hasItems'=>(bool)($section['hasItems'] ?? false),\n 'status'=>(string)($section['status'] ?? '')\n ];\n }\n $result['html_marker'] = strpos($html, 'data-newfi-portal200=\"1\"') !== false;\n $result['locked_resources'] = strpos($html, 'data-portal-locked=\"resources\"') !== false;\n $result['locked_media'] = strpos($html, 'data-portal-locked=\"media\"') !== false;\n $result['html_bytes'] = strlen($html);\n if (!$result['html_marker']) { fwrite(STDERR, \"HTML_MARKER_MISSING\\n\"); exit(61); }\n if ($result['sections']['resources']['available'] || $result['sections']['media']['available']) {\n fwrite(STDERR, \"GUEST_PERMISSION_CONTRACT_CHANGED\\n\"); exit(62);\n }\n if (!$result['locked_resources'] || !$result['locked_media']) {\n fwrite(STDERR, \"LOCKED_COPY_MISSING\\n\"); exit(63);\n }\n if (!$result['sections']['forums']['available'] || !$result['sections']['events']['available'] || !$result['sections']['dogs']['available']) {\n fwrite(STDERR, \"PUBLIC_SECTION_UNAVAILABLE\\n\"); exit(64);\n }\n} else if ($state === 'old') {\n $html = (string)$app->templater()->renderTemplate('public:newfi_portal', []);\n $result['html_marker'] = strpos($html, 'data-newfi-portal200') !== false;\n $result['old_copy'] = strpos($html, 'Закрытое пространство для общения, знаний и материалов сообщества.') !== false;\n $result['html_bytes'] = strlen($html);\n if ($result['html_marker'] || !$result['old_copy']) {\n fwrite(STDERR, \"OLD_RENDER_CONTRACT\\n\"); exit(65);\n }\n} else {\n exit(66);\n}\necho \"PORTAL200_RUNTIME_JSON=\" . json_encode(\n $result,\n JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_INVALID_UTF8_SUBSTITUTE\n) . \"\\n\";\n?>"},{"bytes":3277,"lineno":311,"names":["HTTP_VERIFY_PY"],"sha256":"15b9839d95798833aef597b59787d301155d32cfb404b14ca7e0bf7d83a1a71e","value":"import html,json,re,subprocess,sys,urllib.parse\nmode=sys.argv[1]\nstate=sys.argv[2]\nif mode=='staging':\n host='newfi-staging.gram1.ru'; scheme='http'; port='80'; expected_status=503\nelif mode=='prod':\n host='newfi.ru'; scheme='https'; port='443'; expected_status=200\nelse:\n raise SystemExit(40)\ndef fetch(url):\n cmd=[\n '/usr/bin/curl','-sS','-k','--max-time','30','--connect-timeout','10'\n ]\n if mode=='staging':\n cmd += ['--resolve',f'{host}:{port}:127.0.0.1']\n cmd += [\n '-o','-','-w','\\n__PORTAL_STATUS__%{http_code}',url\n ]\n p=subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,check=False)\n if p.returncode!=0: raise SystemExit(41)\n text=p.stdout.decode('utf-8','replace')\n marker='\\n__PORTAL_STATUS__'\n if marker not in text: raise SystemExit(42)\n body,status=text.rsplit(marker,1)\n return int(status.strip()),body\nroot=f'{scheme}://{host}/'\nstatus,body=fetch(root)\nhrefs=[]\nfor raw in re.findall(r\"href=[\\\"']([^\\\"']*css\\.php[^\\\"']*)[\\\"']\",body,re.I):\n raw=html.unescape(raw)\n hrefs.append(urllib.parse.urljoin(root,raw))\nnormal_css=[]\nnormal_status=[]\nfor u in hrefs:\n s,b=fetch(u)\n normal_status.append(s)\n normal_css.append(b)\nnormal_css_text='\\n'.join(normal_css)\nforced_css_marker=None\nforced_css_status=None\nif mode=='staging' and state=='new' and hrefs:\n u=urllib.parse.urlsplit(hrefs[0])\n pairs=urllib.parse.parse_qsl(u.query,keep_blank_values=True)\n out=[]\n found=False\n for k,v in pairs:\n if k=='css':\n found=True\n parts=[x for x in v.split(',') if x]\n if 'public:newfi_portal.less' not in parts:\n parts.append('public:newfi_portal.less')\n v=','.join(parts)\n out.append((k,v))\n if found:\n forced=urllib.parse.urlunsplit((u.scheme,u.netloc,u.path,urllib.parse.urlencode(out),u.fragment))\n forced_css_status,forced_body=fetch(forced)\n forced_css_marker='--newfi-portal200' in forced_body\nresult={\n 'status':status,\n 'bytes':len(body.encode()),\n 'html_marker':'data-newfi-portal200=\"1\"' in body,\n 'css_marker':'--newfi-portal200' in normal_css_text,\n 'old_copy':'Закрытое пространство для общения, знаний и материалов сообщества.' in body,\n 'css_count':len(hrefs),\n 'css_status':normal_status,\n 'forced_css_status':forced_css_status,\n 'forced_css_marker':forced_css_marker,\n}\nprint('PORTAL200_HTTP_JSON='+json.dumps(result,ensure_ascii=False,sort_keys=True,separators=(',',':')))\nif status!=expected_status: raise SystemExit(45)\nif mode=='prod':\n if not hrefs or any(x!=200 for x in normal_status): raise SystemExit(46)\nelif mode=='staging':\n if any(x!=200 for x in normal_status): raise SystemExit(46)\nelse:\n raise SystemExit(50)\nif mode=='prod' and state=='new':\n if not result['html_marker'] or not result['css_marker']: raise SystemExit(47)\nelif mode=='prod' and state=='old':\n if result['html_marker'] or result['css_marker'] or not result['old_copy']: raise SystemExit(48)\nelif mode=='staging' and state=='new':\n if hrefs and (forced_css_status!=200 or not forced_css_marker): raise SystemExit(49)\nelif mode=='staging' and state=='old':\n pass\nelse:\n raise SystemExit(50)\n"}] +SOURCE_VERSION_IDS=[] +SOURCE_VERSION_STRINGS=[] +AST_RECOVERY_END=true OUTPUT_END CHAT_OUTPUT_END