From cd3d944054e589f75490340358cdcd8ef767be88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yann=20Le=C3=A3o?= Date: Thu, 23 Jul 2026 16:17:00 -0300 Subject: [PATCH 1/6] test: reorganize unit and integration tests directories Ref #16 --- docs/INVENTARIO_ATUAL.md | 2 +- docs/TESTING.md | 4 ++-- tests/__init__.py | 0 tests/{ => integration}/test_api.py | 0 tests/{ => unit}/test_model_manifest.py | 0 5 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 tests/__init__.py rename tests/{ => integration}/test_api.py (100%) rename tests/{ => unit}/test_model_manifest.py (100%) diff --git a/docs/INVENTARIO_ATUAL.md b/docs/INVENTARIO_ATUAL.md index 89fdf60..caa0fe8 100644 --- a/docs/INVENTARIO_ATUAL.md +++ b/docs/INVENTARIO_ATUAL.md @@ -12,7 +12,7 @@ Data do levantamento: 22 de julho de 2026. | Utilitários | `src/utils/` | Sanitização de texto e ferramenta de rotulagem. | | Dados | `dataset/` e fonte externa indicada no README | Dataset e pesos são locais e ignorados; apenas a configuração de treino é versionada. | | Contêiner | `Dockerfile` | Instala `requirements.txt` e inicia `src.api.api:app`, mas não recebe o modelo. | -| Testes | `tests/test_api.py` | Mistura teste HTTP e modelo real; cria artefato local; alguns casos são ignorados quando não há imagem. | +| Testes | `../tests/integration/test_api.py` | Mistura teste HTTP e modelo real; cria artefato local; alguns casos são ignorados quando não há imagem. | ## Contrato HTTP observado (baseline) diff --git a/docs/TESTING.md b/docs/TESTING.md index 8afa2e3..5189614 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -6,8 +6,8 @@ somente os níveis rápidos; testes que requerem artefato real são opt-in. | Nível | Localização | Dependências | Quando executar | | --- | --- | --- | --- | | Unitário | `tests/unit/` | Nenhuma rede, peso ou GPU | Todo commit e pull request | -| Integração | `tests/test_api.py` | FastAPI e adaptador falso | Todo commit e pull request | -| Manifesto | `tests/test_model_manifest.py` | Arquivos pequenos | Todo commit e pull request | +| Integração | `../tests/integration/test_api.py` | FastAPI e adaptador falso | Todo commit e pull request | +| Manifesto | `../tests/unit/test_model_manifest.py` | Arquivos pequenos | Todo commit e pull request | | Smoke de artefato | `tests/model/` | Peso local já verificado | Antes de deploy ou promoção | | Inferência real | futuro fixture de imagens aprovada | Peso + grupo `inference` | Pipeline manual dedicado | diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_api.py b/tests/integration/test_api.py similarity index 100% rename from tests/test_api.py rename to tests/integration/test_api.py diff --git a/tests/test_model_manifest.py b/tests/unit/test_model_manifest.py similarity index 100% rename from tests/test_model_manifest.py rename to tests/unit/test_model_manifest.py From e7e353c4cb2fb60822c2018aecd346d9d29c0435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yann=20Le=C3=A3o?= Date: Mon, 27 Jul 2026 19:20:58 -0300 Subject: [PATCH 2/6] fix(docker): align healthcheck port with railway dynamic port --- Dockerfile | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8c456d6..1490600 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,14 +19,16 @@ RUN apt-get update \ libgomp1 \ && rm -rf /var/lib/apt/lists/* \ && groupadd --system app \ - && useradd --system --gid app --home-dir /home/app --create-home app + && useradd --system --gid app --home-dir /home/app --create-home app \ + && mkdir -p /app \ + && chown app:app /app WORKDIR /app -COPY --from=uv /uv /uvx /bin/ +COPY --from=uv --chown=app:app /uv /uvx /bin/ # Resolve dependências antes do código para aproveitar o cache de camadas. -COPY pyproject.toml uv.lock README.md ./ +COPY --chown=app:app pyproject.toml uv.lock README.md ./ RUN uv sync --frozen --no-dev --group inference --no-install-project COPY --chown=app:app src ./src @@ -34,7 +36,7 @@ COPY --chown=app:app config ./config COPY --chown=app:app scripts ./scripts RUN uv sync --frozen --no-dev --group inference -COPY docker/entrypoint.sh /usr/local/bin/medtrack-entrypoint +COPY --chown=app:app docker/entrypoint.sh /usr/local/bin/medtrack-entrypoint RUN chmod 755 /usr/local/bin/medtrack-entrypoint ENV MEDTRACK_FETCH_MODEL_ON_START=false \ @@ -44,7 +46,7 @@ ENV MEDTRACK_FETCH_MODEL_ON_START=false \ EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=5s --start-period=45s --retries=3 \ - CMD python -c "from urllib.request import urlopen; urlopen('http://127.0.0.1:8000/healthz', timeout=3)" + CMD python -c "import os; from urllib.request import urlopen; urlopen(f\"http://127.0.0.1:{os.environ.get('PORT', '8000')}/healthz\", timeout=3)" ENTRYPOINT ["/usr/local/bin/medtrack-entrypoint"] CMD ["sh", "-c", "uvicorn medtrack_ai.api.main:app --host 0.0.0.0 --port ${PORT:-8000}"] From 6ff84c1b9b85ef3e6e3b9effc368e7809b278223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yann=20Le=C3=A3o?= Date: Mon, 27 Jul 2026 20:21:45 -0300 Subject: [PATCH 3/6] build(docker): enable verbose tracing and run as root for diagnostics --- docker/entrypoint.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index f3393de..d4a47d0 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,16 +1,16 @@ #!/bin/sh -set -eu +# Diagnostic mode: send shell errors and execution tracing to the container's +# standard output so Railway captures failures that happen before Uvicorn. +exec 2>&1 +set -eux artifact_directory="$(dirname "${MEDTRACK_MODEL_URI}")" easyocr_directory="${EASYOCR_MODULE_PATH:-/home/app/.EasyOCR}" -# Volumes Railway são montados no início do container. Preparamos permissões -# como root e reexecutamos o entrypoint com o usuário sem privilégios. -if [ "$(id -u)" = "0" ]; then - mkdir -p "${artifact_directory}" "${easyocr_directory}" - chown -R app:app "${artifact_directory}" "${easyocr_directory}" - exec gosu app "$0" "$@" -fi +# Temporary Railway diagnostic: keep the process running as root. This isolates +# permission failures from entrypoint, model-download, and application failures. +echo "Diagnostic mode: running as uid=$(id -u), gid=$(id -g)" +mkdir -p "${artifact_directory}" "${easyocr_directory}" if [ "${MEDTRACK_FETCH_MODEL_ON_START:-false}" = "true" ]; then python scripts/fetch_model.py --manifest "${MEDTRACK_MODEL_MANIFEST}" From 099a8ce16ad6a092ddbbf2236d3eb88ca4bec77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yann=20Le=C3=A3o?= Date: Mon, 27 Jul 2026 20:54:58 -0300 Subject: [PATCH 4/6] build(railway): change healthcheck '/readyz' to '/healthz' --- railway.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/railway.toml b/railway.toml index c5209a8..d698901 100644 --- a/railway.toml +++ b/railway.toml @@ -2,7 +2,7 @@ builder = "DOCKERFILE" [deploy] -healthcheckPath = "/readyz" +healthcheckPath = "/healthz" healthcheckTimeout = 300 restartPolicyType = "ON_FAILURE" restartPolicyMaxRetries = 10 From 41972e8257fa7c8dc8fe6ef0c7d63fcef800798c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yann=20Le=C3=A3o?= Date: Mon, 27 Jul 2026 21:08:22 -0300 Subject: [PATCH 5/6] build(docker): add temporary bypass the entrypoint to isolate container statup --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1490600..7aac94e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,5 +48,7 @@ EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=5s --start-period=45s --retries=3 \ CMD python -c "import os; from urllib.request import urlopen; urlopen(f\"http://127.0.0.1:{os.environ.get('PORT', '8000')}/healthz\", timeout=3)" -ENTRYPOINT ["/usr/local/bin/medtrack-entrypoint"] -CMD ["sh", "-c", "uvicorn medtrack_ai.api.main:app --host 0.0.0.0 --port ${PORT:-8000}"] +# Temporary Railway diagnostic: bypass the entrypoint to isolate container +# startup from entrypoint execution. +# ENTRYPOINT ["/usr/local/bin/medtrack-entrypoint"] +CMD ["sh", "-c", "echo 'CONTAINER IS ALIVE' && uvicorn medtrack_ai.api.main:app --host 0.0.0.0 --port ${PORT:-8000}"] From 690d732fef559beb30c9b0e32a92ec878801b3b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yann=20Le=C3=A3o?= Date: Mon, 27 Jul 2026 21:24:08 -0300 Subject: [PATCH 6/6] build(docker): restore secure container deployment --- Dockerfile | 6 ++-- README.md | 2 +- docker/entrypoint.sh | 16 ++++----- docs/CLOUD_DEPLOYMENT.md | 54 +++++++++++++++++++++++++++++ docs/CONTAINER.md | 3 +- docs/RAILWAY.md | 73 ---------------------------------------- railway.toml | 8 ----- 7 files changed, 67 insertions(+), 95 deletions(-) create mode 100644 docs/CLOUD_DEPLOYMENT.md delete mode 100644 docs/RAILWAY.md delete mode 100644 railway.toml diff --git a/Dockerfile b/Dockerfile index 7aac94e..1490600 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,5 @@ EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=5s --start-period=45s --retries=3 \ CMD python -c "import os; from urllib.request import urlopen; urlopen(f\"http://127.0.0.1:{os.environ.get('PORT', '8000')}/healthz\", timeout=3)" -# Temporary Railway diagnostic: bypass the entrypoint to isolate container -# startup from entrypoint execution. -# ENTRYPOINT ["/usr/local/bin/medtrack-entrypoint"] -CMD ["sh", "-c", "echo 'CONTAINER IS ALIVE' && uvicorn medtrack_ai.api.main:app --host 0.0.0.0 --port ${PORT:-8000}"] +ENTRYPOINT ["/usr/local/bin/medtrack-entrypoint"] +CMD ["sh", "-c", "uvicorn medtrack_ai.api.main:app --host 0.0.0.0 --port ${PORT:-8000}"] diff --git a/README.md b/README.md index c351a88..429a8e2 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ descritos em [docs/CI_CD.md](docs/CI_CD.md). | Artefato e versão do modelo | [docs/models/README.md](docs/models/README.md) | | Contêiner local | [docs/CONTAINER.md](docs/CONTAINER.md) | | CI/CD e versionamento | [docs/CI_CD.md](docs/CI_CD.md) | -| Deploy de staging | [docs/RAILWAY.md](docs/RAILWAY.md) | +| Deploy em nuvem | [docs/CLOUD_DEPLOYMENT.md](docs/CLOUD_DEPLOYMENT.md) | | Decisões arquiteturais | [docs/adr/README.md](docs/adr/README.md) | | Governança | [docs/GOVERNANCE.md](docs/GOVERNANCE.md) | diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index d4a47d0..88baf11 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,16 +1,16 @@ #!/bin/sh -# Diagnostic mode: send shell errors and execution tracing to the container's -# standard output so Railway captures failures that happen before Uvicorn. -exec 2>&1 -set -eux +set -eu artifact_directory="$(dirname "${MEDTRACK_MODEL_URI}")" easyocr_directory="${EASYOCR_MODULE_PATH:-/home/app/.EasyOCR}" -# Temporary Railway diagnostic: keep the process running as root. This isolates -# permission failures from entrypoint, model-download, and application failures. -echo "Diagnostic mode: running as uid=$(id -u), gid=$(id -g)" -mkdir -p "${artifact_directory}" "${easyocr_directory}" +# Volumes são montados no início do contêiner. Preparamos as permissões como +# root e reexecutamos o entrypoint com o usuário sem privilégios. +if [ "$(id -u)" = "0" ]; then + mkdir -p "${artifact_directory}" "${easyocr_directory}" + chown -R app:app "${artifact_directory}" "${easyocr_directory}" + exec gosu app "$0" "$@" +fi if [ "${MEDTRACK_FETCH_MODEL_ON_START:-false}" = "true" ]; then python scripts/fetch_model.py --manifest "${MEDTRACK_MODEL_MANIFEST}" diff --git a/docs/CLOUD_DEPLOYMENT.md b/docs/CLOUD_DEPLOYMENT.md new file mode 100644 index 0000000..5bb59eb --- /dev/null +++ b/docs/CLOUD_DEPLOYMENT.md @@ -0,0 +1,54 @@ +# Deploy em plataforma de nuvem + +Este repositório pode ser implantado em uma plataforma que execute imagens +Docker, como Render ou outro provedor compatível. A plataforma deve construir o +`Dockerfile` da raiz e não deve substituir o `ENTRYPOINT` nem o `CMD` da imagem. + +## Configuração do serviço + +1. Crie um serviço web a partir deste repositório. +2. Selecione o runtime Docker e a branch que será implantada. +3. Não configure um comando de início personalizado. O `CMD` da imagem inicia o + Uvicorn usando a variável `PORT`, com fallback local para `8000`. +4. Configure a verificação HTTP de saúde em `GET /healthz`. +5. Se o modelo precisar persistir entre implantações, monte um volume em + `/data`. + +## Variáveis de ambiente + +Configure os valores adequados ao ambiente pela interface segura do provedor: + +```dotenv +MEDTRACK_ENV=staging +MEDTRACK_LOG_LEVEL=INFO +MEDTRACK_MODEL_URI=/data/models/medtrack-yolo/v1.0.0/best.pt +MEDTRACK_MODEL_VERSION=v1.0.0 +MEDTRACK_MODEL_MANIFEST=config/models/medtrack-yolo-v1.0.0.json +MEDTRACK_FETCH_MODEL_ON_START=true +MEDTRACK_DEVICE=cpu +MEDTRACK_MAX_IMAGE_DIMENSION=1024 +MEDTRACK_YOLO_CONFIDENCE=0.5 +EASYOCR_MODULE_PATH=/data/easyocr +MEDTRACK_CORS_ORIGINS= +``` + +Não copie um arquivo `.env` com segredos para o Git. A variável +`MEDTRACK_MODEL_URI` é obrigatória para o entrypoint preparar o diretório do +modelo antes de reduzir seus privilégios para o usuário `app`. + +## Saúde e prontidão + +- `GET /healthz` confirma que o processo HTTP está em execução e deve ser usado + como healthcheck da plataforma. +- `GET /readyz` confirma que o modelo foi carregado e que o serviço está pronto + para inferência. + +Após o deploy, substitua `URL` pelo domínio fornecido pela plataforma: + +```powershell +Invoke-WebRequest https://URL/healthz +Invoke-WebRequest https://URL/readyz +``` + +Os logs são enviados para stdout. Antes de promover uma versão, valide os dois +endpoints e confirme que o provedor preservou o volume do modelo. diff --git a/docs/CONTAINER.md b/docs/CONTAINER.md index 84aded1..5605257 100644 --- a/docs/CONTAINER.md +++ b/docs/CONTAINER.md @@ -49,4 +49,5 @@ use `$PWD.Path` para fornecer o caminho absoluto. - O Compose é CPU. Uma variante GPU só será criada após a escolha da plataforma de deploy e do runtime NVIDIA. -Para deploy de staging na Railway, consulte [RAILWAY.md](RAILWAY.md). +Para deploy em um provedor de nuvem, consulte +[CLOUD_DEPLOYMENT.md](CLOUD_DEPLOYMENT.md). diff --git a/docs/RAILWAY.md b/docs/RAILWAY.md deleted file mode 100644 index 9d01ddf..0000000 --- a/docs/RAILWAY.md +++ /dev/null @@ -1,73 +0,0 @@ -# Deploy de staging na Railway - -Este repositório está preparado para ser implantado como um serviço Railway a -partir da branch `main`. A Railway detecta o `Dockerfile` na raiz e usa -`railway.toml` para aguardar a prontidão real em `/readyz`. - -## Preparação da conta e do projeto - -1. Entre na Railway usando sua conta GitHub e crie o projeto `medtrack`. -2. Crie o ambiente `staging` e conecte o repositório `MedTrack-Project/MedTrack-IA` - à branch `main`. -3. Crie um serviço a partir do repositório. Não defina Start Command: o `CMD` do - Dockerfile usa automaticamente a porta `PORT` injetada pela Railway. -4. Em **Volumes**, adicione um volume de ao menos 0,5 GB no caminho `/data`. -5. Em **Networking**, gere um domínio público Railway. Registre a URL no canal - interno do projeto; o app Android deve recebê-la por configuração de build, - nunca como literal no código-fonte. - -## Variáveis do ambiente `staging` - -Use o Raw Editor da aba **Variables** e cole os valores abaixo. Não copie um -`.env` com segredos para o Git. - -```dotenv -MEDTRACK_ENV=staging -MEDTRACK_LOG_LEVEL=INFO -MEDTRACK_MODEL_URI=/data/models/medtrack-yolo/v1.0.0/best.pt -MEDTRACK_MODEL_VERSION=v1.0.0 -MEDTRACK_MODEL_MANIFEST=config/models/medtrack-yolo-v1.0.0.json -MEDTRACK_FETCH_MODEL_ON_START=true -MEDTRACK_DEVICE=cpu -MEDTRACK_MAX_IMAGE_DIMENSION=1024 -MEDTRACK_YOLO_CONFIDENCE=0.5 -EASYOCR_MODULE_PATH=/data/easyocr -MEDTRACK_CORS_ORIGINS= -RAILWAY_HEALTHCHECK_TIMEOUT_SEC=300 -RAILWAY_DEPLOYMENT_DRAINING_SECONDS=30 -``` - -O entrypoint prepara as permissões do volume, baixa o peso apenas se ele ainda -não passar no checksum e inicia a API como usuário sem privilégios. O volume -também preserva os modelos auxiliares baixados pelo EasyOCR entre reinícios. - -## Verificação após deploy - -Substitua `URL` pelo domínio público gerado: - -```powershell -Invoke-WebRequest https://URL/healthz -Invoke-WebRequest https://URL/readyz -``` - -`/healthz` confirma o processo; `/readyz` só retorna `200` quando o modelo foi -carregado. A Railway usa `/readyz` durante o deploy e aguarda até 300 segundos. - -## Logs, operação e rollback - -- Logs são JSON no stdout e incluem `request_id`, rota, status, duração e versão - do modelo; não registram a imagem nem o texto extraído. -- Para correlacionar uma ocorrência do mobile, envie o valor de `X-Request-ID` - junto ao relato. -- Antes de promover uma mudança, confira o workflow `Quality` e o endpoint - `/readyz` em staging. -- Para rollback, em **Deployments** escolha o deployment estável anterior e use - **Redeploy**. O volume contém o mesmo modelo versionado; para reverter modelo, - altere explicitamente `MEDTRACK_MODEL_*` para outro manifesto/versão e faça - novo deploy. - -## Limitações intencionais - -O ambiente Trial/Free tem créditos e políticas de restart limitadas. Ele é -adequado para staging acadêmico, não para alegações de disponibilidade clínica. -O serviço não deve decidir administração de medicamentos sem conferência humana. diff --git a/railway.toml b/railway.toml deleted file mode 100644 index d698901..0000000 --- a/railway.toml +++ /dev/null @@ -1,8 +0,0 @@ -[build] -builder = "DOCKERFILE" - -[deploy] -healthcheckPath = "/healthz" -healthcheckTimeout = 300 -restartPolicyType = "ON_FAILURE" -restartPolicyMaxRetries = 10