Metadata-Version: 2.4
Name: repolens-worker
Version: 0.1.0
Summary: Safe asynchronous RepoLens analysis worker
Author: Jonatanjrss
Maintainer: Jonatanjrss
License-Expression: MIT
Project-URL: Homepage, https://github.com/Jonatanjrss/repolens-worker
Project-URL: Documentation, https://github.com/Jonatanjrss/repolens-worker#readme
Project-URL: Repository, https://github.com/Jonatanjrss/repolens-worker.git
Project-URL: Issues, https://github.com/Jonatanjrss/repolens-worker/issues
Project-URL: Changelog, https://github.com/Jonatanjrss/repolens-worker/blob/main/CHANGELOG.md
Keywords: code-analysis,dramatiq,github,repolens,worker
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: No Input/Output (Daemon)
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: dramatiq[redis]<2,>=1.17
Requires-Dist: prometheus-client<1,>=0.20
Requires-Dist: redis<6,>=5
Requires-Dist: repolens-core<1,>=0.1
Requires-Dist: repolens-api<1,>=0.1
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-cov>=5; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Provides-Extra: release
Requires-Dist: build>=1.2; extra == "release"
Requires-Dist: twine>=6; extra == "release"
Dynamic: license-file

# RepoLens Worker

RepoLens Worker recebe solicitações versionadas, clona repositórios públicos do GitHub em um
diretório temporário, executa o `repolens-core` sem importar ou executar código do repositório e
publica o relatório normalizado no `repolens-api`.

## Visão geral

```text
mensagem JSON
    ↓
Dramatiq → Redis → Worker → clone Git temporário → RepoLens Core
                                                    ↓
PostgreSQL ← RepoLens API ← relatório autenticado ──┘
```

| Serviço | Responsabilidade | Porta local |
|---|---|---:|
| `postgres` | Persistência do RepoLens API | somente rede do Compose |
| `redis` | Fila, estado e claims de idempotência | `6379` |
| `api` | Instala `repolens-api` via `pip` e recebe relatórios | `8000` |
| `worker` | Consome jobs, clona, analisa e publica | métricas internas em `9000` |

O processador síncrono concentra as regras de negócio e depende de portas substituíveis para Git,
Core, API e estado. A fronteira Dramatiq converte a mensagem, registra o resultado, agenda retries
e encaminha falhas terminais para a dead-letter queue.

## Requisitos

- Docker com Docker Compose;
- `curl` e `jq` para o exemplo local;
- acesso à internet para instalar os pacotes e clonar o repositório público de demonstração;
- Python 3.13 apenas para desenvolvimento fora dos containers.

## Instalação via PyPI

Depois da primeira publicação, o pacote e o produtor de mensagens podem ser instalados com:

```sh
python3.13 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install repolens-worker
```

O comando `repolens-worker-enqueue` lê uma mensagem JSON de `stdin` e a envia ao Redis configurado
por `REDIS_URL`. O consumidor é iniciado com:

```sh
dramatiq repolens_worker.worker --processes 1 --threads 4
```

Para a experiência local completa, incluindo API e banco, prefira o Compose descrito a seguir.

## Teste local completo — caminho recomendado

Crie a configuração local e execute o script de demonstração:

```sh
cp .env.example .env
./scripts/local_demo.sh
```

Esse único script:

1. sobe PostgreSQL, Redis e RepoLens API;
2. aguarda o healthcheck da API;
3. cria um projeto com credencial administrativa local;
4. captura o `project_id`;
5. cria e captura o token de ingestão, exibido apenas uma vez pela API;
6. salva as credenciais em `.env.local` com permissão restrita;
7. inicia o serviço `worker` normal do Compose;
8. enfileira `https://github.com/Jonatanjrss/repolens-core`;
9. aguarda e imprime o relatório persistido.

`.env.local` e `.env` são ignorados pelo Git. O script não imprime o token no terminal e nunca o
coloca na mensagem da fila.

Para analisar outro repositório público do GitHub:

```sh
REPOSITORY_URL=https://github.com/owner/repository ./scripts/local_demo.sh
```

## Fluxo manual

Use esta seção quando quiser entender ou depurar cada etapa.

### 1. Iniciar infraestrutura e API

```sh
cp .env.example .env
docker compose up -d postgres redis api
until curl -fsS http://localhost:8000/health >/dev/null; do sleep 2; done
```

O serviço `api` instala `repolens-api==0.1.0` via `pip`. Como o pacote publicado não distribui os
arquivos Alembic, o Compose cria as tabelas com o metadata SQLAlchemy exclusivamente para o
ambiente local. Em produção, use migrations versionadas gerenciadas pelo deploy da API.
Altere `REPOLENS_API_VERSION` em `.env` para testar outra versão publicada. A primeira inicialização
pode levar alguns instantes enquanto o container instala as dependências.

### 2. Criar projeto e token

```sh
PROJECT_SLUG="local-repolens-$(date +%s)"

PROJECT_JSON="$(curl -fsS -X POST http://localhost:8000/v1/projects \
  -H "X-Admin-Key: ${REPOLENS_ADMIN_API_KEY:-local-admin-key}" \
  -H 'Content-Type: application/json' \
  -d "{\"name\":\"Local RepoLens\",\"slug\":\"${PROJECT_SLUG}\",\"repository_url\":\"https://github.com/Jonatanjrss/repolens-core\"}")"

PROJECT_ID="$(printf '%s' "$PROJECT_JSON" | jq -er '.id')"

TOKEN_JSON="$(curl -fsS -X POST \
  "http://localhost:8000/v1/projects/${PROJECT_ID}/tokens" \
  -H "X-Admin-Key: ${REPOLENS_ADMIN_API_KEY:-local-admin-key}")"

REPO_API_TOKEN="$(printf '%s' "$TOKEN_JSON" | jq -er '.token')"
export PROJECT_ID REPO_API_TOKEN
```

O token pertence ao projeto indicado por `PROJECT_ID`. Trocar o projeto e reutilizar um token
antigo retorna `401 Unauthorized`. Se um token aparecer em logs, histórico compartilhado ou uma
mensagem pública, considere-o comprometido e crie outro projeto/token para o teste.

### 3. Iniciar o worker

```sh
export REPO_PROJECT_ID="$PROJECT_ID"
export REPO_API_TOKEN
docker compose up -d --build worker
```

Usar `docker compose up` mantém o worker como serviço do projeto e evita containers órfãos e
conflitos de nome causados por `docker compose run --name ...`.

### 4. Enfileirar uma análise

```sh
jq -n \
  --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  --arg job_id "job-$(date +%s)" \
  '{version: 1, job_id: $job_id, repository_url: "https://github.com/Jonatanjrss/repolens-core", ref: "main", requested_at: $now, attempt: 0}' \
  | docker compose exec -T worker python -m repolens_worker.cli
```

Use uma URL simples no JSON. A sintaxe Markdown `[https://...](https://...)` não é uma URL de
repositório válida.

### 5. Consultar o resultado

```sh
curl -fsS "http://localhost:8000/v1/projects/${PROJECT_ID}/runs" \
  -H "Authorization: Bearer ${REPO_API_TOKEN}" \
  | jq .
```

Para recuperar as variáveis salvas pelo script em outro terminal:

```sh
set -a
. ./.env.local
set +a
PROJECT_ID="$REPO_PROJECT_ID"
```

## Contrato da mensagem

```json
{
  "version": 1,
  "job_id": "job-123",
  "repository_url": "https://github.com/owner/repository",
  "ref": "main",
  "requested_at": "2026-08-29T12:00:00+00:00",
  "attempt": 0,
  "commit": "0123456789abcdef0123456789abcdef01234567"
}
```

`commit` é opcional. `job_id` é a chave estável de idempotência e `attempt` começa em zero. A URL
deve usar HTTPS, não pode conter credenciais, query ou fragmento e deve apontar para
`github.com`/`www.github.com`.

O worker publica em `POST /v1/projects/{REPO_PROJECT_ID}/runs` usando `REPO_API_TOKEN` como Bearer
token. A API responde `201` para uma nova execução e `200` para uma repetição idêntica. `429`,
timeouts e respostas `5xx` são transitórios; erros permanentes seguem para a dead-letter queue.

## Desenvolvimento

```sh
python3.13 -m venv .venv
.venv/bin/pip install -e '.[dev]'
.venv/bin/ruff check .
.venv/bin/ruff format --check .
.venv/bin/mypy
.venv/bin/pytest
```

O CI usa Python 3.13 e executa lint, formatação, tipos, testes e build da imagem. Testes
determinísticos não acessam a internet; o smoke usa uma fixture local:

```sh
docker compose run --rm smoke
```

## Publicação no PyPI

A versão do pacote tem uma única fonte em `repolens_worker/__init__.py`. Antes de cada release,
atualize `__version__`; versões já enviadas ao PyPI não podem ser substituídas.

Para construir e validar os dois artefatos localmente:

```sh
python3.13 -m venv .venv-release
. .venv-release/bin/activate
python -m pip install --upgrade pip
python -m pip install '.[release]'
rm -rf ./build ./dist ./repolens_worker.egg-info
python -m build
python -m twine check --strict dist/*
```

Faça primeiro um upload de teste. O token é lido sem aparecer no histórico ou no terminal:

```sh
export TWINE_USERNAME=__token__
read -rsp 'Token do TestPyPI: ' TWINE_PASSWORD && printf '\n'
export TWINE_PASSWORD
python -m twine upload --repository testpypi dist/*
unset TWINE_PASSWORD
```

Depois de validar o pacote no TestPyPI, publique os mesmos artefatos no índice real com um token do
PyPI:

```sh
read -rsp 'Token do PyPI: ' TWINE_PASSWORD && printf '\n'
export TWINE_PASSWORD
python -m twine upload dist/*
unset TWINE_PASSWORD TWINE_USERNAME
```

O workflow `publish.yml` é a alternativa recomendada: uma execução manual publica no TestPyPI e
uma GitHub Release publica no PyPI usando Trusted Publishing, sem armazenar token no GitHub. A
configuração inicial, validação da instalação e comandos de release estão detalhados em
[docs/PUBLISHING.md](https://github.com/Jonatanjrss/repolens-worker/blob/main/docs/PUBLISHING.md).

## Operação e segurança

- clone raso (`depth=1`) com timeout de 120 segundos;
- checkout limitado a 256 MiB e 100.000 arquivos por padrão;
- cliente HTTP sem redirects e com timeout de conexão/leitura de 5/30 segundos;
- até cinco tentativas com backoff exponencial, jitter e teto de 300 segundos;
- claims Redis por `job_id` e por projeto/commit resolvido;
- container não-root, filesystem raiz somente leitura e `/tmp` temporário;
- logs JSON correlacionados por `job_id`, sem tokens ou payload completo;
- métricas Prometheus de duração, destino do job e tamanho do repositório.

Repositórios privados, outros provedores Git e execução de código de terceiros permanecem fora do
escopo.

## Solução de problemas

Redis recusando conexão em execução local:

```sh
docker compose up -d redis
docker compose exec redis redis-cli ping
```

Use `redis://127.0.0.1:6379/0` para processos no host e `redis://redis:6379/0` dentro do Compose.

API retorna `relation "projects" does not exist` após usar uma configuração antiga:

```sh
docker compose up -d --force-recreate api
docker compose logs --tail=50 api
```

Em um ambiente local descartável, `docker compose down -v` recria o banco do zero, mas apaga todos
os projetos, tokens e relatórios locais.

API retorna `401 Unauthorized`:

- confirme que o token foi criado para o mesmo `PROJECT_ID` da URL;
- exporte as variáveis antes de recriar o worker;
- gere outro token caso o valor tenha sido exposto;
- reinicie o worker após mudar credenciais.

Job não aparece na API:

```sh
docker compose logs --tail=100 worker
docker compose logs --tail=100 api
```

Use um `job_id` novo durante testes e gere a mensagem com `jq` para evitar JSON inválido.

Aviso de container órfão `repolens-worker-dev` após seguir uma versão antiga deste README:

```sh
docker rm -f repolens-worker-dev
```

O fluxo atual usa o serviço `worker` do Compose e não cria esse container nomeado.

## Encerrar o ambiente

```sh
docker compose down
```

Para apagar também os dados locais do PostgreSQL e Redis:

```sh
docker compose down -v
```

Veja também
[SECURITY.md](https://github.com/Jonatanjrss/repolens-worker/blob/main/SECURITY.md),
[docs/CONTRACT.md](https://github.com/Jonatanjrss/repolens-worker/blob/main/docs/CONTRACT.md),
[docs/ADR-0001-architecture.md](https://github.com/Jonatanjrss/repolens-worker/blob/main/docs/ADR-0001-architecture.md),
[docs/ADR-0002-local-integration.md](https://github.com/Jonatanjrss/repolens-worker/blob/main/docs/ADR-0002-local-integration.md)
e [CHECKLIST.md](https://github.com/Jonatanjrss/repolens-worker/blob/main/CHECKLIST.md).
