Arquitetura — plat-eng-scaffolder
O motor core/, as 3 cascas (CLI, GitHub App, API), o contrato app.yaml e o fluxo do webhook.
core/ — o motor compartilhado
Nenhuma das 3 cascas tem lógica própria de negócio — todas chamam as mesmas funções em core/:
| Módulo | Responsabilidade |
|---|---|
detect/ | Heurística que infere shape/instances/porta/health a partir de Dockerfile + package.json + grep no código |
contract/ | Motor de validação único (Ajv2020) — schemas de app.yaml, app-delivery.yaml, values.yaml por shape, blueprint.yaml |
generate/ | Gera app.yaml, Dockerfile placeholder, workflow de CI (build-and-bump.yml) |
github/ | Abstração de escrita única (GitHubClient) + auth de instalação do App (JWT) + auth OAuth de usuário (ADR-0011) + client via API (sem checkout local) |
argocd/ | Client REST read-only do ArgoCD (listApplications, getApplication) |
init.ts / register.ts / bump.ts / stack.ts / unstack.ts / restack.ts / validate.ts | Um "engine" por comando — a mesma função que a CLI chama é a que o webhook e a API chamam |
restack.ts (edita resources/scaling de um app já instanciado, preservando comentários do
values.yaml) só é exposto pela API — não existe comando scaffold restack na CLI.
As 3 cascas
| Casca | Entrada | Deploy | Autenticação |
|---|---|---|---|
CLI (cli.ts) | bun run src/cli.ts <comando> | não deployada, roda local | SCAFFOLD_GITHUB_TOKEN (PAT) |
GitHub App / webhook (app.ts) | POST /webhook | scaffolder, Dockerfile raiz | instalação do App (JWT + installation token) |
API web (src/web/server.ts) | /api/* + /docs (Scalar) + /mcp (MCP server, ADR-0012) | scaffolder-api, Dockerfile.web | sessão OAuth do usuário (aprovar/mergear) + instalação do App (abrir PR) + OAuth do usuário via MCP (Onboarding/Registro/Blueprint dev-facing) |
Sem framework em nenhuma das duas cascas HTTP — Bun.serve() puro, decisão deliberada (ADR-0009).
Sem frontend desde 2026-08-10 — a API é o produto, quem consome decide a UI. O MCP (/mcp,
src/web/mcp/) não é uma quarta casca — mesmo Bun.serve(), mesmo deployable da API web, só um
transporte a mais na frente do mesmo core/; detalhe das 7 tools e das skills que as consomem no
Manual.
O contrato app.yaml
apiVersion: prophub.tech/v1, kind: App, additionalProperties: false em tudo (campo
desconhecido reprova, não ignora). Campos:
metadata.name(obrigatório, DNS-1123) /metadata.stack(opcional, rótulo de agrupamento)spec.shape(obrigatório) —static-web | web | worker | jobspec.instances(obrigatório) —single | per-tenantspec.build.dockerfile(defaultDockerfile) /spec.build.buildArgs(só nomes, nunca valores)spec.runtime.port/spec.runtime.healthPath(default/) — obrigatório seshapeéstatic-web/webspec.ingress.host(literal, só quandoinstances: single) ouspec.ingress.hostPrefix(só quandoinstances: per-tenant) — nunca os dois juntos, o schema proíbespec.env.fromSecret(defaultfalse)spec.resources.cpu/.memory,spec.scaling.{min,max,targetCPU}— proibidos quandoshapeéworker/jobspec.worker.consumerName— só quandoshape: worker, nunca pode conter{{ }}sobrandospec.job.schedule— só quandoshape: job; presente vira CronJob, ausente vira Job único
O webhook — 4 eventos, não 3
A RFC-0006 original documenta 3 eventos; o código hoje implementa 4:
pull_request(opened/synchronize) — buscaapp.yamlno HEAD do PR, valida, publica um Check Runscaffold validate(sucesso/falha) com o motivo.push— só loga; a escrita de versão real acontece dentro do workflow de CI que oscaffold initgerou (que chamascaffold bump), não aqui.repository(created) — checa se o repo novo já nasceu comapp.yaml; se sim e ainda não está registrado em nenhumapp-delivery.yaml, comenta sugestão descaffold registerno PR que introduziu o arquivo (ou abre issue se não achar PR).issue_comment(/scaffold init, primeira linha exata) — checa permissãoadmin/writedo autor do comentário, e se autorizado chama o mesmoinitFromRepo()que a CLI chama, lendo o repo via API do GitHub em vez de checkout local.
Todos os 4 autenticam como a instalação do App (nunca PAT) — o installationId vem do próprio
payload do evento.