diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e8ddfa..56b2a07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,10 +4,25 @@ on: push: pull_request: +# Mínimo necessário. Nenhum job deste workflow escreve no repositório. +permissions: + contents: read + +# O repositório é público e o runner é uma máquina real. Um pull request de +# fork traria código de terceiro para rodar nele, com Docker à disposição. +# Enquanto não houver aprovação manual de workflow configurada na instância, +# só PR da própria origem executa. Push continua rodando normalmente. +# Evita fila de runs esperando runner: um push novo cancela o anterior do +# mesmo ref. O que já estava em execução não é interrompido no meio. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + jobs: verify: name: lint, typecheck, testes e build runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository env: ADAPTER_ENV: development ADAPTER_JOB_MODE: inline @@ -27,6 +42,7 @@ jobs: services: name: Postgres e Redis reais runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository services: postgres: image: postgres:16-alpine @@ -99,6 +115,7 @@ jobs: image: name: imagem Docker Linux runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository env: ADAPTER_IMAGE: adaptador-planilhas:ci steps: diff --git a/.github/workflows/runner-check.yml b/.github/workflows/runner-check.yml new file mode 100644 index 0000000..e7d856f --- /dev/null +++ b/.github/workflows/runner-check.yml @@ -0,0 +1,78 @@ +name: runner-check + +# Primeiro workflow a rodar quando um runner entrar online. Não instala +# dependência, não constrói nada, não toca em rede externa, não usa segredo e +# não faz checkout do código: só confirma que a ligação Gitea → runner funciona +# e mostra o que o runner tem disponível. +# +# Dispare pela interface (Actions → runner-check → Run workflow) ou editando +# este arquivo. + +on: + workflow_dispatch: + push: + paths: + - ".github/workflows/runner-check.yml" + +permissions: + contents: read + +concurrency: + group: runner-check-${{ github.ref }} + cancel-in-progress: true + +jobs: + ligacao: + name: o runner responde + runs-on: ubuntu-latest + # Público: PR de fork não executa enquanto não houver aprovação manual. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + steps: + - name: Identidade do runner + run: | + echo "runner.os = ${RUNNER_OS:-?}" + echo "runner.arch = ${RUNNER_ARCH:-?}" + echo "ref = ${GITHUB_REF:-?}" + echo "sha = ${GITHUB_SHA:-?}" + echo "evento = ${GITHUB_EVENT_NAME:-?}" + echo "---" + uname -a + echo "usuario: $(id -un) (uid $(id -u))" + + - name: Ferramentas que os outros jobs exigem + run: | + faltando="" + for cmd in node npm docker curl git psql python3; do + if command -v "$cmd" >/dev/null 2>&1; then + printf ' ok %-8s %s\n' "$cmd" "$($cmd --version 2>&1 | head -1)" + else + printf ' AUSENTE %-8s\n' "$cmd" + faltando="$faltando $cmd" + fi + done + echo "" + if [ -n "$faltando" ]; then + echo "Faltam no runner:$faltando" + echo "node e npm são exigidos pelo job verify." + echo "docker é exigido pelo job image (precisa de Docker-in-Docker)." + echo "psql e python3 são exigidos pelo job services e pela jornada assíncrona." + echo "" + echo "Isto não falha o workflow de propósito: o objetivo é inventariar." + else + echo "todas as ferramentas presentes" + fi + + - name: Docker funciona de verdade + continue-on-error: true + run: | + if ! command -v docker >/dev/null 2>&1; then + echo "docker ausente: o job image não vai rodar neste runner" + exit 0 + fi + docker version + docker run --rm hello-world >/dev/null 2>&1 \ + && echo "ok: o runner executa contêiner" \ + || echo "docker presente mas não executa contêiner: confira o Docker-in-Docker" + + - name: Resultado + run: echo "ligacao Gitea -> runner confirmada" diff --git a/docs/spreadsheet-adapter/22-gitea-actions.md b/docs/spreadsheet-adapter/22-gitea-actions.md new file mode 100644 index 0000000..30a5038 --- /dev/null +++ b/docs/spreadsheet-adapter/22-gitea-actions.md @@ -0,0 +1,122 @@ +# Gitea Actions — diagnóstico e ativação do runner + +Repositório `cferraz/adaptador-planilhas` em `gitea.fatorxtech.com`, Gitea **1.26.2**. + +## Causa confirmada da ausência de execuções + +**Os runs existem. O que não existe é runner para executá-los.** A falha está na **atribuição**, não no enfileiramento. + +| Evidência | Resultado | +| --- | --- | +| `ci.yml` detectado por Gitea | sim, listado em Workflows | +| Gatilho de `ci.yml` | `on: push` sem filtro de branch — contempla `homologacao` | +| Actions no repositório | habilitado (`has_actions: true`) | +| Actions na instância | habilitado (a página `/actions` responde e lista workflows) | +| Run criado pelo push | **sim** — `ci.yml #2`, run `666`, commit `5b11496cd9`, branch `homologacao`, `2026-10-16 00:05:18 +00:00` | +| Estado do run | **Waiting**, duração `0s` | +| Mensagem do Gitea | **`No matching online runner with label: ubuntu-latest`** | +| Run anterior | `ci.yml #1`, run `584`, commit `419325db12`, branch `main` — **Canceled** | + +Links: + +- https://gitea.fatorxtech.com/cferraz/adaptador-planilhas/actions +- https://gitea.fatorxtech.com/cferraz/adaptador-planilhas/actions/runs/666 (Waiting) +- https://gitea.fatorxtech.com/cferraz/adaptador-planilhas/actions/runs/584 (Canceled) + +### Por que a API parecia dizer "zero execuções" + +`GET /api/v1/repos/cferraz/adaptador-planilhas/actions/tasks` devolve `{"workflow_runs":[],"total_count":0}`. Esse endpoint lista **tasks**, isto é, jobs que um runner aceitou. Nenhum runner aceitou nada, então a lista é vazia — mas os runs estão criados e esperando. `GET /actions/runs` responde `token is required`, e foi por isso que a primeira leitura passou a impressão errada. + +**Consequência prática: nada precisa ser republicado.** O run `666` está em `Waiting` e começa sozinho no momento em que um runner com a label compatível ficar online. + +## Runners encontrados e labels + +| | | +| --- | --- | +| Runners com label `ubuntu-latest` **online** | **nenhum** (é o que o Gitea reporta) | +| Outros runners na instância | **não determinado** | + +Listar runners exige token de owner ou admin: `GET /api/v1/repos/{owner}/{repo}/actions/runners`, ou a interface em **Settings → Actions → Runners** (nível repositório, organização ou admin). Os números de run (`584`, `666`) sugerem que a instância já teve atividade de Actions em outros repositórios, então **é possível que existam runners com outra label**. Confira antes de instalar um novo: + +- se já houver runner Linux com Docker e outra label (`linux_amd64`, `ubuntu-22.04`, `docker`), **não instale nada**: basta ajustar o `runs-on` dos workflows. Diga qual é a label e a troca é de uma linha em três arquivos; +- se não houver, siga a instalação abaixo. + +## Configuração recomendada do runner + +Máquina Linux **dedicada a homologação**, sem nenhuma relação com o Docker de produção do Elo Pro. + +### Por que Docker-in-Docker, e não o socket do host + +O repositório é **público**. Montar `/var/run/docker.sock` do host dá ao workflow controle do Docker da máquina — e, num repositório público, código de terceiro poderia chegar lá por pull request de fork. A imagem `-dind-rootless` sobe um Docker próprio dentro do contêiner do runner: o job `image` constrói e executa contêineres sem alcançar o Docker do host. + +### Instalação + +Versão fixada, compatível com o Gitea 1.26.2 (`0.6.1` é a mais recente publicada; as alternativas semver são `0.1.4` a `0.6.1`): + +```bash +# 1. Token de registro: Settings → Actions → Runners → Create new runner, +# no NÍVEL DO REPOSITÓRIO (escopo restrito, não use token de admin). +# Grave o token num arquivo com permissão 600 — nunca em commit, nunca em log. +umask 077 +printf '%s' 'COLE_O_TOKEN_AQUI' > /etc/gitea-runner.token + +# 2. Suba o runner com Docker-in-Docker rootless. +docker run -d \ + --name gitea-runner-hml \ + --restart always \ + --privileged \ + -e GITEA_INSTANCE_URL=https://gitea.fatorxtech.com \ + -e GITEA_RUNNER_REGISTRATION_TOKEN_FILE=/run/secrets/token \ + -e GITEA_RUNNER_NAME=hml-adaptador \ + -e GITEA_RUNNER_LABELS='ubuntu-latest:docker://node:24-bookworm' \ + -v /etc/gitea-runner.token:/run/secrets/token:ro \ + -v gitea-runner-hml-data:/data \ + docker.io/gitea/act_runner:0.6.1-dind-rootless +``` + +Notas que importam: + +- `GITEA_RUNNER_REGISTRATION_TOKEN_FILE` em vez da variável com o valor: o token não aparece em `docker inspect` nem no histórico do shell. +- `--privileged` é exigência do Docker-in-Docker. É justamente por isso que a máquina precisa ser dedicada. +- A label **tem de ser `ubuntu-latest`**, que é o `runs-on` dos três jobs. O mapeamento `docker://node:24-bookworm` já entrega `node` e `npm` na versão certa. +- `psql` e `python3` não vêm nessa imagem. O job `services` usa `psql` e a jornada assíncrona usa `python3`. Duas saídas: trocar o mapeamento para uma imagem que os tenha, ou deixar que o `runner-check` reporte a ausência e então instalá-los num passo do workflow. Rode o `runner-check` primeiro: ele faz esse inventário. +- Rede de saída necessária: `gitea.fatorxtech.com` (fila e logs), `github.com` (o Gitea resolve `actions/checkout@v4`, `actions/setup-node@v4` e `docker/*` via proxy), `registry.npmjs.org` e `docker.io`. + +### Conferir que subiu + +```bash +docker logs -f gitea-runner-hml # espere "runner registered successfully" +``` + +Depois, em **Settings → Actions → Runners**, o runner aparece online com a label `ubuntu-latest`. O run `666`, que está em `Waiting`, começa sozinho. + +## Ordem de validação + +Nenhum destes foi executado ainda. Não marque nada como aprovado sem o log. + +1. **`runner-check`** — dispare manualmente. Não faz checkout, não instala nada, não usa segredo. Confirma a ligação e inventaria `node`, `npm`, `docker`, `psql`, `python3`. +2. **`verify`** — lint, typecheck, 102 testes, build. +3. **`services`** — Postgres e Redis efêmeros: as quatro migrations, idempotência, `--dry-run` sem efeito, shim recusado com `ADAPTER_APPLY_IDENTITY_SHIM=0`, `app_user` sem `BYPASSRLS`, `FORCE RLS`, isolamento entre workspaces, auditoria não alterável. +4. **`image`** — build Linux, conteúdo da imagem, usuário não-root, ausência de `.env`/docs/testes, os oito scripts operacionais transformando e executando, fail-fast com código diferente de zero na web e no worker, liveness com cabeçalhos, compose até `ready`, heartbeat do worker, jornada assíncrona entre processos e desligamento gracioso exigindo `SIGTERM recebido` em menos de 25 s. + +## Segurança revisada + +| Risco | Estado | +| --- | --- | +| Repositório **público** com Actions | `ci.yml`, `runner-check` e os três jobs têm guarda: PR de fork **não executa** (`head.repo.full_name == github.repository`). Push continua rodando | +| Código não confiável com acesso privilegiado | O runner usa Docker-in-Docker rootless, sem o socket do host. A máquina é dedicada e não alcança o Docker de produção | +| Deploy automático | `deploy-staging.yml` tem gatilho único `workflow_dispatch`. Nenhum push, tag ou merge dispara deploy | +| Aprovação manual de deploy | Os ambientes `hml-migracao`, `hml-deploy` e `hml-flag` são conceito do GitHub. **O Gitea não os implementa da mesma forma**: se o deploy for rodar aqui, as aprovações deixam de existir e isso precisa ser resolvido antes de apontar o workflow ao servidor | +| Permissão do token de Actions | `permissions: contents: read` nos dois workflows | +| Fila de runs esperando runner | `concurrency` por ref, com cancelamento do anterior | +| SSH de produção | **não configurado**. Nenhum segredo de produção foi adicionado | +| Branch `main` | **preservada** em `419325d` | +| Token de registro do runner | por arquivo com permissão 600, fora de commit e de log | + +Vale decidir também se este repositório deve seguir público. Auditei os commits e não há segredo algum — só modelos com campos vazios — mas um repositório privado remove de uma vez o risco de PR de fork. + +## Pendências + +**Para o CI rodar:** um runner Linux online com a label `ubuntu-latest`, Docker-in-Docker e saída de rede. Nada além disso. + +**Para homologação, depois do CI verde:** projeto Supabase de homologação com contas de teste e bucket privado; `DATABASE_URL` e a decisão do `ADAPTER_APPLY_IDENTITY_SHIM`; Redis com senha; DSN do Sentry; DNS, TLS e Nginx de `hml-ada.elopro.app`; smoke real e ensaio de rollback. O procedimento está em [`20-homologacao-runbook.md`](20-homologacao-runbook.md) e o estado item a item em [`21-checklist-validacao.md`](21-checklist-validacao.md).