Lint do manifesto (2.183.0+, servidor e deploy em 2.186.0)
O `runner manifest validate` aponta cada achado no texto do
.deploy.yml: o campo, a linha, a coluna, a causa e, quando a correção é
mecânica, as edições que a aplicam. O editor do CCS usa esse formato para
marcar o trecho e oferecer a correção.
O runner anuncia a capability manifest.lint.v1 em runner --capabilities.
Sem ela, o runner não tem diagnosticos[] nem --estatico.
runner --capabilitiesUso rápido: `--estatico`
runner manifest validate <app> --estatico < candidato.deploy.ymlO --estatico lê o texto do candidato e os arquivos da app no disco (o
pull/, a raiz vigente e os nomes de chave do .env, do .secrets e do
store de segredos). Não consulta Docker, Traefik nem DNS, não pega a trava da
app e não roda o preflight. Serve para validar a cada tecla. A lista de redes
compartilhadas é fixa no runner.
- As checagens de servidor não rodam, inclusive as quatro regras de
fonte: servidor(2.186.0+). As fontes saem com statusnao_consultada. - Continuam valendo
parse,semantica,segredo_literal,credencial_plataforma,mascara,identidade,required,assetse o componentelint. - O vermelho
nome_base_de_outra_app(2.185.0+, componenteidentidade) também depende da lista de apps do servidor e não roda no--estatico. Sem--estatico, ele sai comgravidade: "vermelho",caminho: "/project"efonte: "servidor". - O
orfaosdepende da lista de apps do servidor. No--estaticoele sai como não verificável: toda instância removida viraorfaos_nao_verificavel(amarelo). - Sem
--estatico, ovalidateroda tudo, como antes.
O formato do diagnóstico
O relatório ganha o campo aditivo diagnosticos[]. Os campos de antes
(components[], codigos[], overall e os exits) não mudam. Os achados que
já existiam saem também em diagnosticos[], com posição.
Exemplo real. O candidato abaixo usa um healthcheck de outra ferramenta:
project: loja
system: sys
type: sys
image: python:3.12-slim
port: 8080
healthcheck:
probe: tcp
port: 8080
instances:
production:
domain: loja.example.comrunner manifest validate loja --estatico < candidato.deploy.ymlO comando sai com exit 14. Entre os itens de diagnosticos[]:
{
"codigo": "healthcheck_dialeto_alheio",
"gravidade": "vermelho",
"componente": "parse",
"fonte": "texto",
"caminho": "/healthcheck/probe",
"linha": 7,
"coluna": 3,
"fim": {
"linha": 7,
"coluna": 13
},
"mensagem": "`healthcheck.probe` é de outra ferramenta: o runner não tem `probe`, `port` nem `type` no healthcheck e recusa o manifesto",
"sugestao": {
"texto": "use a forma do runner: `tcp: <porta>` para sonda TCP, `path: /rota` para HTTP (na porta do container), `cmd: [...]` para comando",
"aplicar": [
{
"linha": 7,
"coluna": 3,
"fim": {
"linha": 7,
"coluna": 13
},
"texto": "tcp: 8080"
},
{
"linha": 8,
"coluna": 1,
"fim": {
"linha": 9,
"coluna": 1
},
"texto": ""
}
]
},
"desde": "2.183.0",
"ref": "curadoria-2026-11-01#2"
}Aplicadas as duas edições ao texto original, o healthcheck vira tcp: 8080
e o manifesto parseia.
| Campo | Significado |
|---|---|
codigo |
Código estável da regra. Nunca é reaproveitado |
gravidade |
verde, amarelo ou vermelho |
componente |
Componente do relatório onde o achado também aparece (lint, parse, ...) |
fonte |
De onde a regra lê: texto (só o YAML), app (arquivos da app) ou servidor (consultas vivas) |
caminho |
JSON Pointer sobre o YAML, com a chave como está escrita (alias incluído) |
linha, coluna |
Início do trecho, a partir de 1 |
fim |
{"linha","coluna"} do fim do trecho, exclusivo |
mensagem |
Causa em uma frase. Nunca traz valor de segredo |
sugestao |
texto com a correção e, quando mecânica, aplicar |
desde |
Versão do runner que introduziu o código |
ref |
Referência da evidência da regra |
- Coluna em code points. A coluna conta caracteres Unicode, não bytes. Um acento antes do trecho vale uma coluna.
fimé exclusivo. O trecho vai delinha/colunaaté antes defim.- Sem posição.
caminho,linha,colunaefimvêmnullquando o achado não tem campo. Exemplos: YAML que nem parseia (vale a posição do erro do parser, se houver) erequiredsem valor no.env. Mostre o achado no topo do editor. - Campo ausente. Quando a regra acusa o que falta, a posição é a do bloco
pai e o
caminhoaponta onde o campo entraria. caminhocom a chave escrita. Umtype:no arquivo vira/type, não/app_type.
As edições de texto (`sugestao.aplicar`)
O campo aplicar é opcional. Quando existe, é uma lista de edições no estilo
TextEdit do LSP:
{"linha": 7, "coluna": 3, "fim": {"linha": 7, "coluna": 13}, "texto": "tcp: 8080"}O editor aplica as edições sobre o texto original do arquivo, não sobre o YAML parseado. Assim, os comentários, a ordem das chaves, as aspas e o fim de linha (inclusive CRLF) ficam como estavam. Só as linhas da edição mudam.
- A edição troca o trecho de
linha/colunaatéfim(exclusivo) portexto. - A coluna é em code points. Converta para o índice do seu editor.
- Uma inserção tem largura zero:
fimigual ao início. - Aplique várias edições da maior posição para a menor. Elas não se sobrepõem.
O runner só oferece aplicar depois de conferir três coisas: o texto editado
parseia, passa na validação do runner e o achado sumiu. Se qualquer uma
falhar, a sugestão vem só com o texto. A correção que não é mecânica também
vem sem aplicar.
Gravidade
| Gravidade | Efeito |
|---|---|
| vermelho | O editor recusa salvar e o `manifest pin` recusa o candidato |
| amarelo | O pin exige o código em --aceita-avisos |
runner manifest pin <app> --aceita-avisos rede_alias_compartilhada,secret_vazio < candidato.deploy.ymlAs regras vermelhas de parse só acusam o que o parser já recusa hoje, e
somam posição e sugestão ao invalid_manifest. Nenhuma regra do lint
bloqueia o deploy: ele roda no validate, no pin e, só como aviso, no
deploy (ver O lint no deploy).
Catálogo
| Código | Gravidade | Componente | Fonte | Desde |
|---|---|---|---|---|
version_git_tag_em_dist |
amarelo | lint |
app | 2.183.0 |
version_file_nao_estruturado |
vermelho | lint |
app | 2.183.0 |
version_file_inexistente |
amarelo | lint |
app | 2.183.0 |
image_mode_obsoleto |
amarelo | lint |
texto | 2.183.0 |
imagem_sem_image_mode |
amarelo | lint |
texto | 2.183.0 |
caddy_sem_caddyfile |
amarelo | lint |
app | 2.183.0 |
healthcheck_dialeto_alheio |
vermelho | parse |
texto | 2.183.0 |
healthcheck_cmd_string |
vermelho | parse |
texto | 2.183.0 |
rede_alias_compartilhada |
amarelo | lint |
texto | 2.183.0 |
rede_alias_aninhado |
vermelho | parse |
texto | 2.183.0 |
secret_prompt_sem_required |
amarelo | lint |
app | 2.183.0 |
secret_vazio |
amarelo | lint |
app | 2.183.0 |
bloco_removido |
amarelo | lint |
app | 2.183.0 |
healthcheck_imagem_sem_shell |
amarelo | lint |
servidor | 2.186.0 |
healthcheck_localhost_alpine |
amarelo | lint |
servidor | 2.186.0 |
imagem_nao_verificavel |
amarelo | lint |
servidor | 2.186.0 |
url_dominio_de_outra_app |
amarelo | lint |
servidor | 2.186.0 |
A coluna Fonte diz o que a regra lê. texto e app rodam no
--estatico e no deploy. servidor consulta o Docker e a lista de apps do
servidor: roda só no validate e no pin sem --estatico. A coluna Componente diz onde o achado aparece em
components[]: parse para o que explica um invalid_manifest, lint para o
resto.
Regras
Versão
`version_git_tag_em_dist` (amarelo)
Detecta. version.source: git-tag numa source dist (também
source.branch: dist ou github.dist_branch declarado) cujo pull/ não tem
nenhuma tag alcançável a partir do HEAD. O deploy não acha versão, e a versão
nunca muda. Sem o pull/ clonado, a regra não dispara. Com source
type: tag também não: o deploy usa o nome da tag como versão.
Corrija. Troque por version.source: git-commit (cada commit novo na dist
vira versão nova). A sugestão traz essa edição. A outra saída é fazer a dist
receber o git merge main com a tag.
`version_file_nao_estruturado` (vermelho)
Detecta. version.file que aponta para um arquivo de código (.py,
.rs, .ex, .js, .go, .sh e semelhantes). Com o arquivo no pull/,
detecta também conteúdo com mais de uma linha (depois de tirar os espaços das
pontas). Nesses casos o deploy usa o arquivo inteiro como versão. O nome do
container estoura (ENAMETOOLONG). Um version.txt de uma linha vale, curta
ou longa. Com source type: tag a regra não dispara: a versão é a tag.
Corrija. Aponte para um version.json com {"version": "1.2.3"} e
field: version, ou use version.source: git-commit.
`version_file_inexistente` (amarelo)
Detecta. version.file que não existe nos caminhos que o deploy procura:
o pull/, src/*/current, a raiz da app e current/. Com source de arquivo
(local, https, rsync), só o pull/ conta, como no deploy. A mensagem
lista os arquivos de versão achados. Sem o pull/ clonado, a regra confere a
raiz da app e os caminhos legados, como o deploy. Com source type: tag a
regra não dispara: a versão é a tag.
Corrija. Aponte para o caminho certo ou use version.source: git-commit.
Imagem e build
`image_mode_obsoleto` (amarelo)
Detecta. image_mode: dockerfile. O runner o lê como self-contained e
avisa a cada deploy.
Corrija. Troque a linha pelo bloco build: (context: . e
dockerfile: Dockerfile). A sugestão traz a edição e não mexe no type.
`imagem_sem_image_mode` (amarelo)
Detecta. type: image com image: e sem image_mode nem build:. O
padrão é bind-mount: o runner monta o checkout em /app por cima da imagem
pronta.
Corrija. Declare image_mode: prebuilt para a imagem rodar como veio. Se a
app depende do código montado em /app, declare image_mode: bind-mount.
`caddy_sem_caddyfile` (amarelo)
Detecta. front-static com a imagem oficial do Caddy (caddy ou
library/caddy, com ou sem docker.io/ e com qualquer tag) e sem Caddyfile
montado em volumes:. O site serve a página padrão do Caddy e o deploy sai
verde. A regra dispara com ou sem Caddyfile no pull/. Não dispara com
imagem própria, com build: declarado nem com image_mode prebuilt ou
self-contained: nesses casos a imagem já traz a configuração.
Corrija. Monte o Caddyfile em volumes: e aponte healthcheck.path para
uma rota do site. Com o Caddyfile no pull/, a sugestão traz a edição
(quando o volumes: está em lista de bloco). Sem ele, a sugestão manda criar
o arquivo e não traz aplicar.
Healthcheck
`healthcheck_dialeto_alheio` (vermelho, `parse`)
Detecta. Chave de outra ferramenta em healthcheck: probe, port ou
type. O runner não as tem e recusa o manifesto.
Corrija. Use a forma do runner: tcp: <porta> para sonda TCP, path: /rota para HTTP ou cmd: [...] para comando. Quando a conversão é
inequívoca (probe: tcp com port), a sugestão traz a edição.
`healthcheck_cmd_string` (vermelho, `parse`)
Detecta. healthcheck.cmd escrito como texto. O runner exige lista.
Corrija. Escreva cmd: ["sh", "-c", "<comando>"] ou os argumentos
separados. A sugestão embrulha o texto em sh -c quando o valor está em uma
linha.
Rede
`rede_alias_compartilhada` (amarelo)
Detecta. Alias em uma rede compartilhada do servidor. Funciona hoje, mas toda app da rede resolve o nome, e outra app que declare o mesmo alias na mesma rede divide o DNS com esta. Vale também para o alias inline.
Corrija. Crie uma rede do projeto, ponha nela as apps que chamam o nome e
declare o alias nela. Se o risco é aceito, use --aceita-avisos rede_alias_compartilhada. Não há aplicar.
`rede_alias_aninhado` (vermelho, `parse`)
Detecta. network_aliases dentro de instances.<nome>. O bloco é do topo
do manifesto e o runner recusa.
Corrija. Mova network_aliases: para o topo do arquivo (vale para todas as
instâncias). A sugestão traz a edição quando há uma instância só.
Secrets
`secret_prompt_sem_required` (amarelo)
Detecta. secrets.K com pergunta ({{::K}}) sem valor padrão, que não
está em required: e que não tem valor nas fontes do deploy. Se o registro não
responder, o deploy segue com a chave vazia.
Corrija. Acrescente a chave em required:. A sugestão traz a edição.
`secret_vazio` (amarelo)
Detecta. secrets.K: "" sem valor na app. O container recebe a chave
vazia e o runner a recria a cada deploy.
Corrija. Apague a chave do manifesto. Se a app precisa do valor, grave com
runner env set <app> K '<valor>' --secret. A sugestão traz a edição de
apagar.
Remoção
`bloco_removido` (amarelo)
Detecta. O candidato tira de volumes, assets, network_aliases ou de
chaves de environment o que o manifesto vigente da app tem. É o desenho do
merge na dist que apaga um bloco sem querer. A mensagem lista só nomes. Chave
de env que foi para secrets ou para a instância não conta como removida.
Corrija. Confira se a remoção é intencional. Se for, aceite com
--aceita-avisos bloco_removido. Se não, devolva o bloco. Não há aplicar.
Servidor (2.186.0+)
Estas regras consultam o servidor onde o runner está. Rodam no validate e no
pin sem --estatico, nunca no deploy. Todas são amarelas.
`healthcheck_imagem_sem_shell` (amarelo)
Detecta. Uma sonda que o runner roda dentro do container (path, tcp ou
cmd) numa imagem que prova não ter shell. O runner executa a sonda com
sh -c, e sem sh ela falha em todo ciclo: o container fica unhealthy e o
Traefik não roteia. A prova de "sem shell" é uma destas:
- o nome ou a base declarada da imagem é
distroless(a variante:debugtraz o busybox e fica de fora); - a base declarada é
scratch; - o histórico da imagem é todo de Dockerfile, sem
RUNe sem raiz de sistema.
Um HEALTHCHECK próprio da imagem que o deploy honra cala a regra, porque
nesse caso o runner não injeta a sonda do manifesto. Imagem ausente neste
servidor também cala.
Corrija. Troque a sonda por mode: external_http ou mode: external_tcp
(o runner sonda a partir do servidor, sem entrar no container), ou ponha um
HEALTHCHECK em forma exec no Dockerfile da imagem. Trocar path por tcp
não resolve: o tcp também roda com sh -c.
healthcheck:
mode: external_http
path: /health`healthcheck_localhost_alpine` (amarelo)
Detecta. Um healthcheck.cmd que chama wget (ou busybox) contra
localhost, numa imagem Alpine. O wget do busybox resolve localhost para
::1 e não tenta 127.0.0.1 depois. Se a app escuta só em IPv4, a sonda
falha. O curl tenta os dois e não é acusado. A regra reconhece Alpine pelo
nome da imagem, pela base declarada, pelo minirootfs ou por um apk <comando>
no histórico.
Se o container da app já está no ar nesta instância, healthy, rodando essa
mesma sonda, a regra não acusa: a app escuta também em IPv6 (dual-stack) e
responde em ::1. Sem essa prova (app nunca implantada, container unhealthy
ou ainda subindo, ou rodando outra sonda), o aviso sai e diz que o container
pode ficar unhealthy. Ele não afirma, porque depende de onde a app escuta.
Corrija. Troque localhost por 127.0.0.1 no cmd. A sugestão traz a
edição.
`imagem_nao_verificavel` (amarelo)
Detecta. Uma regra de imagem precisava inspecionar a imagem da app e o Docker não respondeu dentro de 2 s. O runner não conclui "sem problema" com a fonte fora do ar. O achado é um só, para as duas regras de imagem. Imagem que não está no servidor (app pausada ou nunca implantada) não é fonte fora: as regras de imagem ficam caladas e este código não sai.
Corrija. Não há o que corrigir no manifesto. Veja se o Docker está de pé e
rode o validate de novo. Com o Docker fora, o pin exige o código em
--aceita-avisos.
`url_dominio_de_outra_app` (amarelo)
Detecta. Um valor de environment do tipo http(s)://<host> em que o
<host> é o domínio público de outra app deste servidor, servido agora por ele
(app pausada não conta). O container que chama o próprio servidor pelo domínio
público passa pelo Traefik e, se o domínio usa vpn-only, é recusado. A regra
só olha chave com cara de chamada interna: *_API*, *_BASE_URL, *_HOST,
*_ENDPOINT, *_SERVICE*, *_INTERNAL* e *_UPSTREAM*. Chave de link
público (S3, STORAGE, MEDIA, CDN, PUBLIC, CALLBACK, WEBHOOK,
ISSUER, SSO, SITE, FRONT, CORS, VITE_, NEXT_PUBLIC e
semelhantes) não é acusada. Também não dispara quando esta app ou a outra é
front, nem com valor de template. A mensagem cita só o host.
Corrija. Chame a outra app pelo nome na rede. Se ela declara um alias numa
rede em comum, a sugestão traz http://<alias>:<porta>. Senão, declare
network_aliases: {<rede privada>: [<nome_base>]} na outra app, ponha as duas
apps nessa rede e use http://<nome_base>:<porta>. Se a chamada é mesmo
externa de propósito, aceite com --aceita-avisos url_dominio_de_outra_app.
Não há aplicar.
O lint no deploy
A partir da 2.186.0, o deploy roda as regras de fonte texto e app sobre o
.deploy.yml da app, na etapa PREPARE. As de fonte servidor não rodam, e a
bloco_removido também não (não há manifesto vigente para comparar).
O lint nunca bloqueia o deploy. Vermelho ou amarelo, o deploy segue. O que já falhava antes continua falhando pelo motivo de sempre.
Cada achado vira uma linha na etapa:
⚠ lint image_mode_obsoleto (/image_mode, linha 2): `image_mode: dockerfile` é obsoleto → troque pelo bloco `build:`No
runner deploy, a linha sai na saída da etapa.No cron (
fetch --deploy), a linha sai no log do cron como aviso (warn), para aparecer emrunner-cron.log. Ela começa com[<app>/<instância>], em que<app>é o diretório da app. Oprojectnão serve aqui, porque várias apps podem ter o mesmo:WARN [loja-sys/production] ⚠ lint image_mode_obsoleto (/image_mode, linha 2): …O evento
deploy_successoudeploy_failuree odeploy-audit.json(o que orunner logs historylê) trazem o campolint, uma lista de itens comcodigo,gravidade,caminhoelinha. Sem achado, o campo não aparece.Com
-o json, o envelopeafterdo deploy traz o mesmo campolint, no sucesso e na falha.
"lint": [
{"codigo": "image_mode_obsoleto", "gravidade": "amarelo", "caminho": "/image_mode", "linha": 2}
]Os itens do evento não levam mensagem nem sugestão: só a posição. Para a causa
e a correção, rode o runner manifest validate.
Se o lint falhar por conta própria (YAML que não parseia, erro interno), o deploy segue sem avisos e registra o motivo no log, sem citar o manifesto.
Veja também
- `runner manifest`: ler, gravar, fixar, contexto e validação
- `runner manifest schema`: JSON Schema do
.deploy.yml