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 --capabilities

Uso rápido: `--estatico`

runner manifest validate <app> --estatico < candidato.deploy.yml

O --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 status nao_consultada.
  • Continuam valendo parse, semantica, segredo_literal, credencial_plataforma, mascara, identidade, required, assets e o componente lint.
  • O vermelho nome_base_de_outra_app (2.185.0+, componente identidade) também depende da lista de apps do servidor e não roda no --estatico. Sem --estatico, ele sai com gravidade: "vermelho", caminho: "/project" e fonte: "servidor".
  • O orfaos depende da lista de apps do servidor. No --estatico ele sai como não verificável: toda instância removida vira orfaos_nao_verificavel (amarelo).
  • Sem --estatico, o validate roda 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.com
runner manifest validate loja --estatico < candidato.deploy.yml

O 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 de linha/coluna até antes de fim.
  • Sem posição. caminho, linha, coluna e fim vêm null quando o achado não tem campo. Exemplos: YAML que nem parseia (vale a posição do erro do parser, se houver) e required sem 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 caminho aponta onde o campo entraria.
  • caminho com a chave escrita. Um type: 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.

  1. A edição troca o trecho de linha/coluna até fim (exclusivo) por texto.
  2. A coluna é em code points. Converta para o índice do seu editor.
  3. Uma inserção tem largura zero: fim igual ao início.
  4. 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.yml

As 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).

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 :debug traz o busybox e fica de fora);
  • a base declarada é scratch;
  • o histórico da imagem é todo de Dockerfile, sem RUN e 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 em runner-cron.log. Ela começa com [<app>/<instância>], em que <app> é o diretório da app. O project nã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_success ou deploy_failure e o deploy-audit.json (o que o runner logs history lê) trazem o campo lint, uma lista de itens com codigo, gravidade, caminho e linha. Sem achado, o campo não aparece.

  • Com -o json, o envelope after do deploy traz o mesmo campo lint, 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

By Borlot.com.br on 04/11/2026