diff --git a/deploy/homologacao/README.md b/deploy/homologacao/README.md new file mode 100644 index 00000000..62419b57 --- /dev/null +++ b/deploy/homologacao/README.md @@ -0,0 +1,44 @@ +# Disponibilização automática da homologação + +Destino: `https://teste-investidor.tempo.co.mz`. Branch escolhida pela Tempo: `main`. + +## Trabalho dos programadores + +1. Trabalhar numa branch, fazer commits e enviar para o Git existente. +2. Rever e integrar em `main` segundo as designações já aprovadas pela Tempo. +3. O push em `main` activa o webhook existente. A fila serializa as execuções; a revisão mais recente é verificada e compilada numa pasta nova. +4. Conferir o commit efectivamente servido em `/build/revision.json`. Um commit no Git só está instalado quando consta desse endereço. Em caso de falha, a revisão anterior continua servida; o operador consulta os relatórios e logs protegidos. + +Não editar a pasta servida. Uma alteração manual detectada interrompe a substituição até ser reconciliada, preservando os ficheiros existentes. + +## O que segue o Git + +Fontes, menus, estilos e formulários são instalados com o build da mesma revisão. Não são transportados por Git a base de dados, ficheiros carregados, contas, permissões, segredos nem estados de publicação. + +As migrations são comparadas com o contrato nominal protegido do destino. Uma alteração nesse contrato exige análise e autorização específicas; este executor não executa SQL de migração no destino. A criação do schema durante a verificação usa exclusivamente PostgreSQL/PostGIS descartável, em `127.0.0.1:55441`, com nome terminado em `_test` e identidade conferida pela guarda QA. + +## Operação e recuperação + +O receptor `/deploy` conserva a protecção própria do Nginx e a assinatura HMAC do Gitea. As páginas públicas não exigem a credencial ambiental; as operações privadas conservam login, permissões e MFA aplicável. + +O executor instalado e a configuração em `/etc/investmoz-deploy` são controlados pelo operador. Alterar as suas fontes no repositório não substitui automaticamente esse controlo. A imagem de CI é fixada por digest; não modifica o PostgreSQL ou PostGIS da aplicação. + +O operador consulta: + +- `systemctl status teste-investidor-deploy.service teste-investidor-deploy.path` +- `/var/lib/investmoz-deploy/reports/`: commit solicitado, resultado e revisão instalada. +- `/var/lib/investmoz-deploy/logs/`: detalhe protegido da verificação e compilação. + +Para reverter, suspender primeiro novas execuções com `systemctl stop teste-investidor-deploy.path` e aguardar a execução em curso. Executar, como o utilizador de disponibilização existente: + +```sh +python3 /var/www/deploy/investmoz-pipeline.py --config /etc/investmoz-deploy/delivery.json --rollback +``` + +A reversão verifica os hashes, repõe a release anterior e reinicia graciosamente os workers. Conserva a base, uploads, configurações partilhadas e dados posteriores. Não é restauro integral de dados. Repor uma release incompatível com dados/schema posteriores exige análise específica. Retomar a path unit apenas depois da decisão de corrigir ou voltar a disponibilizar a revisão integrada. + +O checkout físico anterior é preservado; não é feito `reset --hard` nele. Assets com nomes hash permanecem num conjunto partilhado para conservar páginas já abertas. Não há eliminação automática de releases ou dados. + +## Limites + +Uma verificação automática não publica conteúdos nem comprova todos os percursos de negócio. Mantêm-se as provas aceites e as pendências funcionais da matriz. Falhas do fluxo devem indicar o comando e o log concreto, sem exposição de segredos ao visitante. diff --git a/deploy/homologacao/pipeline.py b/deploy/homologacao/pipeline.py index a246cf62..52d18d86 100644 --- a/deploy/homologacao/pipeline.py +++ b/deploy/homologacao/pipeline.py @@ -52,7 +52,8 @@ def archive_source(data: bytes, root: Path) -> None: path = (root/item.name).resolve() if not path.is_relative_to(root.resolve()) or not (item.isfile() or item.isdir()): raise RuntimeError('Unsafe source archive entry') - if item.name == '.env' or item.name == 'public/hot' or item.name.startswith('storage/app/private/'): + if item.name == '.env' or item.name == 'public/hot' or (item.isfile() + and item.name.startswith('storage/app/private/') and item.name != 'storage/app/private/.gitignore'): raise RuntimeError('Private/runtime files must not be distributed by Git') # Debian's installed Python predates tarfile extraction filters. Validate # every entry first, then create only ordinary directories/files ourselves. @@ -171,7 +172,7 @@ class Delivery: # Only the newly created, fully identified CI container and its anonymous volumes. self.run(['docker','rm','--force','--volumes',cid], timeout=60) - def build(self, target: str, release: Path) -> None: + def build(self, target: str, release: Path, attach_runtime: bool = True) -> None: data = self.run(['git','--git-dir',self.mirror,'archive','--format=tar',target], timeout=120,log_output=False) # Binary archives must never be appended to text build logs. archive_source(data, release) @@ -193,13 +194,15 @@ class Delivery: # The guard is verified against the actual PostgreSQL identity, not only environment strings. 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);" self.run([self.c['php'],'-r',guard],cwd=release,env=env) - self.run([self.c['php'],'artisan','migrate','--force','--no-interaction'],cwd=release,env=env) + # The project intentionally gates later schemas behind nominal authenticated + # installation. Reuse its accepted isolated baseline lifecycle test instead + # of bypassing those guards with a generic migrate of every module. + self.run([self.c['php'],'vendor/bin/phpunit','--configuration=phpunit.investmoz.xml','--do-not-cache-result'],cwd=release,env=env) self.run(['npm','ci','--no-audit','--no-fund'],cwd=release,env=env) self.run(['npm','run','build'],cwd=release,env=env) self.run(['npm','run','types:check'],cwd=release,env=env) self.run([self.c['php'],'tools/portal-diagnostics-deployment-guard.php'],cwd=release,env=env) self.run([self.c['php'],'vendor/bin/phpunit','--configuration=phpunit.xml','--testsuite','Unit','--do-not-cache-result'],cwd=release,env=env) - self.run([self.c['php'],'vendor/bin/phpunit','--configuration=phpunit.investmoz.xml','--do-not-cache-result'],cwd=release,env=env) tests=sorted((release/'tests/frontend').glob('*.test.mjs')) self.run(['node','--test',*tests],cwd=release,env=env) # Final production vendor from the same lock, before linking any real configuration. @@ -210,6 +213,8 @@ class Delivery: for entry in manifest.values(): for path in [entry.get('file'),*entry.get('css',[]),*entry.get('assets',[])]: if path and not (release/'public/build'/path).is_file(): raise RuntimeError('Missing compiled asset') + if not attach_runtime: + return # Discard only this release's synthetic runtime, retaining it privately outside the served tree. (release/'storage').rename(self.state/'qa-runtime'/release.name) (release/'.env').unlink()