Manual — tools/scaffolder
Todo comando da CLI, toda rota da API, e o que cada casca precisa pra rodar localmente.
Absorvido em
plat-eng-platform/tools/scaffolder/pela ADR-0012 (fusão de repos) —plat-eng-scaffoldernão tem mais código vivo.stack/unstacke as 4 tools MCP de Blueprint (list_blueprints/get_blueprint_params/preview_stack/submit_stack) saíram do binário nessa mesma janela: o caminho de tenant foi descontinuado (ADR-0012) e será redesenhado sobre o XRDAppPlacement, não retomado como estava. VerCONTEXT.md(plat-eng-bootstrap) edocs/adr/ADR-0010-colocacao-fora-do-git.mdx.
CLI — os 7 comandos
scaffold detect <repo>
scaffold validate [path]
scaffold bump <versionsKey> --tag <tag> [--path <repo>]
scaffold init <repo> --github <owner/repo> [--force]
scaffold register <repo> --namespace <ns> --image-repository <repo> --image-tag <tag> [--existing-secret <nome>] [--by <login>]
scaffold promote <repo> <tag> [--by <login>] [--reason <texto>]
scaffold status <nome>| Comando | O que faz | Precisa de token? |
|---|---|---|
detect | Infere shape/instances/porta a partir do código — só imprime, não escreve nada | Não |
validate | Valida app.yaml — exit 1 se falhar, usável direto em CI | Não |
bump | Escreve/atualiza a tag de versão em app-delivery/charts/<chart>/versions/<versionsKey>.yaml (plat-eng-platform) — só o scaffolder-api ainda usa este caminho (ADR-0013 §D2, não se autopromove); as outras 3 apps saíram dele no Grupo F | Sim |
init | Gera app.yaml (+ Dockerfile placeholder + CI se faltando) e abre PR — idempotente, não faz nada se já existe e valida (a menos que --force) | Sim |
register | Projeta o app.yaml num AppPlacement e aplica no cluster (ADR-0011) — não abre PR. --namespace e a imagem (--image-repository/--image-tag) são obrigatórios; a tag precisa ser imutável. --by grava a atribuição em status.changes — sem ele a mudança fica sem autor | Não (precisa de credencial de cluster) |
promote | Troca só spec.placement.image.tag de um AppPlacement já registrado (ADR-0013 §1) — nunca reprojeta o Contrato. Recusa se o Contrato do commit divergir do que está no recurso, ou se a imagem não existir no OCIR | Não (precisa de credencial de cluster) |
status | A Esteira de uma Colocação (Grupo B5): Contrato válido? imagem no OCIR? ExternalSecret saudável? Observa e escreve no status do AppPlacement. Sai 1 se alguma etapa reprovou | Não (cluster + ARGOCD_BASE_URL/ARGOCD_API_TOKEN) |
bump/init recusam rodar sem SCAFFOLD_GITHUB_TOKEN (PAT com contents:write+
pull-requests:write no(s) repo(s) alvo). detect e validate nunca tocam em token nenhum —
seguros de rodar em qualquer ambiente, inclusive CI de terceiros.
register é o único que precisa de credencial de cluster, não do GitHub (para escrever; o
app.yaml continua vindo de um checkout local). Dentro do cluster ele usa a ServiceAccount do
próprio pod; localmente, o KUBECONFIG (senão ~/.kube/config) — e precisa ser um kubeconfig de
token estático, como o portável do plat-eng-bootstrap. O que a OCI gera com
oci ce cluster create-kubeconfig tem bloco exec, que este cliente recusa de propósito, com
mensagem própria, em vez de seguir e tomar 401.
register valida o render antes de escrever — e kubectl não
Antes de aplicar o AppPlacement, o register monta o values que a Composition vai passar ao
chart e o valida contra o values.schema.json daquele chart. Erro de render volta síncrono, e
nada é escrito no cluster.
Essa defesa é da ferramenta, não do cluster: quem aplica um AppPlacement com kubectl apply
contorna a checagem por inteiro — o API server só conhece as regras do XRD, que são mais
frouxas que as do chart (exemplo real: env.fromSecret é aceito em qualquer forma pelo XRD, mas
o chart prophub-static não tem bloco env e falha no render). Nesse caminho o recurso nasce
válido e o problema só aparece como Application Degraded, minutos depois. Registrado nas
Consequências da ADR-0011.
API — rotas autenticadas
A maioria exige sessão (requireSession — cookie dashboard_session, OAuth GitHub do próprio
dev, 401 sem ela). Exceção: POST /api/ci/promote, autenticado por um ID token OIDC do
próprio GitHub Actions — nunca cookie, nunca kubeconfig (ADR-0013 §1) — verificado contra a JWKS
pública do GitHub; owner/repo/commit vêm do token, nunca de campo do corpo.
| Rota | O que faz |
|---|---|
GET /api/me | usuário logado |
POST /api/detect | Detecção (shape/instances/porta) de um repo via API do GitHub, com evidências — não escreve nada |
POST /api/init | Onboarding: gera app.yaml e abre PR no repo da própria aplicação |
POST /api/register | Registro (ADR-0011): projeta app.yaml num AppPlacement e aplica no cluster — não abre PR |
GET /api/prs/{owner}/{repo}/{number} | status do PR (mergeable, approved, checks) |
POST /api/prs/{owner}/{repo}/{number}/approve / .../merge | aprovar/mergear como a pessoa logada, nunca o bot (ADR-0011) — merge reconfirma mergeable no servidor antes de agir |
GET /api/argocd/applications?tenant=<slug> | Applications do ArgoCD pra um tenant |
POST /api/ci/promote | Promoção via CI (ADR-0013 §1/§2) — troca só spec.placement.image.tag do AppPlacement do repo do token |
GET /api/placements | Lista todos os AppPlacement — nome, namespace, shape, imagem, status já observado |
PATCH /api/placements/{name} | Edita a Colocação (namespace/ingress/resources/scaling/env.existingSecret) — nunca image (é .../promote) nem contract (é /api/register) |
POST /api/placements/{name}/promote | Promove pela sessão de um membro logado — mesmo motor de /api/ci/promote, requestedBy vem da sessão, nunca do corpo |
GET /api/placements/{name}/status | A Esteira de uma Colocação (Grupo B5): Contrato válido? imagem no OCIR? ExternalSecret saudável? últimas mudanças |
/api/tenants, /api/blueprints, /api/stacks/*, /api/tenants/{slug}/apps/{app}/edit/*,
/api/tenants/{slug}/removal/* e /api/argocd/claims-status não existem mais — eram do
caminho de tenant/Blueprint, descontinuado pela ADR-0012.
MCP server e skills dev-facing (ADR-0012)
Mesma casca da API (/mcp em server.ts, mesmo Bun.serve(), não um deployable à parte) —
autenticado com o OAuth do próprio dev (GitHub App 4490460, Dynamic Client Registration, RFC
7591), sem SCAFFOLD_GITHUB_TOKEN nem o binário scaffold no ambiente de quem só quer
onboardar/instanciar/remover algo.
Conectar (Claude Code)
claude mcp add --transport http scaffolder https://scaffolder-api.prophub.tech/mcp
claude mcp list # "scaffolder: ✔ Connected" depois do handshake OAuth no navegadorRequer ser membro da org quanthubbr no GitHub (isAuthorized(login), mcp/authz.ts) — quem
não for consegue conectar, mas as tools que mutam (ver coluna abaixo) recusam com "não está
autorizado a disparar ações que mutam via MCP".
As 3 tools (src/web/mcp/tools.ts)
O comentário no topo do arquivo ainda fala em "sete tools" (C1: list_blueprints/
get_blueprint_params/preview_stack/submit_stack, embrulhando rotas de tenant/Blueprint que
já existiam antes da ADR-0012) — as quatro do C1 foram removidas junto com o caminho de tenant, e
registerTools() só registra as três do C2 hoje:
| Tool | O que faz | Muta? |
|---|---|---|
detect_app | Detecta Forma/Instâncias de um repo avulso, com evidências (nunca resolve ambiguidade em silêncio) | Não |
init_app | Onboarding — gera app.yaml e abre PR no repo do app. Idempotente | Sim |
register_app | Registro — valida o Contrato, confere o render e aplica um AppPlacement no cluster (ADR-0011). Exige namespace, imageRepository, imageTag | Sim |
Nenhuma tool aprova/mergeia PR — decisão deliberada (ADR-0012, decisão 3). Isso é sempre a
página web (/pr/{owner}/{repo}/{number}, linkada pela notificação do Slack) ou o GitHub direto;
nunca uma tool que um agente possa acionar sozinho.
As skills (.claude/skills/)
| Skill | Pra quem | O que faz |
|---|---|---|
onboardar-app | Dev sem experiência em Kubernetes | Entrevista em linguagem de negócio, chama só as 3 tools acima — nunca mostra YAML/kubectl/Crossplane |
gerar-blueprint | Quem já entende Crossplane/schema | Cria um Blueprint novo pro catálogo — descreve usar scaffold stack --preview/checkout local. Achado do Grupo H (2026-08-19): core/stack.ts/unstack.ts não existem mais no binário (removidos junto com o caminho de tenant, ADR-0012) — o mecanismo que esta skill descreve está quebrado, não só desatualizado. Não corrigido ainda; avaliar com a plataforma antes de usar. |
remover-ambiente | Quem já entende Crossplane/schema | Descreve usar scaffold unstack pra tenant inteiro. Mesmo achado: o comando não existe mais. O caminho manual (git/gh/kubectl pra app avulso) continua sendo o único que ainda funciona. |
Instalar as skills
As skills vivem versionadas aqui, em tools/scaffolder/.claude/skills/<nome>/SKILL.md, dentro do
plat-eng-platform (a fusão da ADR-0012 acabou com plat-eng-scaffolder como checkout
separado). Ficam disponíveis automaticamente pro Claude Code quando o cwd está dentro deste
checkout (escopo de projeto). Pra ficar disponível em qualquer sessão/repo (ex. abrindo a
partir de plat-eng-bootstrap), copiar as três pro escopo global do Claude Code:
# se ainda não tiver um checkout deste repo, cloná-lo primeiro:
git clone git@github.com:quanthubbr/plat-eng-platform.git
cd plat-eng-platform/tools/scaffolder # se já tinha um checkout, `git pull` antes de copiar
for skill in onboardar-app gerar-blueprint remover-ambiente; do
mkdir -p ~/.claude/skills/"$skill"
cp .claude/skills/"$skill"/SKILL.md ~/.claude/skills/"$skill"/SKILL.md
doneOu uma por vez:
mkdir -p ~/.claude/skills/onboardar-app && cp .claude/skills/onboardar-app/SKILL.md ~/.claude/skills/onboardar-app/SKILL.md
mkdir -p ~/.claude/skills/gerar-blueprint && cp .claude/skills/gerar-blueprint/SKILL.md ~/.claude/skills/gerar-blueprint/SKILL.md
mkdir -p ~/.claude/skills/remover-ambiente && cp .claude/skills/remover-ambiente/SKILL.md ~/.claude/skills/remover-ambiente/SKILL.mdCópia simples, não symlink — não há sincronização automática nem versionamento da cópia global.
Depois que uma das três mudar aqui (git pull trazendo um SKILL.md novo), repetir o cp
daquela skill específica pra atualizar a instalação global — nada avisa sozinho que a cópia
global ficou desatualizada.
Rodando cada casca localmente
CLI: só SCAFFOLD_GITHUB_TOKEN pros comandos de escrita (bump/init); register/
promote/status pedem credencial de cluster, não de GitHub (ver seção da CLI acima).
Webhook (bun src/app.ts): GITHUB_WEBHOOK_SECRET, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY
(PEM, aceita \n literal). Não precisa de SCAFFOLD_GITHUB_TOKEN nem de installation ID fixo — o
ID vem do payload de cada evento.
API (bun src/web/server.ts): GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY (mesmos do webhook),
GITHUB_APP_INSTALLATION_ID (aqui sim fixo — sem payload de evento pra ler dinamicamente),
GITHUB_OAUTH_CLIENT_ID + GITHUB_OAUTH_CLIENT_SECRET (login do usuário, mesmo App, não um OAuth
App separado), DASHBOARD_SESSION_SECRET (cifra o cookie de sessão), ARGOCD_API_TOKEN (+
ARGOCD_BASE_URL, default o DNS interno do cluster) + credencial de cluster pras rotas de
AppPlacement (mesma da CLI).
Notificação no Slack (core/notify/slack.ts, ADR-0012 Grupo E)
SLACK_BOT_TOKEN (token de bot com escopo chat:write) e SLACK_CHANNEL (ID ou nome do canal)
— opcionais, e usados só por init (Onboarding), o único comando que ainda abre PR e tem o que
anunciar. register não notifica: desde a ADR-0011 ele não abre mais PR, aplica direto no
cluster e devolve o resultado síncrono pra quem chamou. Sem as duas variáveis, init continua
abrindo PR normalmente; a notificação é só pulada e logada uma vez, nunca erro fatal (diferente do
padrão de credencial obrigatória do OAuth de usuário acima). Ação do usuário, não deste código:
criar o app/bot do Slack, o canal de destino, e provisionar as duas variáveis como secret
(runbook ADR-0012, Grupo E1) — ainda não existem em nenhum ambiente.