For future Claude
Pipeline de release D2U.NET: Util/D2U.NET/deploy_pipeline.py + README Util/D2U.NET/DEPLOY-PIPELINE.md.
Build na máquina Windows, deploy por SSH para 192.168.0.29. A unidade atómica é a tag da imagem
Docker, não uma pasta — d2unet-web:<versão> imutável, :local é só apontador; a troca é
docker tag + docker compose up -d --no-build. O deploy.py legado continua a existir e a
funcionar (extrai por cima, sem backup nem rollback). Não há repositório git em nenhuma das
árvores, logo o argumento branch/tag é um rótulo de versão. Duas releases reais e ciclo de
rollback completo testados a 2026-09-03; o servidor corre main-20260903T213703Z. Hub:
Homologacao · Observability · Testes.
CI/CD — pipeline de release D2U.NET
Levantamento (2026-09-03)
| Pergunta | Resposta |
|---|---|
| Solução | d2u.net.slnx (SLNX), 12 projetos; dois artefactos publicáveis: D2U.Web e D2U.Workers |
| Entry point | src/D2U.Web (Microsoft.NET.Sdk.Web), net10.0 |
| Hospedagem | Docker Compose, projeto d2unet — nem IIS, nem systemd, nem nginx |
| Kestrel | dentro do container na 8080, publicada no host como 8098, sem proxy à frente |
| Utilizador | container corre como root (uid=0) |
| Diretório publicado | /app dentro da imagem; o publish/ no host é só contexto de build |
| Deploy actual | deploy/deploy.py — tar por SFTP, extrai por cima, docker rm -f, up -d --build |
| Config no repo | appsettings.json, appsettings.Production.json (placeholders ${MYSQL_HOST}) |
| Config no servidor | .env, 68 chaves, todos os segredos de pagamento e mail |
| Logs | Serilog só consola → Docker json-file 20 MB × 5 → docker logs; sem sudo no servidor |
| Versionamento | nenhum — Directory.Build.props não define Version, tudo reporta 1.0.0 |
| Git | não existe repo em C:\Projetos\D2U.NET nem em C:\Projetos\Util\D2U.NET |
| CI existente | .github/workflows/ci.yml (build + test com Postgres), sem job de deploy, nunca corre |
| Disco | 313 GB livres de 458 GB; imagens 16,69 GB; build cache 69,31 GB, 45,35 GB recuperáveis |
Porque a tag da imagem é a unidade atómica
docker/Dockerfile.web faz COPY publish/web/ . sobre uma base aspnet:10.0 limpa. Logo /app
dentro da imagem é o único caminho de carregamento e não pode acumular DLLs órfãs entre
deploys, ao contrário de copiar por cima de um diretório. Trocar d2unet-web:local para apontar a
uma tag de versão nova e correr up -d --no-build é uma troca atómica real, e o --no-build
elimina a armadilha de 2026-08-19 (o COPY publish/web/ marcado CACHED a devolver SUCCESS
com binários antigos — ver Defeito-DBNull-Bool-2026-08-19).
O que o pipeline garante
Nove requisitos, todos implementados: aborta no primeiro erro (a troca é o penúltimo passo, logo
uma falha deixa a aplicação intocada); valida o artefacto por bandas de tamanho, parse de
deps.json/runtimeconfig.json/appsettings.Production.json, contagem de ficheiros e sha256;
publica em dist/<versão> sempre recriado e releases/<versão> sempre novo; confirma fonte única
de verdade com find / -xdev mais sha256sum dentro do container contra o artefacto validado;
declara a posse de cada ficheiro de config; faz backup do .env e poda para as últimas
--keep (3) releases e imagens, com gate de espaço livre; health check HTTP real exigindo 200 e
"status":"healthy"; rollback num comando; e diagnóstico no README.
Bandas de tamanho são por serviço: D2U.Workers.dll tem 58 KB porque o projeto de workers é
um host fino — a lógica vive em D2U.Infrastructure.dll. Uma banda global rejeitava o artefacto
correcto (aconteceu no primeiro dry-run).
Mudanças de comportamento face ao deploy legado
| Tema | Legado | Pipeline novo |
|---|---|---|
.env do servidor |
sobrescrito a cada deploy pela cópia da máquina de build | nunca escrito; só validado (5 chaves obrigatórias) |
Workers__Enabled |
depende de alguém passar --workers-off |
propriedade do servidor; muda-se com deploy/set_workers.py |
appsettings.Development*/Testing |
publicados dentro da imagem de produção | excluídos do artefacto |
| Porta do health check | assumida | lida de DOCKER_WEB_PORT no .env do servidor |
| Rollback | inexistente | python deploy_pipeline.py --rollback |
Antes de mudar a posse do .env confirmei que a divergência real era de uma chave
(Workers__Enabled, local true / servidor false), sem divergência de segredos.
Dívida técnica registada (decisão: publicar e stage primeiro)
- Segredos em texto simples. Stripe, PayPal, RealEx, GoogleCheckout, SMTP, IMAP, MS Graph e
Lexacom vivem no
.envdo servidor e numa cópia não versionada emdeploy/.envna máquina de build. Decisão explícita de 2026-09-03: publicar e fazer stage primeiro, resolver depois. Candidatos: Docker secrets, ou cofre. Liga a Seguranca (Onda 9, "Secrets: Key Vault / env"). - Credenciais SSH em código.
deploy/_config.pytem host, utilizador e password como valores por omissão. Sobreponíveis por variáveis de ambiente, mas o default está no ficheiro. - Sem versionamento de assembly. Sem
VersionnoDirectory.Build.props, todos os builds reportam1.0.0e o/healthnão expõe versão; a identidade de uma release é carregada pelosha256. Estampar a versão precisa de aprovação (é build da aplicação) — uma linha noDirectory.Build.propsmais-p:Version=no publish, e o--expect-versionpassa a ter dentes. - Sem git. Sem repositório não há checkout, não há histórico de commits no manifesto, e o
ci.ymlnunca corre. O manifesto já grava"vcs": "none"e passa a gravarHEADmais os últimos 50 commits assim que existir repo, sem alterações ao script.
Limpezas pendentes no servidor (não autorizadas)
Nenhuma foi executada. A poda do pipeline toca apenas em caminhos por baixo de releases/.
| Item | Tamanho | Nota |
|---|---|---|
/home/edinaldo/projetos/D2U.NET/app/ |
92 MB | cópia da árvore de código de 22 Jul, com .env e compose próprios; sem DLLs da aplicação (verificado) |
publish/ na raiz remota |
315 MB | contexto de build do deploy legado; obsoleto quando releases/ estiver em uso |
| Build cache do Docker | 45,35 GB recuperáveis | o verdadeiro risco de disco, muito acima dos backups |
Fora de âmbito
Zero-downtime real (o up -d recria os containers, ~5–10 s; blue/green exigia um proxy que não
existe), migrações de base de dados (AutoMigrate=false, MySQL partilhado com o Java), container
sem ser root, job de deploy no GitHub Actions, e a suíte de testes completa como gate — esta tem
~40 falhas pré-existentes (Testes), por isso o gate por omissão é --filter Parity2026
(216 testes verdes no dry-run de 2026-09-03), a prática já usada no test_and_deploy.py.
O que as releases reais ensinaram (2026-09-03)
Três falhas apareceram só por correr a sério, e todas geraram correcções no pipeline.
1. O disco da máquina de build encheu. Verificava espaço no servidor mas não localmente, e o
dotnet publish reportou "not enough space on the disk" como warning de retry — continuou a
andar e teria produzido um artefacto incompleto. Havia 210 MB livres em 232 GB usados. Correcções:
gate de 600 MB no preflight local, tarball enviado a partir de memória em vez de escrito em disco,
e --keep-local a 0 por omissão (o artefacto já está no servidor com o manifesto). Libertados
2,3 GB de bin/obj mais 305 MB de deploy/publish/, com autorização.
2. MCR devolveu 429 Too Many Requests. O docker build resolve os metadados da base em
mcr.microsoft.com em cada invocação; a imagem web construiu e a dos workers apanhou o rate
limit. Correcção: retry com backoff (4 tentativas) para padrões transitórios de registry/rede;
erros não transitórios continuam a abortar na primeira.
3. Presumi mal que as imagens eram idênticas. docker images mostrava o mesmo CreatedAt e
tamanho para as três tags, o que me levou a duvidar que o rollback provasse algo. Os image IDs são
distintos (5fe2aa25, 69731c8d, 43fdf039) — o timestamp da coluna não distingue builds.
As duas primeiras falharam antes de qualquer coisa tocar no servidor (passos 3 e 7, a troca é
o passo 8), com releases/ intacto e a aplicação no ar inalterada. É o requisito 1 a funcionar.
Evidência
| Passo | Resultado |
|---|---|
| Testes gate | 216 Parity2026 verdes (198 unit + 18 integration) |
| Artefacto | D2U.Web.dll 1,8 MB sha256=7fc288e0… · D2U.Workers.dll 58 KB sha256=2a394990… |
| Pacote | 125,5 MB, streamed de memória |
| Fonte única de verdade | exactamente um DLL em /app por container, sha256 igual ao artefacto validado |
| Health | 200 healthy em cada release e em cada rollback |
| Rollback | 213703Z → 213121Z → 213703Z; container passou a sha256:69731c8d… e voltou a 43fdf039… |
O alvo de rollback da primeira release foi criado etiquetando as imagens de 2026-08-20 como
pre-pipeline-20260820 — aditivo, sem remover nada.
Próximo passo
Nada bloqueante. Quando houver git, o manifesto começa a gravar HEAD e commits sem alterações ao
script. A release parcial main-20260903T212245Z (só imagem web, da falha do MCR) é inerte e sai
na próxima poda.