docs(offers): verify live target backup and isolated recovery before install
This commit is contained in:
@@ -10211,3 +10211,6 @@ A Tempo aceitou tecnicamente `9a014268` e autorizou apenas preparação. Parecer
|
||||
# Menus das Ofertas uniformizados na prévia QA — 14/11/2026
|
||||
|
||||
Por pedido expresso da Tempo durante a conferência, a navegação privada das Ofertas reutiliza o padrão horizontal com dropdown do portal. Área de trabalho → Ofertas agrupa consulta/preparação conforme os acessos existentes; o módulo agrupa Ofertas, estados e acompanhamento em dropdowns responsivos. Mudar de estado conserva os filtros de acompanhamento/denúncias. Todas as páginas Ofertas usam o mesmo enquadramento PortalNavigation. Não foram alteradas permissões, rotas, publicação ou dados. Passaram 29 testes de navegação, tipos, lint focado e build QA; um aviso de lint preexistente fora do delta foi preservado. A lista e os dois exemplos já existentes responderam HTTP 200 com os novos assets, mantendo a sessão e os dados da prévia. Conferência visual pelo utilizador pendente; sem instalação em homologação. Recibo: `outputs/offers-menu-qa-20261114/qa-result.json`. O bundle anterior `9a014268` permanece preservado e não inclui este novo delta visual.
|
||||
# Verificação real pré-instalação das Ofertas — 14/11/2026
|
||||
|
||||
Por autorização específica, destino `teste-investidor.tempo.co.mz` confirmado em `96f51096`, sem instalar Ofertas ou alterar configuração/permissões/dados operacionais. Estruturas verificadas compatíveis; GD/WebP presente em CLI/FPM e codec em memória aprovado. Backup actual privado e restauro isolado passaram: 219 tabelas, 417 ficheiros, dois conjuntos de MFA; zero originais Laravel cifrados na fonte para ensaiar. Release anterior arquivada e 9.873 ficheiros reconferidos. Após a cópia, apenas uma sessão nova, com as 51 anteriores intactas; nenhuma outra tabela divergente. Parecer APTO COM CONDIÇÕES: autorização futura com SHA expresso (base `9a014268` ou candidata com menus `3558647e`), reconferência da salvaguarda e janela sem integrações concorrentes; o executor segue o último `main`. Publicação/NDA/agendamento/correio de Ofertas mantidos fechados; visual, galeria completa, email real e decisões jurídicas permanecem separados. Relatório: `../v2/auditoria-portal/ofertas-verificacao-preinstalacao-20261114.md`; recibos sanitizados em `outputs/offers-preinstall-verification-20261114`. Não houve instalação, migração operacional, alteração de `mne` ou envio real de email.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8,6 +8,8 @@
|
||||
|
||||
**Preparação final após aceitação técnica expressa de `9a014268`:** [parecer de homologação privada](ofertas-prontidao-homologacao-20261114.md). Integridade das fases/correcção reconfirmada por 48 hashes; bundle local verificado. Aprovação técnica em QA aceite; instalação ainda não autorizada/executada, condicionada à conferência real do destino e backup/restauro actual. Publicação operacional permanece bloqueada. Visual, GD/derivadas e email real são critérios de avaliação pendentes, sem invalidar a prova integrada privada aceite.
|
||||
|
||||
**Destino e salvaguarda posteriormente verificados por autorização específica:** [verificação pré-instalação](ofertas-verificacao-preinstalacao-20261114.md), **APTO COM CONDIÇÕES**. Schema/runtime compatíveis, GD/WebP do destino disponível, restauro isolado de 219 tabelas/417 ficheiros aprovado. Instalação e publicação continuam não autorizadas; a instalação exige SHA expresso e janela sem integrações concorrentes porque o executor segue o último `main`. Menus `3558647e` constituem delta QA posterior à candidata `9a014268`.
|
||||
|
||||
## Evidência e limites
|
||||
|
||||
- Confirmada a ancestralidade dos commits F1 `1f2990e4`, plano `6e302db6`, F2 `91c18573`, F3 `7691cab3`, F4 `e431de8b` e F5 `528a2597`.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
14/11/2026. Candidata técnica aceite: **`9a01426864a47dacd97db2b4da363aba86a94df6`**. Esta passagem autoriza exclusivamente preparação; não houve instalação, alteração operacional, migração, activação pública ou envio de email.
|
||||
|
||||
**Actualização da verificação autorizada posterior:** [parecer real pré-instalação](ofertas-verificacao-preinstalacao-20261114.md), **APTO COM CONDIÇÕES**. Destino `96f51096` conferido; backup/restauro de 219 tabelas e 417 ficheiros aprovado; GD/WebP disponível. As referências abaixo a destino/backup ainda por conferir descrevem a preparação original. Mantêm-se nova autorização de instalação e condição de revisão exclusiva em `main`; nenhuma instalação executada.
|
||||
|
||||
## Parecer
|
||||
|
||||
**Existe validação integrada em QA suficiente para recomendar uma instalação controlada de avaliação privada, com condições de entrada. Não existe aceitação integral para operação/publicação pública.** A autorização para instalar continua separada e ainda não foi recebida nesta passagem.
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# Ofertas — verificação real pré-instalação
|
||||
|
||||
14/11/2026. Autorização exclusiva de inspeção, cópia actual e restauro isolado; nenhuma autorização de instalação ou publicação. Referências: preparação `d954c83b`, integração tecnicamente aceite `9a014268`, menus QA `3558647e`.
|
||||
|
||||
## Parecer: APTO COM CONDIÇÕES
|
||||
|
||||
O destino é compatível com a **avaliação privada** das Ofertas. As estruturas verificadas estão presentes, não há migrações/dependências novas no delta, GD/WebP está disponível no destino e o backup/restauro actual passou. A conclusão não autoriza instalar nem declarar aceite a publicação operacional.
|
||||
|
||||
A instalação futura tem de identificar uma revisão exacta e ocorrer numa janela sem integrações concorrentes: o executor actual segue o último `main`, em vez de fixar o SHA indicado no pedido. Se essa condição não puder ser garantida, **não se cria o pedido de instalação nem se publica a candidata em `main`**. Nenhuma alteração ao executor ou às suas configurações foi feita.
|
||||
|
||||
## Destino e compatibilidade comprovados
|
||||
|
||||
| Item | Observado no destino real |
|
||||
| --- | --- |
|
||||
| Endereço / revisão instalada e servida | `https://teste-investidor.tempo.co.mz` — `96f510965f70581a4b41dd94463f598b77dd7cf5` |
|
||||
| Release resolvida | `/var/www/investmoz-homologacao/releases/96f510965f70581a4b41dd94463f598b77dd7cf5-8ee72062` |
|
||||
| Base real | `teste_investidor`, porta 5432; PostgreSQL 17.11; PostGIS 3.6.4; pgcrypto 1.3 |
|
||||
| Aplicação | PHP CLI 8.4.27; Nginx, PHP-FPM 8.4, PostgreSQL, fila existente e ClamAV activos |
|
||||
| Executor | `/var/www/deploy/investmoz-pipeline.py`, serviço inactivo, path activo, zero pedidos pendentes na observação |
|
||||
| Configuração | Perfis e circuito editorial activos; notificações desactivadas; zero destinatários autorizados; nenhuma configuração alterada |
|
||||
| Segurança observada | `APP_ENV=local`, debug falso; cookies Secure e HttpOnly, SameSite lax; URL HTTPS; sem overrides APP_ENV/APP_DEBUG encontrados no FPM/Nginx |
|
||||
| Reserva global do ambiente | `homologation_only=false`, modo configurado `perimeter`; não se presume reserva global activa a partir do nome do modo. As Ofertas privadas continuam a exigir autenticação/perfis/âmbito próprios. Não foi alterada esta configuração. |
|
||||
| Capacidade inicial | Cerca de 24,6 GB livres; condição de capacidade do executor a reconferir antes de instalar |
|
||||
|
||||
Foram verificadas 19 relações de suporte, sem ausências nas colunas requeridas: Oportunidades/gestão/JSON/dono/organização, utilizadores e revisões de acesso, perfis, pedidos/aprovações de segurança, recursos, documentos/cifra/scan, edições/notificações, auditoria, tarefas, dossiers/contexto e rascunhos. Confirmados a FK das notificações, `portal_editorial_notice_subject` e `portal_editorial_op_notice_uq`. O destino regista 112 migrações.
|
||||
|
||||
O delta `96f51096..3558647e` não contém diferenças em `database/migrations`, `composer.lock` ou `package-lock.json`. **Não se prevê executar qualquer migração operacional.** Não se aplicaram seeders, concessões de permissões, alterações a `mne` ou alterações de estrutura no destino.
|
||||
|
||||
Evidências sanitizadas em `outputs/offers-preinstall-verification-20261114/target-runtime-schema.json`, `constraints-automatics.json`, `security-runtime.json` e `executor-status.json`. A release não é um checkout Git: a revisão foi confirmada pelo recibo do executor e pelo manifesto servido, não por um `git status` vazio.
|
||||
|
||||
## Publicação e automatismos
|
||||
|
||||
O módulo ainda não está instalado: não existem classes de preparação/operações/catálogo de Ofertas no destino, não há rotas `ofertas/offers` registadas, preparações privadas, projecções públicas ou registos publicados de Ofertas. Não há jobs ou falhas de `NotifyPrivateOfferDecision` na fila observada.
|
||||
|
||||
Na candidata, mantêm-se as barreiras aceites em QA: o modelo conserva o marcador privado e exige `draft`/`published_at=null`; o scope nativo exclui Ofertas; aprovação não publica; os consumidores públicos normais permanecem fechados; listagem/detalhe/API/mapa/CSV só foram demonstrados com injecção sintética guardada pela identidade da base isolada. A base real 5432 não satisfaz essa guarda. Nenhuma rota ou variável de ambiente foi aberta para executar esta verificação.
|
||||
|
||||
NDA permanece sem exemplar configurado e o agendamento optativo de expiração está desactivado. A instalação privada deve preservar estes estados. As provas de bloqueio das cinco fases e a correcção integrada foram reutilizadas; não se criaram Ofertas ou ensaios de publicação no destino real para voltar a testar o que já foi aceite.
|
||||
|
||||
## Backup actual e restauro executados
|
||||
|
||||
Backup criado em **14/11/2026 às 19:34:37, Africa/Johannesburg (17:34:37 UTC)**, em `/var/backups/investmoz-offers-preinstall-20261114/backup`, directório privado com modo 0700. A fonte foi lida por transacções `REPEATABLE READ READ ONLY`, com snapshot exportado partilhado entre o dump e as contagens/checksums. O arquivo inclui todos os schemas de aplicação; não se limitou a `public`.
|
||||
|
||||
| Prova | Resultado |
|
||||
| --- | --- |
|
||||
| Dump consistente | 219 tabelas; SHA256 `662c5d9f1fe7e01a5ccf7529c8c8e8f27205907d60d2da75a580b7b6c37c252e` |
|
||||
| Ficheiros persistentes e configuração | 417 ficheiros verificados; arquivo SHA256 `f438f31f212e516442526b609bf5ac02cfd2f630a6ac2a2b6fc12f1f88717a42` |
|
||||
| Código, assets e dependências da release anterior | Arquivo de 9.873 ficheiros regulares, 25.862.122 bytes; SHA256 `914bc32627a51ec9001d07e5ac31d5717719a4a7f93e43dc26189865962735ca` |
|
||||
| Restauro da base | Nova instância `offers_preinstall_restore_20261114_test`, porta interna 55441, imagem PostGIS já existente e fixada por digest, rede `none`, sem porta publicada |
|
||||
| Comparação do restauro | 219/219 tabelas com contagens/checksums exactamente iguais ao snapshot |
|
||||
| Recuperação dos ficheiros | 417/417 ficheiros extraídos apenas para directório privado de ensaio e verificados por tamanho/hash; nenhuma extracção sobre a aplicação |
|
||||
| Chaves e envelopes | Dois conjuntos de MFA recuperados com as chaves preservadas; zero documentos activos com `storage_encryption=laravel` nesta fonte, pelo que não se inventa uma prova de decifragem de originais inexistentes |
|
||||
| Limpeza do ensaio | Container/volume criado para a prova removido após validação do seu identificador e isolamento; sem tocar no cluster real ou no QA local da prévia |
|
||||
|
||||
Configuração, segredos e dados do backup permaneceram no directório privado do servidor; não foram colocados no repositório ou nas respostas. Os recibos locais contêm apenas metadados e resultados sanitizados. O restauro usa `pg_restore` para reproduzir o snapshot na instância nova; não executa migrações do produto nem altera o schema `mne` operacional.
|
||||
|
||||
Na verificação posterior, os 9.873 ficheiros da release e os ficheiros persistentes/configuração mantêm-se iguais. Das 219 tabelas, 218 permanecem iguais ao backup; a diferença é uma sessão adicional. A reconciliação confirmou **51 sessões anteriores integralmente preservadas e sem alteração de campos, zero removidas, 52 actuais**. Não se sobrescreveu esta actividade concorrente para obter igualdade artificial. As operações desta verificação não executaram escritas de dados operacionais.
|
||||
|
||||
Recibos: `backup-summary.json`, `restore-summary.json`, `target-final-preservation.json`, `data-preservation.json`, `session-reconciliation.json`. Os procedimentos revistos de backup/restauro estão na mesma pasta de evidências. A cópia é válida para o instante registado; se a instalação ocorrer posteriormente, reconferir actividade e actualizar a salvaguarda quando necessário.
|
||||
|
||||
## Imagens e email
|
||||
|
||||
**GD/WebP:** presentes no CLI e no PHP-FPM do destino. O ensaio em memória de PNG 400×400 → descodificação → WebP → conferência de dimensões passou, sem ler/alterar uploads ou criar documentos operacionais. A ausência de GD continua a ser uma característica do PHP QA local, não um bloqueante de dependências deste destino. Isto não substitui a futura aceitação do processamento completo das Ofertas, quarentena/scans, derivadas, metadados e galeria com exemplos autorizados.
|
||||
|
||||
**Email:** SMTP configurado, mas `portal.notifications_enabled=false` e lista autorizada vazia. Nenhum email foi enviado. O job de Ofertas reconfirma destinatário na allowlist e acesso à versão. Falta designar uma caixa de teste e autorizar o ensaio/configuração correspondente. Preferir instância isolada com dados sintéticos, destinatário controlado e apenas o job identificado; não activar a flag partilhada nem escoar a fila acumulada para provar entrega. As actuais condições impedem o envio de Ofertas a utilizadores operacionais. A recepção real continua por verificar.
|
||||
|
||||
## Operações exactas previstas — não executadas
|
||||
|
||||
1. **Decisão da Tempo:** autorizar um SHA concreto. A integração aceite é `9a014268`; a prévia com menus é `3558647e8d82092cd0e31eefbe8b9e5e9eacf573`. O bundle congelado da preparação anterior contém apenas `9a014268`. Para instalar o que foi mostrado na prévia, a autorização terá de incluir expressamente `3558647e`; não substituir silenciosamente uma candidata pela outra.
|
||||
2. **Entrada controlada:** reconfirmar revisão instalada `96f51096`, ficheiros sem drift, estruturas/flags, capacidade, backup/restauro e fila vazia. Acordar janela sem integrações concorrentes em `main`. Se não houver garantia de revisão exclusiva, não iniciar o executor.
|
||||
3. **Integração Git:** só depois da autorização, fazer chegar a revisão exacta ao ramo `main` de `nala/Investidor-V2` pelo processo existente; não publicar simplesmente o HEAD local, não criar pedidos nesta verificação. A actualização pode desencadear o path activo, pelo que o push faz parte da instalação autorizada, não desta preparação.
|
||||
4. **Executor existente:** `fetch` do ramo, verificação de drift e capacidade; criar release nova sob `/var/www/investmoz-homologacao/releases`; usar base QA isolada e versões fixadas; executar Composer sem scripts, descoberta de pacotes, guarda da base, suite estrutural `phpunit.investmoz.xml`, `npm ci`, build, tipos, deployment guard, unitários e testes frontend. Depois instalar vendor de produção a partir do mesmo lock e conferir plataforma. Nenhum `artisan migrate` operacional está previsto.
|
||||
5. **Preparação da release:** validar ausência de `public/hot`; ligar os caminhos persistentes `.env`, `storage` e `public/storage` existentes; gerar o manifesto da candidata e aplicar apenas as permissões técnicas já previstas pelo executor aos directórios novos. Não alterar papéis/permissões de negócio nem configuração funcional.
|
||||
6. **Promoção:** reconfirmar SHA e ausência de drift, trocar `current` atomicamente e verificar manifesto/homepage; só depois reiniciar a fila pelo comando existente `queue:restart` e actualizar `previous.json`/`installed.json`. Estas escritas de runtime/cache devem constar do recibo, separadas dos dados de negócio. Não executar publicação, importações, agendamento ou ensaio de email.
|
||||
7. **Pós-verificação:** confirmar revisão/assets, serviços, logs, portas e preservação; conferir fecho público por todas as vias, acesso privado autorizado/recusado, continuidade no dossier e uploads com exemplares sintéticos autorizados; reconciliar cache/sessões e contagens/hash de dados protegidos. Registar quem fez a conferência desktop/móvel, revisão e resultado. Não concluir visual ou entrega de correio por inferência.
|
||||
|
||||
**Risco concreto do executor:** nas linhas 263–266, um avanço do ramo durante o build cria pedido para a revisão mais recente; nas linhas 290–298, os pedidos são processados seguindo `latest()`, não fixando o SHA nominal do ficheiro. Não se apresenta isto como uma recusa automática de revisões não autorizadas. É condição de utilização garantir a janela exclusiva e a coincidência do único `main` com a candidata autorizada; sem isso, a instalação fica bloqueada. A correcção do executor não foi iniciada nem autorizada nesta tarefa.
|
||||
|
||||
## Interrupção e reversão
|
||||
|
||||
Interromper antes de qualquer promoção se mudar a revisão/ramo, aparecer pedido concorrente ou drift, faltar espaço, divergir estrutura ou backup/restauro, falhar um teste/asset/guarda, houver proposta de migração/permissão/configuração não autorizada ou não for possível conservar publicação/notificações fechadas. Uma revisão nova de `main` invalida o âmbito concreto; não a aceitar como parte implícita da autorização.
|
||||
|
||||
O executor existente preserva a release servida durante o build e verifica drift antes da troca. Dispõe de reversão por `previous.json` e troca atómica, com health-check e reinício da fila; **nenhuma reversão foi executada no destino nesta tarefa**. A inspecção da sua implementação não equivale a ensaio de comutação na homologação.
|
||||
|
||||
- **Antes de qualquer uso das Ofertas:** o destino tem zero Ofertas; se isso se mantiver e não houver jobs/estados novos, uma reposição da release/configuração anterior compatível pode ser autorizada com os artefactos preservados, sem restaurar toda a base.
|
||||
- **Depois de utilização:** não regressar cegamente a `96f51096` ou F4: desconhecem parte dos estados/guardas novos. Uma retirada, expiração, denúncia, versão ou grant novo exige recuperação compatível que mantenha os bloqueios e históricos, preferindo correcção em frente. Não apagar dados ou jobs para acomodar código antigo.
|
||||
- **Incidente de dados:** preservar a actividade posterior e delimitar uma reconciliação/restauro próprio. Não substituir a base por este dump sem autorização adicional. Não usar migrações `down` nem rollback destrutivo de schema.
|
||||
|
||||
## Saldo e autorização necessária
|
||||
|
||||
| Dimensão | Estado |
|
||||
| --- | --- |
|
||||
| Integração técnica em QA e menus | Provas aceites preservadas; menus QA verificados; sem nova ronda geral |
|
||||
| Destino, estruturas, GD e capacidade | Conferidos, compatíveis para avaliação privada |
|
||||
| Backup actual e restauro isolado | Aprovados, com reconciliação da sessão nova e recuperação das chaves |
|
||||
| Instalação privada | **Não autorizada/não executada; APTO COM CONDIÇÕES** |
|
||||
| Condição operacional do Git | SHA autorizado e janela sem integrações concorrentes obrigatórios; caso contrário bloquear |
|
||||
| Publicação operacional | Bloqueada; não autorizada; publicador efectivo não implementado |
|
||||
| Visual/móvel, galeria e email real | Pendentes de conferência/ensaio específicos; sem utilizadores operacionais notificados |
|
||||
| NDA, comprovativos e avisos legais/validade | Decisões pendentes, mantidas fechadas nos comportamentos dependentes |
|
||||
|
||||
A decisão seguinte necessária é a autorização expressa de instalação privada do SHA escolhido, identificando se inclui os menus `3558647e`, mantendo publicação, NDA, notificações e agendamento nos estados acima e respeitando a janela exclusiva de Git. Esta verificação não concede essa autorização nem a solicita novamente para acções já executadas de backup/restauro.
|
||||
@@ -0,0 +1,68 @@
|
||||
set -eu
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import os,subprocess,json,hashlib,tarfile,time
|
||||
assert os.geteuid()==0 and os.environ.get('SSH_CONNECTION')
|
||||
deadline=time.monotonic()+90
|
||||
while subprocess.run(['systemctl','is-active','teste-investidor-deploy.service'],capture_output=True,text=True).stdout.strip() not in ['inactive','failed']:
|
||||
if time.monotonic()>deadline: raise RuntimeError('Existing deployment still active; no backup or schema change performed')
|
||||
time.sleep(3)
|
||||
assert json.loads(Path('/var/lib/investmoz-deploy/installed.json').read_text())['commit']=='96f510965f70581a4b41dd94463f598b77dd7cf5', 'Destination advanced; reconcile first'
|
||||
assert subprocess.run(['systemctl','is-active','teste-investidor-deploy.service'],capture_output=True,text=True).stdout.strip() in ['inactive','failed']
|
||||
root=Path('/var/www/html/teste-investidor.tempo.co.mz/current').resolve()
|
||||
stage=Path('/var/backups/investmoz-offers-preinstall-20261114/backup')
|
||||
assert not stage.exists(), 'Existing backup must be reconciled before repeating'
|
||||
assert __import__('shutil').disk_usage(root).free >= 5*1024**3, 'Capacity changed; active release preserved'
|
||||
stage.mkdir(mode=0o700,parents=True)
|
||||
php=r'''<?php
|
||||
require 'vendor/autoload.php';$app=require 'bootstrap/app.php';$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
use Illuminate\Support\Facades\DB;
|
||||
DB::beginTransaction(); DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY'); DB::statement("SET TIME ZONE 'UTC'");
|
||||
try {
|
||||
$identity=DB::selectOne('select current_database() as database,inet_server_port() as port');
|
||||
if($identity->database!=='teste_investidor'||(int)$identity->port!==5432)throw new RuntimeException('Wrong active source');
|
||||
$snapshot=DB::selectOne('select pg_export_snapshot() as id')->id;
|
||||
$tables=[];$sql="BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY; SET TRANSACTION SNAPSHOT '".$snapshot."'; SET TIME ZONE 'UTC';\n";
|
||||
foreach(DB::select("select schemaname,tablename from pg_tables where schemaname not in ('pg_catalog','information_schema') and schemaname not like 'pg_%' order by schemaname,tablename") as $t){
|
||||
$name=$t->schemaname.'.'.$t->tablename;$quoted='"'.str_replace('"','""',$t->schemaname).'"."'.str_replace('"','""',$t->tablename).'"';
|
||||
$sql.="select json_build_object('name','".$name."','rows',count(*),'checksum',md5(coalesce(string_agg(row_to_json(t)::text,E'\\n' order by md5(row_to_json(t)::text) COLLATE \"C\"),''))) from ".$quoted." t;\n";
|
||||
}
|
||||
$sql.='ROLLBACK;';$reader=new Symfony\Component\Process\Process(['runuser','-u','postgres','--','psql','-X','-q','-A','-t','-v','ON_ERROR_STOP=1','-d','teste_investidor']);$reader->setInput($sql)->setTimeout(90)->mustRun();
|
||||
foreach(explode("\n",trim($reader->getOutput())) as $line){$row=json_decode($line,true,512,JSON_THROW_ON_ERROR);$name=$row['name'];unset($row['name']);$tables[$name]=$row;}
|
||||
$process=new Symfony\Component\Process\Process(['runuser','-u','postgres','--','pg_dump','--format=custom','--no-owner','--no-privileges','--lock-wait-timeout=10000','--snapshot='.$snapshot,'teste_investidor']);
|
||||
$process->setTimeout(90)->mustRun();file_put_contents('/var/backups/investmoz-offers-preinstall-20261114/backup/database.dump',$process->getOutput());
|
||||
echo json_encode(['target'=>$identity,'tables'=>$tables,'consistent_exported_snapshot'=>true,'importer_fingerprint'=>app(App\Services\TerritoryGeometryImport::class)->fingerprint(),'scanner'=>['socket'=>config('services.malware.socket'),'host'=>config('services.malware.host'),'port'=>config('services.malware.port')]],JSON_THROW_ON_ERROR);
|
||||
}finally{DB::rollBack();}
|
||||
'''
|
||||
answer=json.loads(subprocess.check_output(['php8.4'],input=php.encode(),cwd=root))
|
||||
dump=stage/'database.dump';os.chmod(dump,0o600)
|
||||
listing=subprocess.check_output(['pg_restore','--list',str(dump)]).decode()
|
||||
assert 'TABLE DATA public geo_locations' in listing and 'TABLE DATA public geo_geometries' in listing
|
||||
storage=(root/'storage/app').resolve()
|
||||
manifest={}
|
||||
for p in sorted(storage.rglob('*')):
|
||||
if p.is_file() and not p.is_symlink():manifest['storage/app/'+str(p.relative_to(storage))]={'bytes':p.stat().st_size,'sha256':hashlib.sha256(p.read_bytes()).hexdigest()}
|
||||
env=(root/'.env').resolve();manifest['configuration/.env']={'bytes':env.stat().st_size,'sha256':hashlib.sha256(env.read_bytes()).hexdigest()}
|
||||
archive=stage/'files-before.tar.gz'
|
||||
with tarfile.open(archive,'w:gz') as tf:
|
||||
tf.add(storage,arcname='storage/app',recursive=True);tf.add(env,arcname='configuration/.env',recursive=False)
|
||||
os.chmod(archive,0o600)
|
||||
verified=0
|
||||
with tarfile.open(archive,'r:gz') as tf:
|
||||
for member in tf:
|
||||
if member.isfile():
|
||||
expected=manifest[member.name];f=tf.extractfile(member);digest=hashlib.sha256(f.read()).hexdigest()
|
||||
assert digest==expected['sha256'] and member.size==expected['bytes'];verified+=1
|
||||
assert verified==len(manifest)
|
||||
answer.update({'created_at':time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),'dump_path':str(dump),'sha256':hashlib.sha256(dump.read_bytes()).hexdigest(),'files_path':str(archive),'files_sha256':hashlib.sha256(archive.read_bytes()).hexdigest(),'files_verified':True,'file_count':verified,'file_manifest':manifest,'active_data_writes':0,'restore_verified':False,'root':str(root)})
|
||||
receipt=stage/'backup-receipt.json';receipt.write_text(json.dumps(answer,indent=2));os.chmod(receipt,0o600)
|
||||
assert json.loads(Path('/var/lib/investmoz-deploy/installed.json').read_text())['commit']=='96f510965f70581a4b41dd94463f598b77dd7cf5', 'Source revision changed during backup; do not install'
|
||||
print(json.dumps({'stage':str(stage),'created_at':answer['created_at'],'target':answer['target'],'tables':len(answer['tables']),'files_verified':verified,'dump_sha256':answer['sha256'],'files_sha256':answer['files_sha256'],'restore_verified':False}))
|
||||
|
||||
import shutil
|
||||
for name in ['delivery.json','schema-contract.json','ci-environment.json']:
|
||||
shutil.copy2('/etc/investmoz-deploy/'+name,stage/name)
|
||||
shutil.copy2('/var/lib/investmoz-deploy/installed.json',stage/'installed.json')
|
||||
shutil.copy2('/var/www/deploy/investmoz-pipeline.py',stage/'pipeline.py')
|
||||
|
||||
PY
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"stage": "/var/backups/investmoz-offers-preinstall-20261114/backup",
|
||||
"created_at": "2026-11-14T17:34:37Z",
|
||||
"target": {
|
||||
"database": "teste_investidor",
|
||||
"port": 5432
|
||||
},
|
||||
"tables": 219,
|
||||
"files_verified": 417,
|
||||
"dump_sha256": "662c5d9f1fe7e01a5ccf7529c8c8e8f27205907d60d2da75a580b7b6c37c252e",
|
||||
"files_sha256": "f438f31f212e516442526b609bf5ac02cfd2f630a6ac2a2b6fc12f1f88717a42",
|
||||
"restore_verified": false
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"environment": "local",
|
||||
"postgres_version": "17.11 (Debian 17.11-1.pgdg12+2)",
|
||||
"extensions": [
|
||||
{
|
||||
"extname": "postgis",
|
||||
"extversion": "3.6.4"
|
||||
},
|
||||
{
|
||||
"extname": "pgcrypto",
|
||||
"extversion": "1.3"
|
||||
}
|
||||
],
|
||||
"notification_constraints": [
|
||||
{
|
||||
"conname": "editorial_native_notice_unique"
|
||||
},
|
||||
{
|
||||
"conname": "portal_editorial_notice_subject"
|
||||
},
|
||||
{
|
||||
"conname": "portal_editorial_notifications_edition_id_foreign"
|
||||
},
|
||||
{
|
||||
"conname": "portal_editorial_notifications_native_event_id_foreign"
|
||||
},
|
||||
{
|
||||
"conname": "portal_editorial_notifications_opportunity_id_foreign"
|
||||
},
|
||||
{
|
||||
"conname": "portal_editorial_notifications_pkey"
|
||||
},
|
||||
{
|
||||
"conname": "portal_editorial_notifications_user_id_foreign"
|
||||
},
|
||||
{
|
||||
"conname": "portal_editorial_op_notice_uq"
|
||||
}
|
||||
],
|
||||
"notification_unique_index_present": true,
|
||||
"offer_code_present": {
|
||||
"preparation": false,
|
||||
"operations": false,
|
||||
"public_catalogue": false
|
||||
},
|
||||
"offer_notification_jobs": 0,
|
||||
"offer_notification_failures": 0,
|
||||
"operational_writes": 0
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"database_tables_rechecked": 219,
|
||||
"tables_changed_since_backup": [
|
||||
"public.sessions"
|
||||
],
|
||||
"release_files_rechecked": 9873,
|
||||
"release_files_changed": [],
|
||||
"installed_revision": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"active_data_writes": 0,
|
||||
"source_read_only_transactions": true
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
[
|
||||
{
|
||||
"file": "/var/www/deploy/investmoz-pipeline.py",
|
||||
"range": [
|
||||
190,
|
||||
213
|
||||
],
|
||||
"source": "190: (release/'storage'/directory).mkdir(parents=True,exist_ok=True)\n191: with self.qa_database():\n192: self.run(['composer','install','--no-interaction','--prefer-dist','--no-scripts'],cwd=release,env=env)\n193: self.run([self.c['php'],'artisan','package:discover','--ansi'],cwd=release,env=env)\n194: # The guard is verified against the actual PostgreSQL identity, not only environment strings.\n195: guard = \"require 'vendor/autoload.php'; $app=require 'bootstrap/app.php'; $app->make(Illuminate\\\\Contracts\\\\Console\\\\Kernel::class)->bootstrap(); if (!App\\\\Support\\\\IsolatedQaDatabase::allowsSchemaChanges()) exit(91);\"\n196: self.run([self.c['php'],'-r',guard],cwd=release,env=env)\n197: # The project intentionally gates later schemas behind nominal authenticated\n198: # installation. Reuse its accepted isolated baseline lifecycle test instead\n199: # of bypassing those guards with a generic migrate of every module.\n200: self.run([self.c['php'],'vendor/bin/phpunit','--configuration=phpunit.investmoz.xml','--do-not-cache-result'],cwd=release,env=env)\n201: self.run(['npm','ci','--no-audit','--no-fund'],cwd=release,env=env)\n202: self.run(['npm','run','build'],cwd=release,env=env)\n203: self.run(['npm','run','types:check'],cwd=release,env=env)\n204: self.run([self.c['php'],'tools/portal-diagnostics-deployment-guard.php'],cwd=release,env=env)\n205: # Unit fixtures mock DB/mail and select their own feature settings.\n206: # Do not inject the destination's enabled-module flags into those mocks.\n207: unit_env={k:v for k,v in env.items() if not k.startswith(('PORTAL_','INVESTMOZ_'))}\n208: self.run([self.c['php'],'vendor/bin/phpunit','--configuration=phpunit.xml','--testsuite','Unit','--do-not-cache-result'],cwd=release,env=unit_env)\n209: tests=sorted((release/'tests/frontend').glob('*.test.mjs'))\n210: self.run(['node','--test',*tests],cwd=release,env=env)\n211: # Final production vendor from the same lock, before linking any real configuration.\n212: self.run(['composer','install','--no-dev','--no-scripts','--no-interaction','--prefer-dist','--optimize-autoloader'],cwd=release,env=env)\n213: self.run(['composer','check-platform-reqs','--no-dev'],cwd=release,env=env)"
|
||||
},
|
||||
{
|
||||
"file": "/var/www/deploy/investmoz-pipeline.py",
|
||||
"range": [
|
||||
238,
|
||||
277
|
||||
],
|
||||
"source": "238: 'built_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),\n239: 'manifest_sha256':digest(release/'public/build/manifest.json')})\n240: for directory in [release/'bootstrap/cache']:\n241: os.chmod(directory,0o2770)\n242: \n243: def health(self, target: str) -> None:\n244: request=urllib.request.Request(self.c['target_url']+'/build/revision.json',headers={'User-Agent':'Mozilla/5.0 InvesteMoz-Delivery','Cache-Control':'no-cache'})\n245: with urllib.request.urlopen(request,timeout=20) as response:info=json.load(response)\n246: if info.get('commit')!=target:raise RuntimeError('Served commit differs from built commit')\n247: request=urllib.request.Request(self.c['target_url']+'/',headers={'User-Agent':'Mozilla/5.0 InvesteMoz-Delivery','Cache-Control':'no-cache'})\n248: with urllib.request.urlopen(request,timeout=20) as response:\n249: if response.status!=200 or response.headers.get('WWW-Authenticate'):raise RuntimeError('Public access regression')\n250: \n251: def deliver(self, target: str) -> dict:\n252: installed_file=self.state/'installed.json'\n253: installed=json.loads(installed_file.read_text())\n254: if installed.get('commit')==target:\n255: refuse_drift(self.current.resolve(),installed['hashes'])\n256: return {'status':'already_installed','commit':target}\n257: refuse_drift(self.current.resolve(),installed['hashes'])\n258: self.releases.mkdir(parents=True,exist_ok=True)\n259: if shutil.disk_usage(self.releases).free < 5 * 1024**3:\n260: raise RuntimeError('Insufficient free space for an isolated build; active release preserved')\n261: release=self.releases/(target+'-'+uuid.uuid4().hex[:8]);release.mkdir(mode=0o750)\n262: self.build(target,release)\n263: if self.latest()!=target:\n264: latest=self.git('rev-parse','refs/heads/delivery-target')\n265: write_json(self.state/'requests'/(latest+'.json'),{'commit':latest,'reason':'newer integration during build'})\n266: return {'status':'superseded','commit':target,'newer_commit':latest}\n267: # A programmer may have edited the served release while CI was running.\n268: # Recheck immediately before the atomic switch, preserving that work.\n269: refuse_drift(self.current.resolve(),installed['hashes'])\n270: previous=promote(self.current,release,lambda:self.health(target))\n271: try:\n272: self.run([self.c['php'],str(release/'artisan'),'queue:restart'],cwd=release)\n273: write_json(self.state/'previous.json',installed)\n274: info={'status':'installed','commit':target,'release':str(release),'previous_release':str(previous),\n275: 'hashes':tree_hashes(release),'database_migrations':0,'content_imports':0}\n276: write_json(installed_file,info)\n277: except Exception:"
|
||||
},
|
||||
{
|
||||
"file": "/var/www/deploy/investmoz-pipeline.py",
|
||||
"range": [
|
||||
287,
|
||||
308
|
||||
],
|
||||
"source": "287: fcntl.flock(lock,fcntl.LOCK_EX)\n288: succeeded=True\n289: for _ in range(3):\n290: requests=sorted((self.state/'requests').glob('*.json'))\n291: if not requests:return succeeded\n292: for path in requests:\n293: if not SHA.fullmatch(path.stem):raise RuntimeError('Unexpected queue entry')\n294: path.rename(self.state/'processed'/(path.stem+'-'+uuid.uuid4().hex[:8]+'.json'))\n295: target=None;report={}\n296: with (self.state/'logs'/(str(time.time_ns())+'.log')).open('wb') as self.log:\n297: try:\n298: target=self.latest();report=self.deliver(target)\n299: except Exception as error:\n300: succeeded=False\n301: report={'status':'failed','commit':target,'error':str(error),'active_release_preserved':True}\n302: finally:self.log=None\n303: report.update(requested_commits=[p.stem for p in requests],finished_at=datetime.datetime.now(datetime.timezone.utc).isoformat())\n304: write_json(self.state/'reports'/((target or 'fetch-failed')+'-'+str(time.time_ns())+'.json'),report)\n305: print(json.dumps(report),flush=True)\n306: return succeeded\n307: \n308: def rollback(self) -> dict:"
|
||||
},
|
||||
{
|
||||
"file": "/var/www/deploy/investmoz-pipeline.py",
|
||||
"range": [
|
||||
310,
|
||||
327
|
||||
],
|
||||
"source": "310: with (self.state/'delivery.lock').open('a') as lock:\n311: fcntl.flock(lock,fcntl.LOCK_EX)\n312: installed=json.loads((self.state/'installed.json').read_text())\n313: previous=json.loads((self.state/'previous.json').read_text())\n314: refuse_drift(self.current.resolve(),installed['hashes'])\n315: prior=Path(previous['release'])\n316: refuse_drift(prior,previous['hashes'])\n317: current=self.current.resolve()\n318: promote(self.current,prior,lambda:self.health(previous['commit']))\n319: try:\n320: self.run([self.c['php'],str(prior/'artisan'),'queue:restart'],cwd=prior)\n321: write_json(self.state/'installed.json',previous)\n322: write_json(self.state/'previous.json',installed)\n323: except Exception:\n324: point_current(self.current,current)\n325: write_json(self.state/'installed.json',installed)\n326: raise\n327: report={'status':'rolled_back','commit':previous['commit'],'release':str(prior),"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,152 @@
|
||||
[
|
||||
{
|
||||
"executor_entrypoints": [
|
||||
"/var/www/deploy/investmoz-pipeline.py",
|
||||
"/etc/investmoz-deploy/delivery.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "/var/www/deploy/investmoz-pipeline.py",
|
||||
"operations": [
|
||||
{
|
||||
"line": 4,
|
||||
"code": "Runs as the existing deployment OS user. Never resets the served checkout,"
|
||||
},
|
||||
{
|
||||
"line": 33,
|
||||
"code": "folders[:] = [name for name in folders if not (parent/name).is_symlink()"
|
||||
},
|
||||
{
|
||||
"line": 38,
|
||||
"code": "if path.is_symlink() or rel=='.env' or rel.startswith('public/build/assets/'):continue"
|
||||
},
|
||||
{
|
||||
"line": 55,
|
||||
"code": "if item.name == '.env' or item.name == 'public/hot' or (item.isfile()"
|
||||
},
|
||||
{
|
||||
"line": 72,
|
||||
"code": "if link.exists() and not link.is_symlink():"
|
||||
},
|
||||
{
|
||||
"line": 73,
|
||||
"code": "raise RuntimeError('Current path is not a symlink; existing directory preserved')"
|
||||
},
|
||||
{
|
||||
"line": 75,
|
||||
"code": "temporary.symlink_to(target, target_is_directory=True)"
|
||||
},
|
||||
{
|
||||
"line": 79,
|
||||
"code": "previous = link.resolve() if link.is_symlink() else None"
|
||||
},
|
||||
{
|
||||
"line": 121,
|
||||
"code": "self.git('fetch', '--no-tags', 'origin', '+refs/heads/'+branch+':refs/heads/delivery-target')"
|
||||
},
|
||||
{
|
||||
"line": 124,
|
||||
"code": "raise RuntimeError('Invalid fetched revision')"
|
||||
},
|
||||
{
|
||||
"line": 185,
|
||||
"code": "if not php.exists(): php.symlink_to(self.c['php'])"
|
||||
},
|
||||
{
|
||||
"line": 199,
|
||||
"code": "# of bypassing those guards with a generic migrate of every module."
|
||||
},
|
||||
{
|
||||
"line": 201,
|
||||
"code": "self.run(['npm','ci','--no-audit','--no-fund'],cwd=release,env=env)"
|
||||
},
|
||||
{
|
||||
"line": 202,
|
||||
"code": "self.run(['npm','run','build'],cwd=release,env=env)"
|
||||
},
|
||||
{
|
||||
"line": 203,
|
||||
"code": "self.run(['npm','run','types:check'],cwd=release,env=env)"
|
||||
},
|
||||
{
|
||||
"line": 214,
|
||||
"code": "if (release/'public/hot').exists(): raise RuntimeError('Development Vite marker must not ship')"
|
||||
},
|
||||
{
|
||||
"line": 225,
|
||||
"code": "(release/'storage').symlink_to(self.legacy/'storage',target_is_directory=True)"
|
||||
},
|
||||
{
|
||||
"line": 226,
|
||||
"code": "(release/'.env').symlink_to(self.legacy/'.env')"
|
||||
},
|
||||
{
|
||||
"line": 228,
|
||||
"code": "(release/'public/storage').symlink_to(self.legacy/'storage/app/public',target_is_directory=True)"
|
||||
},
|
||||
{
|
||||
"line": 252,
|
||||
"code": "installed_file=self.state/'installed.json'"
|
||||
},
|
||||
{
|
||||
"line": 265,
|
||||
"code": "write_json(self.state/'requests'/(latest+'.json'),{'commit':latest,'reason':'newer integration during build'})"
|
||||
},
|
||||
{
|
||||
"line": 272,
|
||||
"code": "self.run([self.c['php'],str(release/'artisan'),'queue:restart'],cwd=release)"
|
||||
},
|
||||
{
|
||||
"line": 287,
|
||||
"code": "fcntl.flock(lock,fcntl.LOCK_EX)"
|
||||
},
|
||||
{
|
||||
"line": 290,
|
||||
"code": "requests=sorted((self.state/'requests').glob('*.json'))"
|
||||
},
|
||||
{
|
||||
"line": 291,
|
||||
"code": "if not requests:return succeeded"
|
||||
},
|
||||
{
|
||||
"line": 292,
|
||||
"code": "for path in requests:"
|
||||
},
|
||||
{
|
||||
"line": 303,
|
||||
"code": "report.update(requested_commits=[p.stem for p in requests],finished_at=datetime.datetime.now(datetime.timezone.utc).isoformat())"
|
||||
},
|
||||
{
|
||||
"line": 304,
|
||||
"code": "write_json(self.state/'reports'/((target or 'fetch-failed')+'-'+str(time.time_ns())+'.json'),report)"
|
||||
},
|
||||
{
|
||||
"line": 311,
|
||||
"code": "fcntl.flock(lock,fcntl.LOCK_EX)"
|
||||
},
|
||||
{
|
||||
"line": 312,
|
||||
"code": "installed=json.loads((self.state/'installed.json').read_text())"
|
||||
},
|
||||
{
|
||||
"line": 320,
|
||||
"code": "self.run([self.c['php'],str(prior/'artisan'),'queue:restart'],cwd=prior)"
|
||||
},
|
||||
{
|
||||
"line": 321,
|
||||
"code": "write_json(self.state/'installed.json',previous)"
|
||||
},
|
||||
{
|
||||
"line": 325,
|
||||
"code": "write_json(self.state/'installed.json',installed)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "/etc/investmoz-deploy/delivery.json",
|
||||
"operations": []
|
||||
},
|
||||
{
|
||||
"pending_request_count": 0
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
set -eu
|
||||
python3 - <<'REMOTE'
|
||||
import json,pathlib,subprocess,os
|
||||
assert os.environ.get('SSH_CONNECTION')
|
||||
root=pathlib.Path('/var/www/html/teste-investidor.tempo.co.mz/current').resolve()
|
||||
php=r"""<?php
|
||||
require 'vendor/autoload.php';$app=require 'bootstrap/app.php';$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
use Illuminate\Support\Facades\DB;use Illuminate\Support\Facades\Schema;
|
||||
DB::beginTransaction();DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
||||
try{
|
||||
$id=DB::selectOne('select current_database() as database,inet_server_port() as port');if($id->database!=='teste_investidor'||(int)$id->port!==5432)throw new RuntimeException('Wrong source');
|
||||
$tables=['opportunities'=>['management_version','workflow_context','owner_user_id','organization_id','management_draft'], 'users'=>['portal_access_revision'],'portal_role_assignments'=>['role','opportunity_types'],'portal_superuser_pair'=>[], 'portal_profile_requests'=>[], 'portal_security_approvals'=>[], 'opportunity_images'=>[], 'opportunity_documents'=>[], 'opportunity_locations'=>[], 'documents'=>['storage_encryption','malware_scan_status','malware_scanned_sha256'],'portal_editorial_editions'=>[], 'portal_editorial_notifications'=>['opportunity_id','opportunity_version','content_hash'],'audit_events'=>[], 'tasks'=>[], 'desk_cases'=>['payload','simulation_id'], 'investor_navigation_preferences'=>[], 'simulation_form_drafts'=>[]];
|
||||
$schema=[];foreach($tables as $table=>$columns){$exists=Schema::hasTable($table);$schema[$table]=['exists'=>$exists,'missing_columns'=>$exists?array_values(array_filter($columns,fn($c)=>!Schema::hasColumn($table,$c))):$columns];}
|
||||
$offer=Schema::hasColumn('opportunities','workflow_context')?DB::selectOne("select count(*) filter(where workflow_context->'private_offer_preparation' is not null) as private_offers,count(*) filter(where workflow_context->'private_offer_preparation' is not null and (status='published' or published_at is not null)) as unexpected_published,count(*) filter(where workflow_context->'offer_publication' is not null) as stored_projections from opportunities"):null;
|
||||
$routes=[];foreach($app->make('router')->getRoutes() as $route){if(str_contains($route->uri(),'ofertas')||str_contains($route->uri(),'offers'))$routes[]=['uri'=>$route->uri(),'methods'=>$route->methods(),'action'=>$route->getActionName()];}
|
||||
$recipients=config('portal.notification_recipients',[]);sort($recipients);
|
||||
echo json_encode(['database'=>$id,'php'=>PHP_VERSION,'gd'=>extension_loaded('gd'),'imagecreatefromstring'=>function_exists('imagecreatefromstring'),'imagewebp'=>function_exists('imagewebp'),'schema'=>$schema,'offers'=>$offer,'offer_routes'=>$routes,'flags'=>['profile_access'=>(bool)config('portal.profile_access_enabled'),'editorial'=>(bool)config('editorial_workflow.enabled'),'notifications'=>(bool)config('portal.notifications_enabled'),'homologation_only'=>(bool)config('portal.homologation_only'),'homologation_access_mode'=>config('portal.homologation_access_mode'),'nda_present'=>config('private_offers.nda')!==null,'expiry_scheduler'=>(bool)config('private_offers.expiry_scheduler_enabled',false)],'mail'=>['mailer'=>config('mail.default'),'smtp_host_configured'=>(bool)config('mail.mailers.smtp.host'),'recipient_count'=>count($recipients),'recipient_set_sha256'=>hash('sha256',json_encode($recipients))],'runtime'=>['queue'=>config('queue.default'),'private_disk'=>config('filesystems.default'),'scanner_host_configured'=>(bool)config('services.malware.host'),'scanner_socket_configured'=>(bool)config('services.malware.socket')],'migration_count'=>DB::table('migrations')->count(),'config_cached'=>$app->configurationIsCached(),'operational_data_writes'=>0],JSON_THROW_ON_ERROR);
|
||||
}finally{DB::rollBack();}
|
||||
"""
|
||||
q=json.loads(subprocess.check_output(['php8.4'],input=php.encode(),cwd=root,timeout=90))
|
||||
info=subprocess.run(['php-fpm8.4','-i'],capture_output=True,text=True,timeout=20).stdout
|
||||
q['fpm_image_capabilities']={'gd_enabled':'GD Support => enabled' in info,'webp_enabled':'WebP Support => enabled' in info}
|
||||
units=subprocess.run(['systemctl','list-units','--type=service','--all','--plain','--no-legend'],capture_output=True,text=True,timeout=15).stdout.splitlines()
|
||||
q['service_units']=[' '.join(line.split()[:4]) for line in units if any(x in line.lower() for x in ['teste-investidor','investmoz','nginx','php8.4','postgresql','clamav'])]
|
||||
config=json.loads(pathlib.Path('/etc/investmoz-deploy/delivery.json').read_text());q['restore_image']=config.get('postgis_image');q['executor_config_keys']=sorted(config)
|
||||
installed=json.loads(pathlib.Path('/var/lib/investmoz-deploy/installed.json').read_text());q['installed_revision']=installed.get('commit')
|
||||
rev=root/'public/build/revision.json';q['served_build_revision']=json.loads(rev.read_text()).get('commit') if rev.exists() else None
|
||||
q['executor_service_state']=subprocess.run(['systemctl','is-active','teste-investidor-deploy.service'],capture_output=True,text=True).stdout.strip()
|
||||
print(json.dumps(q))
|
||||
REMOTE
|
||||
@@ -0,0 +1,15 @@
|
||||
set -eu
|
||||
python3 - <<'REMOTE'
|
||||
from pathlib import Path
|
||||
import subprocess,json,hashlib,os
|
||||
assert os.environ.get('SSH_CONNECTION')
|
||||
root=Path('/var/www/html/teste-investidor.tempo.co.mz/current').resolve()
|
||||
stage=Path('/var/backups/investmoz-offers-preinstall-20261114/backup')
|
||||
php='<?php\nrequire \'vendor/autoload.php\';$app=require \'bootstrap/app.php\';$app->make(Illuminate\\Contracts\\Console\\Kernel::class)->bootstrap();\nuse Illuminate\\Support\\Facades\\DB;\nDB::beginTransaction(); DB::statement(\'SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY\'); DB::statement("SET TIME ZONE \'UTC\'");\ntry {\n $identity=DB::selectOne(\'select current_database() as database,inet_server_port() as port\');\n if($identity->database!==\'teste_investidor\'||(int)$identity->port!==5432)throw new RuntimeException(\'Wrong active source\');\n $snapshot=DB::selectOne(\'select pg_export_snapshot() as id\')->id;\n $tables=[];$sql="BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY; SET TRANSACTION SNAPSHOT \'".$snapshot."\'; SET TIME ZONE \'UTC\';\\n";\n foreach(DB::select("select schemaname,tablename from pg_tables where schemaname not in (\'pg_catalog\',\'information_schema\') and schemaname not like \'pg_%\' order by schemaname,tablename") as $t){\n $name=$t->schemaname.\'.\'.$t->tablename;$quoted=\'"\'.str_replace(\'"\',\'""\',$t->schemaname).\'"."\'.str_replace(\'"\',\'""\',$t->tablename).\'"\';\n $sql.="select json_build_object(\'name\',\'".$name."\',\'rows\',count(*),\'checksum\',md5(coalesce(string_agg(row_to_json(t)::text,E\'\\\\n\' order by md5(row_to_json(t)::text) COLLATE \\"C\\"),\'\'))) from ".$quoted." t;\\n";\n }\n $sql.=\'ROLLBACK;\';$reader=new Symfony\\Component\\Process\\Process([\'runuser\',\'-u\',\'postgres\',\'--\',\'psql\',\'-X\',\'-q\',\'-A\',\'-t\',\'-v\',\'ON_ERROR_STOP=1\',\'-d\',\'teste_investidor\']);$reader->setInput($sql)->setTimeout(90)->mustRun();\n foreach(explode("\\n",trim($reader->getOutput())) as $line){$row=json_decode($line,true,512,JSON_THROW_ON_ERROR);$name=$row[\'name\'];unset($row[\'name\']);$tables[$name]=$row;}\n echo json_encode([\'target\'=>$identity,\'tables\'=>$tables],JSON_THROW_ON_ERROR);\n}finally{DB::rollBack();}\n'
|
||||
actual=json.loads(subprocess.check_output(['php8.4'],input=php.encode(),cwd=root,timeout=120))
|
||||
receipt=json.loads((stage/'backup-receipt.json').read_text())
|
||||
changes=[name for name in set(receipt['tables'])|set(actual['tables']) if receipt['tables'].get(name)!=actual['tables'].get(name)]
|
||||
files=json.loads((stage/'release-files-private.json').read_text());code_changes=[name for name,want in files.items() if not (root/name).is_file() or hashlib.sha256((root/name).read_bytes()).hexdigest()!=want]
|
||||
proof={'database_tables_rechecked':len(actual['tables']),'tables_changed_since_backup':sorted(changes),'release_files_rechecked':len(files),'release_files_changed':code_changes,'installed_revision':json.loads(Path('/var/lib/investmoz-deploy/installed.json').read_text())['commit'],'active_data_writes':0,'source_read_only_transactions':True}
|
||||
(stage/'preservation-receipt.json').write_text(json.dumps(proof,indent=2));print(json.dumps(proof))
|
||||
REMOTE
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"verdict": "APTO COM CONDIÇÕES",
|
||||
"scope": "controlled private homologation; no operational publication",
|
||||
"installed_revision": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"accepted_integration_candidate": "9a014268",
|
||||
"candidate_including_preview_menu_delta": "3558647e8d82092cd0e31eefbe8b9e5e9eacf573",
|
||||
"installation_authorized": false,
|
||||
"installation_performed": false,
|
||||
"operational_migrations": false,
|
||||
"operational_data_or_permissions_or_configuration_changes": false,
|
||||
"real_email_sent": false,
|
||||
"backup": {
|
||||
"stage": "/var/backups/investmoz-offers-preinstall-20261114/backup",
|
||||
"created_at": "2026-11-14T17:34:37Z",
|
||||
"target": {
|
||||
"database": "teste_investidor",
|
||||
"port": 5432
|
||||
},
|
||||
"tables": 219,
|
||||
"files_verified": 417,
|
||||
"dump_sha256": "662c5d9f1fe7e01a5ccf7529c8c8e8f27205907d60d2da75a580b7b6c37c252e",
|
||||
"files_sha256": "f438f31f212e516442526b609bf5ac02cfd2f630a6ac2a2b6fc12f1f88717a42",
|
||||
"restore_verified": false
|
||||
},
|
||||
"restore": {
|
||||
"restore_verified": true,
|
||||
"all_table_counts_and_checksums_match": true,
|
||||
"tables": 219,
|
||||
"isolated_network": "none",
|
||||
"isolated_database": "offers_preinstall_restore_20261114_test",
|
||||
"isolated_port": 55441,
|
||||
"schema_changes": 0,
|
||||
"operational_data_writes": 0,
|
||||
"files_restored_and_verified": 417,
|
||||
"encryption": {
|
||||
"encrypted_originals_recovered": 0,
|
||||
"mfa_envelopes_recovered": 2,
|
||||
"preserved_keys_verified": true
|
||||
}
|
||||
},
|
||||
"dependencies_missing": [],
|
||||
"gd_cli_and_fpm_available": true,
|
||||
"in_memory_codec_passed": true,
|
||||
"destination_offer_routes": [],
|
||||
"destination_offer_counts": {
|
||||
"private_offers": 0,
|
||||
"unexpected_published": 0,
|
||||
"stored_projections": 0
|
||||
},
|
||||
"notifications_enabled": false,
|
||||
"authorized_email_recipient_count": 0,
|
||||
"condition_for_exact_revision": "Existing executor follows latest main, not requested SHA. Exclusive integration window with only the expressly authorised revision is required; otherwise do not enqueue/push. No executor changes were made.",
|
||||
"remaining_conditions": [
|
||||
"explicit installation authorisation naming candidate and menu delta",
|
||||
"recheck installed revision, main head, empty deploy queue and current backup before actual installation",
|
||||
"preserve operational publication/NDA/scheduler closure and notifications disabled",
|
||||
"desktop/mobile authenticated acceptance pending",
|
||||
"real controlled mailbox not designated; no real-email test yet",
|
||||
"NDA, property-proof and legal-publication decisions pending"
|
||||
],
|
||||
"guarded_restore": "new container, pinned existing image, network none, no published port, removed after verification",
|
||||
"evidence_sha256": {
|
||||
"backup-reviewed.sh": "ddea0084d9cc2e593e4b2836d27a39fac22f5bfa20e9432545de48d56c5a9a71",
|
||||
"backup-summary.json": "ca1f9fd3aebbf26059a9b015e71a3864172a3fe1e479b3ea57d97a6e3807167c",
|
||||
"constraints-automatics.json": "5b3a1b979c7fd06d4f745ed3814115ba4727982ea0b7e321226499bdfcda163f",
|
||||
"data-preservation.json": "a27f55d2e75ef8f14981d126482cb1d33b24a7132400f22f914d8dc51f5f389f",
|
||||
"executor-operations.json": "448d1abbe9ed6c44b775a09dd7198e1915bd4cd388e241213221d96b5cac96bb",
|
||||
"executor-status.json": "3cac96822ff3a24d135ba12dca93ba2203f6b308f3e465ba38600d7045899efc",
|
||||
"preflight-reviewed.sh": "7a0c701cfa27345268475594604f0d9db19037358e4a55cd6b6151b8b34cc199",
|
||||
"preservation-reviewed.sh": "d735500fb90581a73839478f1e3642584470a79368f69a8bebc9b251853b30d1",
|
||||
"restore-reviewed.sh": "4b2f16350f65b01e9105c5f0a31bad7b4bcaaf92cf67fb296903ca55c1f9b6ad",
|
||||
"restore-summary.json": "c9e96c7fd4eb94b02f9d80508970ceb5413510910ad86e37d12f3edeb928b82f",
|
||||
"security-runtime.json": "564da2cd07c0937687bb579c2e91395b4f598af2f3e765ca9122f39f495df7f2",
|
||||
"session-reconciliation.json": "caa6dc326d1e0dfc6d0c594ea96e935f9303d39b1fb6b8871ef97fe9ca4c2008",
|
||||
"target-final-preservation.json": "e6e6daccdef1568b93f889312d76c5d651aec3a3b85231624ac832385a0a4d35",
|
||||
"target-identity.json": "b276128c113ab7b34a6c9ab8f08f5a39a5d8d710f94dbd6e597a3283e6877eb2",
|
||||
"target-runtime-schema.json": "c6fce61c40a791dae53c625592b6d8f8b9457ca8a6f0de99bf1c2c9b5a6dc876"
|
||||
},
|
||||
"preservation": {
|
||||
"unchanged_tables": 218,
|
||||
"concurrent_new_sessions": 1,
|
||||
"prior_sessions_unchanged": 51,
|
||||
"release_files_unchanged": 9873
|
||||
},
|
||||
"evidence_hash_encoding": "SHA256 of UTF-8 text with Git LF normalisation"
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
set -eu
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import subprocess,json,time,uuid,os
|
||||
assert os.environ.get('SSH_CONNECTION')
|
||||
stage=Path('/var/backups/investmoz-offers-preinstall-20261114/backup')
|
||||
receipt=json.loads((stage/'backup-receipt.json').read_text())
|
||||
config=json.loads((stage/'delivery.json').read_text())
|
||||
name='investmoz-preservation-restore-'+uuid.uuid4().hex[:10]
|
||||
def run(args,**kw):return subprocess.check_output(args,stderr=subprocess.STDOUT,**kw)
|
||||
run(['docker','image','inspect',config['postgis_image']]) # Reuse an existing pinned image; never pull software.
|
||||
cid=run(['docker','run','-d','--name',name,'--label','investmoz.task=offers-preinstall-20261114','--network','none','--memory','768m','--env','POSTGRES_HOST_AUTH_METHOD=trust','--env','POSTGRES_DB=offers_preinstall_restore_20261114_test',config['postgis_image'],'postgres','-p','55441']).decode().strip()
|
||||
assert len(cid)==64
|
||||
try:
|
||||
for _ in range(40):
|
||||
if subprocess.run(['docker','exec',cid,'pg_isready','-p','55441','-U','postgres'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL).returncode==0:break
|
||||
time.sleep(1)
|
||||
else:raise RuntimeError('Restore database not ready')
|
||||
def sql(text):return run(['docker','exec','-i',cid,'psql','-p','55441','-U','postgres','-d','offers_preinstall_restore_20261114_test','-X','-q','-A','-t','-v','ON_ERROR_STOP=1'],input=text.encode()).decode()
|
||||
assert sql('select current_database(),inet_server_port(),current_setting(\'port\');').strip()=='offers_preinstall_restore_20261114_test||55441'
|
||||
# Preserve policy role names, without copying credentials or grants into the test instance.
|
||||
roles=run(['runuser','-u','postgres','--','psql','-XAt','-d','teste_investidor','-c',"select rolname from pg_roles where rolname !~ '^pg_' and rolname <> 'postgres'"]).decode().splitlines()
|
||||
for role in roles:sql('CREATE ROLE "'+role.replace('"','""')+'" NOLOGIN;')
|
||||
run(['docker','cp',str(stage/'database.dump'),cid+':/tmp/database.dump'])
|
||||
run(['docker','exec',cid,'pg_restore','-p','55441','-U','postgres','-d','offers_preinstall_restore_20261114_test','--no-owner','--no-privileges','--exit-on-error','/tmp/database.dump'])
|
||||
text="SET TIME ZONE 'UTC';\n"
|
||||
for table in receipt['tables']:
|
||||
quoted='.'.join('"'+part.replace('"','""')+'"' for part in table.split('.',1))
|
||||
text+="select json_build_object('name','"+table+"','rows',count(*),'checksum',md5(coalesce(string_agg(row_to_json(t)::text,E'\\n' order by md5(row_to_json(t)::text) COLLATE \"C\"),''))) from "+quoted+" t;\n"
|
||||
restored={}
|
||||
for line in sql(text).splitlines():
|
||||
row=json.loads(line);table=row.pop('name');restored[table]=row
|
||||
assert restored==receipt['tables'], 'Restore rows/checksums differ'
|
||||
|
||||
# Extract regular files only into a new private directory, never into the active release.
|
||||
import tarfile,hashlib,shutil
|
||||
files=stage/'restore-files-rehearsal'
|
||||
files.mkdir(mode=0o700)
|
||||
recovered=0
|
||||
with tarfile.open(stage/'files-before.tar.gz','r:gz') as archive:
|
||||
for member in archive:
|
||||
if not member.isfile():continue
|
||||
part=Path(member.name)
|
||||
assert not part.is_absolute() and '..' not in part.parts
|
||||
assert member.name in receipt['file_manifest']
|
||||
destination=files/part;destination.parent.mkdir(mode=0o700,parents=True,exist_ok=True)
|
||||
with archive.extractfile(member) as source, destination.open('xb') as output:shutil.copyfileobj(source,output)
|
||||
expected=receipt['file_manifest'][member.name]
|
||||
assert destination.stat().st_size==expected['bytes'] and hashlib.sha256(destination.read_bytes()).hexdigest()==expected['sha256']
|
||||
recovered+=1
|
||||
assert recovered==receipt['file_count']
|
||||
documents=json.loads(sql("select coalesce(json_agg(row_to_json(d)),'[]'::json) from (select storage_key,storage_encryption,sha256 from documents where deleted_at is null and storage_encryption='laravel' and storage_key is not null) d;").strip())
|
||||
mfa=json.loads(sql("select coalesce(json_agg(row_to_json(u)),'[]'::json) from (select two_factor_secret,two_factor_recovery_codes from users where two_factor_secret is not null) u;").strip())
|
||||
(files/'encrypted-records.json').write_text(json.dumps({'documents':documents,'mfa':mfa}))
|
||||
root=Path('/var/www/html/teste-investidor.tempo.co.mz/current').resolve()
|
||||
php=r"""<?php
|
||||
require 'vendor/autoload.php';$app=require 'bootstrap/app.php';$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
$files='/var/backups/investmoz-offers-preinstall-20261114/backup/restore-files-rehearsal';
|
||||
$env=Dotenv\Dotenv::parse(file_get_contents($files.'/configuration/.env'));
|
||||
$keys=array_values(array_unique(array_filter([$env['APP_KEY']??null,...explode(',',$env['APP_PREVIOUS_KEYS']??'')])));
|
||||
$encrypters=array_map(fn($key)=>new Illuminate\Encryption\Encrypter(str_starts_with($key,'base64:')?base64_decode(substr($key,7),true):$key,config('app.cipher')),$keys);
|
||||
if(!$encrypters)throw new RuntimeException('Preserved encryption configuration missing');
|
||||
$decrypt=function($bytes)use($encrypters){foreach($encrypters as $encrypter){try{return $encrypter->decryptString($bytes);}catch(Throwable $error){}}throw new RuntimeException('Preserved envelope cannot be recovered');};
|
||||
$records=json_decode(file_get_contents($files.'/encrypted-records.json'),true,512,JSON_THROW_ON_ERROR);
|
||||
$source=realpath(base_path('storage/app'));$disk=realpath(config('filesystems.disks.local.root'));
|
||||
if(!$source||!$disk||!str_starts_with($disk,$source))throw new RuntimeException('Unexpected private disk mapping');
|
||||
$relative=ltrim(substr($disk,strlen($source)),'/');$count=0;$mfa=0;
|
||||
foreach($records['documents'] as $doc){$key=$doc['storage_key'];if(str_contains($key,'..')||str_starts_with($key,'/'))throw new RuntimeException('Unsafe storage key');$path=realpath($files.'/storage/app/'.$relative.'/'.$key);if(!$path||!str_starts_with($path,$files.'/storage/app/'))throw new RuntimeException('Missing recovered original');$bytes=$decrypt(file_get_contents($path));if(!hash_equals(trim($doc['sha256']),hash('sha256',$bytes)))throw new RuntimeException('Recovered original hash mismatch');$count++;}
|
||||
foreach($records['mfa'] as $row){foreach(['two_factor_secret','two_factor_recovery_codes'] as $field){if(!empty($row[$field]))$decrypt($row[$field]);}$mfa++;}
|
||||
echo json_encode(['encrypted_originals_recovered'=>$count,'mfa_envelopes_recovered'=>$mfa,'preserved_keys_verified'=>true],JSON_THROW_ON_ERROR);
|
||||
"""
|
||||
envelope=json.loads(subprocess.check_output(['php8.4'],input=php.encode(),cwd=root))
|
||||
proof={'restore_verified':True,'all_table_counts_and_checksums_match':True,'tables':len(restored),'isolated_network':'none','isolated_database':'offers_preinstall_restore_20261114_test','isolated_port':55441,'schema_changes':0,'operational_data_writes':0,'files_restored_and_verified':recovered,'encryption':envelope}
|
||||
(stage/'restore-receipt.json').write_text(json.dumps(proof,indent=2))
|
||||
print(json.dumps(proof))
|
||||
finally:
|
||||
details=json.loads(run(['docker','inspect',cid]))[0]
|
||||
assert details['Config']['Labels'].get('investmoz.task')=='offers-preinstall-20261114' and details['HostConfig']['NetworkMode']=='none'
|
||||
run(['docker','rm','-f','-v',cid])
|
||||
PY
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"restore_verified": true,
|
||||
"all_table_counts_and_checksums_match": true,
|
||||
"tables": 219,
|
||||
"isolated_network": "none",
|
||||
"isolated_database": "offers_preinstall_restore_20261114_test",
|
||||
"isolated_port": 55441,
|
||||
"schema_changes": 0,
|
||||
"operational_data_writes": 0,
|
||||
"files_restored_and_verified": 417,
|
||||
"encryption": {
|
||||
"encrypted_originals_recovered": 0,
|
||||
"mfa_envelopes_recovered": 2,
|
||||
"preserved_keys_verified": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"environment": "local",
|
||||
"debug": false,
|
||||
"session_secure": true,
|
||||
"session_http_only": true,
|
||||
"session_same_site": "lax",
|
||||
"app_url": "https://teste-investidor.tempo.co.mz",
|
||||
"fpm_environment_overrides": [],
|
||||
"nginx_app_environment_overrides": []
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"prior_sessions": 51,
|
||||
"current_sessions": 52,
|
||||
"prior_identifiers_preserved": true,
|
||||
"removed_prior_sessions": 0,
|
||||
"new_sessions": 1,
|
||||
"changed_fields_counts": {},
|
||||
"operational_data_writes": 0,
|
||||
"conclusion": "Live session activity observed during a read-only verification; no session content or identifiers exported."
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"installed_revision_unchanged": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"preexisting_files_changed": [],
|
||||
"release_archive_sha256": "914bc32627a51ec9001d07e5ac31d5717719a4a7f93e43dc26189865962735ca",
|
||||
"release_regular_files_verified": 9873,
|
||||
"release_archive_bytes": 25862122,
|
||||
"backup_directory_mode": "0o700",
|
||||
"codec": {
|
||||
"in_memory_codec_passed": true,
|
||||
"original_files_touched": 0,
|
||||
"network_requests": 0
|
||||
},
|
||||
"executor": {
|
||||
"branch": "main",
|
||||
"current_link": "/var/www/html/teste-investidor.tempo.co.mz/current",
|
||||
"legacy_root": "/var/www/html/teste-investidor.tempo.co.mz",
|
||||
"php": "/usr/bin/php8.4",
|
||||
"release_root": "/var/www/investmoz-homologacao/releases",
|
||||
"repository": "nala/Investidor-V2",
|
||||
"retention": "No automatic deletion of releases or data",
|
||||
"schema_contract": "/etc/investmoz-deploy/schema-contract.json",
|
||||
"state_root": "/var/lib/investmoz-deploy",
|
||||
"target_url": "https://teste-investidor.tempo.co.mz"
|
||||
},
|
||||
"deploy_service": "inactive",
|
||||
"deploy_path": "active",
|
||||
"operational_writes": 0,
|
||||
"services_reconfigured": false
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"root": "/var/www/investmoz-homologacao/releases/96f510965f70581a4b41dd94463f598b77dd7cf5-8ee72062",
|
||||
"git_revision": "",
|
||||
"working_tree": [],
|
||||
"installed_revision": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"services": {
|
||||
"nginx": "active",
|
||||
"php8.4-fpm": "active",
|
||||
"teste-investidor-deploy.service": "inactive",
|
||||
"teste-investidor-deploy.path": "active"
|
||||
},
|
||||
"disk_free_bytes": 24556748800,
|
||||
"read_only": true
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"database": {
|
||||
"database": "teste_investidor",
|
||||
"port": 5432
|
||||
},
|
||||
"php": "8.4.27",
|
||||
"gd": true,
|
||||
"imagecreatefromstring": true,
|
||||
"imagewebp": true,
|
||||
"schema": {
|
||||
"opportunities": {
|
||||
"exists": true,
|
||||
"missing_columns": []
|
||||
},
|
||||
"users": {
|
||||
"exists": true,
|
||||
"missing_columns": []
|
||||
},
|
||||
"portal_role_assignments": {
|
||||
"exists": true,
|
||||
"missing_columns": []
|
||||
},
|
||||
"portal_superuser_pair": {
|
||||
"exists": true,
|
||||
"missing_columns": []
|
||||
},
|
||||
"portal_profile_requests": {
|
||||
"exists": true,
|
||||
"missing_columns": []
|
||||
},
|
||||
"portal_security_approvals": {
|
||||
"exists": true,
|
||||
"missing_columns": []
|
||||
},
|
||||
"opportunity_images": {
|
||||
"exists": true,
|
||||
"missing_columns": []
|
||||
},
|
||||
"opportunity_documents": {
|
||||
"exists": true,
|
||||
"missing_columns": []
|
||||
},
|
||||
"opportunity_locations": {
|
||||
"exists": true,
|
||||
"missing_columns": []
|
||||
},
|
||||
"documents": {
|
||||
"exists": true,
|
||||
"missing_columns": []
|
||||
},
|
||||
"portal_editorial_editions": {
|
||||
"exists": true,
|
||||
"missing_columns": []
|
||||
},
|
||||
"portal_editorial_notifications": {
|
||||
"exists": true,
|
||||
"missing_columns": []
|
||||
},
|
||||
"audit_events": {
|
||||
"exists": true,
|
||||
"missing_columns": []
|
||||
},
|
||||
"tasks": {
|
||||
"exists": true,
|
||||
"missing_columns": []
|
||||
},
|
||||
"desk_cases": {
|
||||
"exists": true,
|
||||
"missing_columns": []
|
||||
},
|
||||
"investor_navigation_preferences": {
|
||||
"exists": true,
|
||||
"missing_columns": []
|
||||
},
|
||||
"simulation_form_drafts": {
|
||||
"exists": true,
|
||||
"missing_columns": []
|
||||
}
|
||||
},
|
||||
"offers": {
|
||||
"private_offers": 0,
|
||||
"unexpected_published": 0,
|
||||
"stored_projections": 0
|
||||
},
|
||||
"offer_routes": [],
|
||||
"flags": {
|
||||
"profile_access": true,
|
||||
"editorial": true,
|
||||
"notifications": false,
|
||||
"homologation_only": false,
|
||||
"homologation_access_mode": "perimeter",
|
||||
"nda_present": false,
|
||||
"expiry_scheduler": false
|
||||
},
|
||||
"mail": {
|
||||
"mailer": "smtp",
|
||||
"smtp_host_configured": true,
|
||||
"recipient_count": 0,
|
||||
"recipient_set_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945"
|
||||
},
|
||||
"runtime": {
|
||||
"queue": "database",
|
||||
"private_disk": "local",
|
||||
"scanner_host_configured": true,
|
||||
"scanner_socket_configured": true
|
||||
},
|
||||
"migration_count": 112,
|
||||
"config_cached": false,
|
||||
"operational_data_writes": 0,
|
||||
"fpm_image_capabilities": {
|
||||
"gd_enabled": true,
|
||||
"webp_enabled": true
|
||||
},
|
||||
"service_units": [
|
||||
"clamav-daemon.service loaded active running",
|
||||
"clamav-freshclam.service loaded active running",
|
||||
"nginx.service loaded active running",
|
||||
"php8.4-fpm.service loaded active running",
|
||||
"postgresql.service loaded active exited",
|
||||
"postgresql@17-main.service loaded active running",
|
||||
"teste-investidor-deploy.service loaded inactive dead",
|
||||
"teste-investidor-queue.service loaded active running"
|
||||
],
|
||||
"restore_image": "postgis/postgis@sha256:01a6a70e41e6c4467c8f55f6063555ed72db2d6662cd0d571040d42eadaeb6f6",
|
||||
"executor_config_keys": [
|
||||
"branch",
|
||||
"ci_environment",
|
||||
"current_link",
|
||||
"legacy_root",
|
||||
"php",
|
||||
"postgis_image",
|
||||
"qa_port",
|
||||
"release_root",
|
||||
"repository",
|
||||
"retention",
|
||||
"schema_contract",
|
||||
"shared_assets",
|
||||
"state_root",
|
||||
"target_url",
|
||||
"test_database",
|
||||
"webhook_secret_file"
|
||||
],
|
||||
"installed_revision": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"served_build_revision": "96f510965f70581a4b41dd94463f598b77dd7cf5",
|
||||
"executor_service_state": "inactive"
|
||||
}
|
||||
Reference in New Issue
Block a user