`runner manifest`

Exibe o conteúdo do .deploy.yml de uma app registrada. A partir de v2.23.0 suporta --resolved para renderizar {{::Var}} templates antes do output.

Sintaxe

runner manifest <app> [--resolved] [-o json] [-x]
Flag Default Descrição
--resolved false Renderiza {{::Var}} usando .env + .secrets + secrets.enc
-o json — Output em JSON (parse do YAML)
--json false Obsoleto: use -o json. Sai na v3.0.0
-x, --exact false Só casamento exato do nome da app

Modo padrão — `.deploy.yml` cru

runner manifest meu-app

Imprime o YAML exatamente como está no disco, com templates não-resolvidos:

project: meu-app
system: sys
environment:
  BASE_DOMAIN: "{{::Domain?meu-app.com.br}}"
secrets:
  DB_PASSWORD: "{{::DB_PASSWORD}}"

`--resolved` — simula o que entra no container (v2.23.0+)

Renderiza os templates usando os valores realmente armazenados no app dir. Útil para validar o deploy sem rodar de fato — paridade com o simulate_deploy do wizard MCP.

runner manifest meu-app --resolved

Output:

project: meu-app
system: sys
environment:
  BASE_DOMAIN: "meu-app.com.br"
secrets:
  DB_PASSWORD: "<UNRESOLVED:DB_PASSWORD>"

Origem dos valores (precedência)

A partir de v2.23.3, o renderer consulta as 3 fontes em ordem:

  1. .runner/secrets.enc (decriptado com ckey) — backup encriptado
  2. .env plaintext — sobrescreve secrets.enc se presente
  3. .secrets plaintext — sobrescreve .env se presente

Templates {{::Var?default}} usam o default quando nenhuma das 3 fontes resolveu a key.

Templates não-resolvidos

Quando uma key não tem valor e nem default, o renderer substitui por <UNRESOLVED:KEY> e emite warning no stderr:

⚠ Unresolved templates (left as <UNRESOLVED:KEY>): DB_PASSWORD, API_KEY

Isso facilita identificar quais secrets você ainda precisa preencher antes do deploy.

Placeholders internos `__*` (v2.175.2+)

{{::__commit}}, {{::__commit_short}} e {{::__version}} são avaliados com a mesma lógica do deploy, a partir do estado atual da app (instância registrada, HEAD do clone ou do pull/, revisão da source). Quando ainda não há valor (app nunca implantada, sem revisão, sem versão calculável), o marcador sai como <no-deploy:__commit> — não é falha e não conta como UNRESOLVED. O git só roda se o manifesto usa __commit.


Output JSON

Com --json, runner re-parseia o YAML rendered (ou cru) e emite estrutura:

runner manifest meu-app --resolved --json
{
  "project": "meu-app",
  "system": "sys",
  "environment": {
    "BASE_DOMAIN": "meu-app.com.br"
  },
  "secrets": {
    "DB_PASSWORD": "<UNRESOLVED:DB_PASSWORD>"
  }
}

`manifest show` e `manifest write` — ler e gravar o `.deploy.yml`

Usados pelo CCS para editar o manifesto. A saída é sempre JSON (desde a 2.180.1, inclusive o write sem -y; veja abaixo).

runner manifest show  <app> [-i <instância>] [-x] [--local]
runner manifest write <app> --base-sha256 <sha> -y [-i <instância>] [-x] [--local] < novo.deploy.yml
  • Sem --local, o show lê o manifesto da branch da app no repositório (git fetch antes) e o write commita e faz push nela. O deploy vem depois, pelo cron ou por runner deploy.
  • Com --local, os dois usam o .deploy.yml da raiz da app no servidor, sem git. Desde a 2.178.0 o write --local fixa o manifesto: ele vale em todos os próximos deploys, inclusive os do cron, até ser descartado (ver "Manifesto fixado no servidor", abaixo). Até a 2.177.x valia só no próximo deploy --local --force, e um deploy normal voltava ao manifesto do repositório. Desde a 2.180.0 o write --local também passa pela validação do candidato: vermelho sai 14, amarelo grava e volta em avisos_validacao (ver "write --local: vermelho recusa, amarelo grava", abaixo).
  • --base-sha256 é o sha256 devolvido pelo show. Se o manifesto mudou desde a leitura, o write recusa com manifest_conflict.
  • Com um manifesto fixado no servidor, o write sem --local recusa com manifesto_fixado (exit 13): o repositório não é o manifesto vigente. Com o pin.json ilegível ele também sai 13 (recusa por segurança), e o message traz o erro de leitura.
  • O write recebe o manifesto pelo STDIN, então não pergunta nada: quando há algo para gravar, exige -y. Manifesto inválido ou conflito respondem com o exit do contrato (10/11/12) mesmo sem -y.
  • Sem -y, com algo para gravar, o write responde exit 1 com {"error": "confirmacao_exigida", "message", "detail"}, como o write --local e o pin. Na 2.180.0 o write sem --local saía com o erro em texto no stderr, sem JSON; corrigido na 2.180.1.

Validação no `write` (2.177.0+)

O write valida o manifesto como o deploy valida: parse, campos desconhecidos e as regras de cada instância (rotas, routing e release). O que o deploy recusaria é recusado na escrita, com invalid_manifest, antes de tocar o arquivo ou o git. Exemplos:

  • routing.tls: acme-dns sem routing.provider;
  • o mesmo path_prefix duas vezes no mesmo domínio, ou duas rotas sem path_prefix no mesmo domínio;
  • release: inválido (sem command, duração inválida, target: sem build:).

Até a 2.176.x o write só conferia o parse, e um manifesto assim era gravado (e commitado) para falhar só no deploy. Os avisos do deploy (ex.: provider ignorado com tls: acme-http) não impedem a escrita.

O CCS sabe que o runner faz essa validação pela capacidade manifest.validate_load.v1 em runner --capabilities.

Erros

error Exit Quando
invalid_manifest 10 O manifesto não passa na validação acima
manifest_conflict 11 O manifesto mudou desde o show (current_sha256 traz o atual)
remote_unavailable 12 Sem clone, fetch ou push falhou
invalid_manifest_path 1 O manifest_path gravado no registro da app é absoluto, tem .. ou é vazio (state antigo). Nada é lido nem gravado. No show e no write, só sem --local; também atinge status, pin e discard
{
  "error": "invalid_manifest",
  "message": "instância 'production': routing.tls: acme-dns exige `routing.provider` (ex.: cloudflare)",
  "detail": "instância 'production': routing.tls: acme-dns exige `routing.provider` (ex.: cloudflare)"
}

message existe desde a 2.177.0, no mesmo formato dos outros erros do runner. detail continua, com o mesmo texto.

Backups

Antes de gravar, o write guarda o manifesto anterior em <app>/.runner/manifest-backups/. Desde a 2.177.0 os backups são criados com permissão 600 e a pasta com 700.

Em app de monorepo (registrada com manifest_path), o show e o write usam o manifesto do serviço, não o .deploy.yml da raiz do repositório. Se o manifest_path registrado for inválido, eles recusam com invalid_manifest_path em vez de cair no .deploy.yml da raiz (que, num monorepo, é de outro serviço). Desde a 2.177.0, o runner add e o runner adopt recusam --manifest-path absoluto, com .. ou vazio.


Manifesto fixado no servidor (2.178.0+)

Fixar é salvar um .deploy.yml neste servidor, sem passar pelo repositório. O fluxo tem três passos, mais a saída pelo repositório:

  1. Salvar (manifest pin). Só grava. Não deploya e não toca container, rota nem rede.
  2. Vale nos próximos deploys. Todo deploy usa o fixado, inclusive os do cron, até ele ser descartado. Para aplicar na hora, use runner deploy <app> --force.
  3. Descartar volta ao repo (manifest discard). A raiz da app recebe de novo o manifesto do repositório, que vale a partir do próximo deploy.
  4. Publicar leva o fixado ao repo (manifest publish, 2.181.0+). O fixado vira um commit na branch dist e a app volta a seguir o repositório, agora com aquele conteúdo.
runner manifest status  <app> [-i <instância>] [-x]
runner manifest pin     <app> --base-sha256 <raiz.sha256> -y [-i <instância>] [-x] [--base-commit <commit>] \
                        [--nota <texto>] [--actor <quem>] [--on-repo-change deploy|hold] \
                        [--aceita-avisos <a,b>] < novo.deploy.yml
runner manifest discard <app> --sha256 <local.sha256> -y [-i <instância>] [-x] [--actor <quem>]
runner manifest publish <app> --sha256 <local.sha256> -y [-i <instância>] [-x] [--actor <quem>]

A saída é sempre JSON, de sucesso ou de erro. O manifesto entra pelo STDIN (até 64 KiB), por isso a escrita exige -y.

O pin, o discard e o write --local esperam até 10 s pela trava da app quando há deploy ou cron em curso. Passado esse tempo, saem com app_lock_ocupado.

O CCS sabe que o runner tem esses comandos pela capacidade manifest.pin.v1 em runner --capabilities; o publish, pela manifest.publish.v1 (2.181.0+).

Exemplo do fluxo

# 1. ler: o sha256 da raiz é a âncora da escrita
runner manifest status loja | jq -r .raiz.sha256

# 2. salvar
runner manifest pin loja --base-sha256 <sha> --nota "porta nova" --actor ccs:ana -y < novo.deploy.yml

# 3. aplicar agora (sem isto, vale no próximo deploy)
runner deploy loja --force

# 4a. voltar ao repositório
runner manifest discard loja --sha256 "$(runner manifest status loja | jq -r .local.sha256)" -y
runner deploy loja --force

# 4b. ou levar o fixado ao repositório (2.181.0+)
runner manifest publish loja --sha256 "$(runner manifest status loja | jq -r .local.sha256)" --actor ccs:ana -y

Enquanto houver um manifesto fixado

Os arquivos ficam em <app>/.runner/manifest-pin/, com a pasta em 700 e os arquivos em 600:

  • pin.json;
  • o manifesto vigente;
  • a base, isto é, o manifesto do repo de onde ele partiu;
  • um histórico com as últimas 20 entradas.

A própria raiz <app>/.deploy.yml fixada também fica em 600. O discard a devolve a 644. Se a pasta .runner/manifest-pin/ for removida à mão, a raiz fica em 600 até o próximo deploy ou fetch reescrevê-la.

Nada reescreve a raiz <app>/.deploy.yml por fora do pin e do discard (o publish não reescreve: a raiz já é o fixado, e só o modo volta a 644):

  • o fetch não copia o manifesto do repositório. Quando ele mudou, só marca a divergência (veja on_repo_change abaixo);
  • o deploy não copia do pull/;
  • a recuperação de uma raiz ilegível usa o manifesto fixado;
  • runner upgrade e manifest write sem --local recusam com manifesto_fixado (exit 13). Com o pin.json ilegível os dois também recusam: o write sai 13; o upgrade sai 1, com manifesto_fixado_ilegivel na mensagem;
  • runner config-backups restore pula a raiz com aviso e restaura o resto (files_skipped no JSON).

Raiz adulterada. Se algo de fora do runner reescrever a raiz (um cp, um editor), ela deixa de bater com o fixado. Nesse caso o deploy, o rollback e o cron se recusam com manifesto_fixado_adulterado (exit 17), antes de qualquer efeito e sem restaurar nada. O cron emite o evento manifesto_fixado_adulterado uma vez por ocorrência.

O status mostra estado: "adulterado". Para resolver, há dois caminhos:

  • voltar ao repo: manifest discard;
  • manter o fixado: devolver o conteúdo fixado à raiz: cp <app>/.runner/manifest-pin/vigente.deploy.yml <app>/.deploy.yml.

O pin com a raiz adulterada também sai 17: ele não serve para resolver.

Depois de um downgrade. Um runner anterior à 2.178.0 não conhece o manifesto fixado: o fetch e o deploy dele reescrevem a raiz com o manifesto do repo. Ao voltar para a 2.178 ou mais nova, a app fixada aparece adulterada (17): o cron a pula e todo deploy dela sai 17 até alguém usar um dos dois caminhos acima.

pin.json ilegível. pin e discard saem com manifesto_fixado_ilegivel (exit 1) e o cron pula a app, com o evento manifesto_fixado_ilegivel. Se o formato é mais novo que o runner, atualize o runner. Se o arquivo está corrompido, confira o .runner/manifest-pin/vigente.deploy.yml (é o fixado) e remova a pasta <app>/.runner/manifest-pin/. A app volta a seguir o repo, e a raiz recebe o manifesto do repo no próximo deploy (runner deploy <app> --force aplica na hora) ou no fetch de um commit novo.

Se um pin cair no meio, o próximo pin ou discard repara o estado. Isso não dá 17.

Publicar o fixado no repositório. Desde a 2.181.0, com o manifest publish (seção abaixo). A mensagem do exit 13 e os helps de pin, write --local e discard voltam a citá-lo. Em app de source: type: tag não há publish (exit 16): o caminho de volta é o discard, e os textos dessas apps citam só ele. Entre a 2.180.0 e a 2.180.1 o comando não existia: a 2.180.0 citava um runner manifest publish inexistente, e a 2.180.1 tirou a menção.

`manifest status`

Só lê, e sai com exit 0 sempre que a app existe. Se o repositório estiver fora do ar, a resposta vem parcial, com repo: null e repo_erro. O mesmo vale para a raiz ilegível (raiz_erro) e para o pin.json ilegível (pin_erro).

Campo Conteúdo
raiz {sha256, content, content_erro, formatacao_preservada} do <app>/.deploy.yml, ou null sem leitura. raiz.sha256 é o --base-sha256 do pin
raiz_erro, pin_erro, repo_erro Por que a raiz, o pin.json ou o repo não foram lidos. null quando foram
vigente repo (sem fixado) ou local. Veja os casos especiais abaixo
estado repo, local ou adulterado. null quando não deu para ler a raiz ou o fixado
interrompido true quando um pin caiu no meio (o próximo pin ou discard repara)
local O fixado: {sha256, content, content_erro, formatacao_preservada, salvo_em, salvo_por, nota, idade_s}
base O manifesto do repo de onde o fixado partiu: {sha256, commit}
repo O manifesto atual do repositório: {sha256, commit, content, content_erro, formatacao_preservada}
repo_divergiu {sha256, commit, visto_em, campos} quando o repo mudou o manifesto depois da base, ou null
divergencia nenhuma, so_local, so_repo ou ambos
diff.base_local, diff.base_repo Diff unificado da base para o fixado e da base para o repo
campos As chaves de topo que mudaram em cada diff
aplicado O que o último deploy implantou: {sha256, versao, em}
pendente_de_aplicar true quando o vigente ainda não foi implantado. null sem fixado quando não há registro do último deploy ou a raiz não foi lida
publicavel, fixacao Se a app aceita fixar (fixacao.suportada). Quando não aceita, fixacao traz motivo e, salvo em source sem git, codigo. publicavel diz se o manifest publish funciona: false em source sem git e, desde a 2.181.0, em app de source: type: tag
on_repo_change deploy ou hold (null sem fixado)
outros_consumidores_locais Desde a 2.187.0. As outras apps deste servidor que o manifest publish afetaria, com a mesma regra e a mesma forma do campo do publish (veja abaixo). Sai antes do publish, para quem publica conferir o escopo. [] quando a app é a única, e sempre [] em source sem git. Feature manifest.status.consumidores.v1 no --capabilities

Casos especiais de vigente:

  • Source sem git (local, https, rsync, registry) e sem fixado: sai vigente: "local" com estado: "repo" e local: null. A raiz é a única fonte do manifesto, não um fixado. Um discard nesse caso sai 15.
  • pin.json ilegível: vigente, estado e divergencia saem null, com o motivo em pin_erro.

Não deduza "há fixado" de vigente: "local". O sinal é estado (local ou adulterado) ou local != null.

{
  "app": "loja", "instance": "production",
  "raiz": {"sha256": "3a51b693…", "content": "project: loja\n…port: 81\n…",
           "content_erro": null, "formatacao_preservada": true},
  "vigente": "local", "estado": "local", "interrompido": false,
  "publicavel": true, "fixacao": {"suportada": true},
  "local": {"sha256": "3a51b693…", "content": "…", "salvo_em": "2026-10-31T06:08:20Z",
            "salvo_por": "ccs:ana", "nota": "porta nova", "idade_s": 0},
  "base": {"sha256": "3de466e3…", "commit": "c1a2b3"},
  "repo": {"sha256": "83460816…", "commit": "d4e5f6", "content": "…"},
  "repo_erro": null, "repo_divergiu": null,
  "divergencia": "ambos",
  "diff": {"base_local": "@@ -2,7 +2,7 @@\n…\n-port: 80\n+port: 81\n…",
           "base_repo": "@@ -3,6 +3,8 @@\n…\n+healthcheck:\n+  path: /saude\n…"},
  "campos": {"base_local": ["port"], "base_repo": ["healthcheck"]},
  "mascarado": false,
  "aplicado": {"sha256": "83460816…", "versao": "v12", "em": "2026-10-30T22:10:04Z"},
  "pendente_de_aplicar": true,
  "on_repo_change": "deploy",
  "outros_consumidores_locais": ["loja-staging"]
}

Máscara de segredos

O status nunca devolve um valor literal que esteja sob secrets:. Isso vale para o topo e para instances.*.secrets, em raiz, local, repo e nos diffs.

O que conta como literal. Literal é tudo o que não é placeholder do runner, inclusive número, bool, mapa e lista. Placeholder é:

  • vazio;
  • {{::Rótulo}};
  • {RANDOM};
  • ${GENERATE:…};
  • $VAR / ${VAR}.

Como o content sai em cada caso:

Situação content Campos ao lado
Sem literal (o normal) O texto do arquivo, byte a byte —
Com literal O YAML re-serializado, com o valor trocado por '***' formatacao_preservada: false, mascarado: true
YAML que não parseia null content_erro: "yaml_invalido: linha L coluna C"
Literal usado como chave null content_erro: "segredo_em_chave"

O sha256 é sempre o dos bytes reais.

A máscara vai além do bloco secrets::

  • Mesmo valor em outro lugar. Todo escalar do arquivo igual a um literal de secrets também vira ***, por exemplo um alias YAML (*senha) de uma âncora definida em secrets. Nos diffs, um literal que mudou entre os lados sai *** (alterado).
  • Diffs. Cada linha dos diffs passa ainda pela máscara de texto do runner (credencial em URL, padrões de token). Ela pode marcar mascarado: true mesmo sem literal em secrets:.
  • Falha ao re-serializar. Se o YAML mascarado não puder ser re-serializado, o content sai null com content_erro: "reserializacao_falhou".

Os erros invalid_manifest do pin e do write também são saneados (no write, desde a 2.178.0):

  • toda ocorrência de um literal de secrets do candidato vira <valor>;
  • YAML que não parseia mostra só a linha e a coluna.

`manifest pin`

Valida o candidato e o fixa. O --base-sha256 é o raiz.sha256 do status: se a raiz mudou desde a leitura, o pin recusa com manifest_conflict (exit 11).

Validação (2.180.0+). O pin roda a validação inteira do manifest validate (ver "Validação do candidato", abaixo) contra este servidor. A regra de gravidade é fixa e só tem dois níveis:

  • vermelho recusa sempre (exit 14), mesmo com --aceita-avisos;
  • amarelo exige que todos os códigos amarelos venham em --aceita-avisos <codigo>[,…]. É a união dos codigos[] dos componentes warn. Faltou um, sai 14 com avisos_nao_confirmados.

Mudança de comportamento (2.180.0). O pin sem --aceita-avisos passa a recusar candidato amarelo, como rede ou middleware inexistente. Na 2.178.0 e na 2.179.0 todos os componentes eram vermelhos e o --aceita-avisos não tinha efeito. Nada que a 2.178.0 recusava passa a ser aceito.

A ordem de exits é: 16/17 → teto (10) → conflito (11) → parse (10) → validação (14) → nota (1) → changed: false → -y (1) → gravação. Um candidato vermelho sem -y sai 14, não 1. No changed: false, o -y só é exigido quando há metadado a gravar (--nota, --on-repo-change, --base-commit). A validação roda antes da trava da app: o pin confere, valida e só então pega a trava para a conferência final e a gravação. O tempo da validação é a coleta do contexto (teto de 5 s) mais os comandos docker do hosts_internos (até 2 × (2 s + graça)); se a raiz ou o candidato mudarem até a trava, a validação roda de novo sob ela. Se a raiz mudar nessa janela, sai 11 (ou 17 com fixado ativo).

O mesmo conteúdo e o reparo não revalidam o ambiente. Quando o candidato é igual à raiz (só --nota, --on-repo-change ou --base-commit) ou igual ao fixado de um pin interrompido, rodam só as checagens da tabela abaixo, sem recoletar o ambiente e sem exigir --aceita-avisos. As fontes saem nao_consultada e o plano sai vazio.

As checagens que não dependem do servidor, já presentes na 2.178.0:

Componente Código Recusa
parse (exit 10) Manifesto que o deploy recusaria (a mesma validação do write)
segredo_literal segredo_literal Valor literal sob secrets:, ou em environment com nome de segredo. Use um placeholder e grave o valor com runner env set --secret
credencial_plataforma credencial_plataforma CF_TOKEN, CLOUDFLARE_API_TOKEN e outras credenciais do runner no env da app
mascara valor_mascarado *** sob secrets: (o candidato veio de um status mascarado)
identidade projeto_alterado, nome_base_de_outra_app, instancia_registrada_removida, candidato_compose project diferente (2.184.0+: amarelo; 2.185.0+: só muda o agrupamento, nada é renomeado), o nome_base desta app é o de outra app registrada na mesma instância (2.185.0+, com a lista de apps do servidor; fora do --estatico), instância registrada ausente, x-runner-source

Desde a 2.185.0 o project só agrupa e pode repetir entre apps: o project_de_outra_app_na_instancia da 2.184.0 deixou de existir. A colisão real é o nome_base (o nome de container e de rota), em nome_base_de_outra_app: caminho /project, dados.nome_base e dados.apps. Ver Nome da app e project.

Recusa por amarelo não confirmado (exit 14):

{
  "error": "candidato_reprovado",
  "message": "candidato_reprovado: avisos não confirmados: rede_inexistente — repita com --aceita-avisos rede_inexistente",
  "detail": "…",
  "avisos_nao_confirmados": ["rede_inexistente"],
  "relatorio": {"overall": "warn", "components": ["…"], "plano": {"…": "…"}, "fontes": {"…": "…"}}
}

Recusa por vermelho (exit 14). O message e o detail trazem o primeiro componente vermelho com o motivo (candidato reprovado — <componente>: <detail>) e (+N) quando há outros; o relatorio traz o que deu para checar (ver "Relatório", em manifest validate):

{
  "error": "candidato_reprovado",
  "message": "candidato reprovado — required: DB_PASSWORD está em `required:` e não tem valor — preencha com `runner env set` (use --secret se for sensível) (+1)",
  "detail": "candidato reprovado — required: DB_PASSWORD está em `required:` e não tem valor — preencha com `runner env set` (use --secret se for sensível) (+1)",
  "avisos_nao_confirmados": [],
  "relatorio": {"overall": "red", "components": [
    {"name": "parse", "status": "green"},
    {"name": "required", "status": "red", "codigos": ["required_sem_valor"],
     "detail": "DB_PASSWORD está em `required:` e não tem valor — preencha com `runner env set` (use --secret se for sensível)",
     "dados": {"sem_valor": [{"campo": "required", "chave": "DB_PASSWORD", "codigo": "required_sem_valor"}]}},
    {"name": "portas", "status": "red", "codigos": ["porta_ocupada"],
     "detail": "a porta 8080 do host já está ocupada por outro processo ou app",
     "dados": {"ocupadas": [{"campo": "ports[0]", "porta": 8080, "codigo": "porta_ocupada"}]}},
    "… (demais componentes verdes)"],
   "plano": {"…": "…"}, "fontes": {"…": "…"}}
}

O detail de cada componente passa pela máscara e só cita nomes (campo, chave, porta, domínio), nunca valor de segredo.

Sucesso:

{"app": "loja", "instance": "production", "changed": true, "vigente": "local", "publicavel": true,
 "sha256": "3a51b693…", "base": {"sha256": "3de466e3…", "commit": "c1a2b3"},
 "avisos": [], "avisos_aceitos": ["rede_inexistente"], "efeitos_no_salvar": [],
 "aplica_com": "deploy --force", "on_repo_change": "deploy", "deploy_eta_s": null,
 "schedule": {}, "backup": null}

avisos_aceitos (2.180.0+) traz os códigos amarelos confirmados ([] quando o candidato é verde). efeitos_no_salvar vem do plano da validação.

Avisos (avisos[], objetos {codigo, message}). Não impedem a gravação:

  • repo_a_frente: o repo já mudou o manifesto além da raiz deste servidor. A divergência fica marcada na hora. Traz também commit e sha256 do manifesto do repo;
  • repo_inacessivel: fixado sem base, e o conflito com o repo só aparece na próxima leitura;
  • pin_sem_base: o fixado atual não tem base (foi salvo com o repo fora do ar). O conflito com o repo segue indetectável até reconhecer o repo com --base-commit;
  • source_sem_git: app de source local, https, rsync ou registry. Grava a raiz direto, com backup, e publicavel sai false.

Gravações sem trocar o manifesto. Mandar o mesmo conteúdo do fixado não grava nada, salvo nestes dois casos:

  • nota ou --on-repo-change diferentes: só atualizam o pin.json (changed: false, meta_atualizada: true);
  • --base-commit <commit>: reconhece o repositório naquele commit, grava a base nova e limpa a divergência sem reescrever o fixado (changed: false, meta_atualizada: true, base_atualizada: true). É o "manter o meu, já considerei o repositório". Se o commit é o da base já gravada e não há divergência marcada, nada muda (base_atualizada: false); se há marca, o comando a limpa e responde base_atualizada: true.

`manifest discard`

Recebe o local.sha256 do status. Se o fixado mudou nesse meio-tempo, recusa com manifest_conflict (exit 11).

A raiz volta ao manifesto do repositório, com permissão 644. Se o repo estiver fora do ar, a raiz recebe o do pull/, com aviso. O fixado vai para o histórico, com quem descartou (--actor).

{"app": "loja", "instance": "production", "changed": true, "vigente": "repo",
 "sha256": "83460816…", "pendente_de_aplicar": true, "aplica_com": "deploy --force",
 "aviso": null, "descartado_por": "ccs:ana", "descartado_em": "2026-10-31T07:12:44Z"}

`manifest publish` (2.181.0+)

Leva o manifesto fixado para o repositório da app: um commit na branch dist (dist_branch do registro), empurrado sem force. Recebe o local.sha256 do status, como o discard, e exige -y. Roda sob a trava da app.

O commit tem o assunto do contrato, chore(deploy): .deploy.yml via CCS, e o corpo só com os nomes dos campos de topo que mudaram, nunca valores:

chore(deploy): .deploy.yml via CCS

.deploy.yml alterado: healthcheck, resources

CAS pelo conteúdo do manifesto, não pelo commit. A dist recebe commits de código o tempo todo, e exigir que o head ainda seja o commit da base faria o publish quase sempre falhar. O publish lê o manifesto no head remoto e compara com a base do fixado (o manifesto do repo de onde ele partiu):

Manifesto no head Resultado
Igual à base Commita o fixado em cima do head atual e empurra. Commit de código de terceiro entre o pin e o publish não atrapalha: o commit do publish fica em cima do dele
Igual ao fixado Conclui sem commit (changed: false). Cobre a queda entre o push e a conclusão local: repetir o publish só conclui
Diferente dos dois manifest_conflict (exit 11). Nada é empurrado
Apagado no repo manifest_conflict (exit 11), com current_sha256: null

Se o remoto andar entre a leitura e o push, o push é recusado e o publish sai 11, com o head relido. Nunca força push.

No 11, a saída traz current_sha256 (o manifesto do repo) e repo_commit. O caminho é conferir as duas versões com manifest status, reconhecer o repo com pin --base-commit <repo_commit> (com o conteúdo ajustado, se for o caso) e publicar de novo. Um fixado sem base (salvo com o repo fora do ar) também sai 11, pedindo o pin --base-commit primeiro.

Segredo nunca vai para o repo. Antes do push, o publish reaplica as checagens de segredo do pin (segredo_literal, credencial_plataforma, mascara) no fixado. Isso pega um pin gravado sob uma regra mais fraca ou um fixado editado à mão: sai 14 candidato_reprovado, com o relatorio só desses componentes e sem o valor. Segredo na história do git do cliente não tem volta. O ambiente do servidor não é revalidado: o pin já validou.

Prazo de 100 s no total, contado do início do comando (inclui a espera pela trava da app): a leitura do head leva até 60 s e o push leva o que sobra (com corte por conexão lenta). Estouro sai 12 remote_unavailable (git_timeout), nunca 11.

Sucesso:

  • o estado volta a repo: o status responde vigente: "repo";
  • a raiz fica com os mesmos bytes, e o modo volta a 644;
  • o fixado vai para o histórico (historico/…publicado.deploy.yml), com um .json que registra publicado_em, publicado_por, commit e nota;
  • o pin.json, o vigente e a base são removidos;
  • não deploya: o deploy vem pelo ciclo normal do runner.
{"app": "loja", "instance": "production", "changed": true,
 "commit": "cff6448f39d336a3c3d9875d1f0cde48ff220a72", "sha256": "3a51b693…",
 "vigente": "repo", "provoca_deploy": false,
 "outros_consumidores_locais": ["loja-explicito", "loja-staging"],
 "publicado_por": "ccs:bia", "publicado_em": "2026-11-01T04:01:24Z"}
  • commit: o commit do publish (ou o head, com changed: false);
  • provoca_deploy: true só quando o conteúdo publicado tem version.source: git-commit, porque aí o commit novo muda a versão e o cron implanta. Com outra fonte de versão, o commit não dispara deploy;
  • outros_consumidores_locais: os nomes das outras apps deste servidor que leem o mesmo repo, a mesma dist_branch e o mesmo manifest_path. O publish muda o manifesto delas também. Ficam de fora a própria app, os states qualificados (app@…) e as sources sem git. Desde a 2.187.0 o manifest status e o manifest context devolvem a mesma lista antes do publish.

Conflito (exit 11):

{"error": "manifest_conflict", "current_sha256": "1bea8ecd…",
 "repo_commit": "0a334ba70679bf724ee2ea08a2a809e9defd832c",
 "message": "o manifesto mudou no repo desde a base do fixado (commit 0a334ba): veja as duas versões com `runner manifest status` e reconheça o repo (`runner manifest pin --base-commit 0a334ba`) antes de publicar de novo",
 "detail": "…"}

Se o push entrou mas a conclusão local falhou (histórico ou state), o erro cita o commit que já está no repo e manda repetir o publish, que então sai changed: false.

Ordem dos exits: 16 fixacao_nao_suportada (compose, layout legado, source sem git) → 16 publish_nao_suportado_em_tag → 15 sem_fixacao → 17 manifesto_fixado_adulterado → 11 (--sha256 diferente do fixado) → 1 confirmacao_exigida (sem -y) → 10 invalid_manifest (o fixado não passa no parse) → 14 candidato_reprovado (segredo) → 11 (fixado sem base) → 12 remote_unavailable → 11 (manifesto mudado ou apagado no repo, corrida no push).

Quando o repositório muda o manifesto: `on_repo_change`

Com um manifesto fixado, o fetch compara o manifesto do repositório com a base. Se ele mudou, o fetch grava repo_divergiu e emite o evento manifest_repo_divergiu, que leva o commit e os nomes dos campos, nunca os valores. A raiz não muda. Commit só de código não marca nada.

App com source: type: tag (2.181.0+). O "manifesto do repositório" é o da tag resolvida, pela mesma resolução do deploy (tag e sort da source, sobre origin/<branch>), e não o head da branch. Vale para a base do pin, o aviso repo_a_frente, o status (repo.sha256, repo.commit), o --base-commit, o discard e o write --local. O fetch com fixado compara pela tag e marca ou limpa repo_divergiu; com isso, o on_repo_change: hold também vale nessas apps. Commit na branch sem tag nova não marca nada.

  • O pin.json ganha o campo aditivo base_ref: "tag" quando a base vem da tag. Um runner antigo ignora o campo.
  • Pin antigo (gravado até a 2.180.x, sem base_ref, com a base lida do head da branch): na primeira passada do fetch, a base é reancorada uma vez no manifesto da tag, sem marcar divergência (com warn no log se o manifesto muda). Uma mudança real do repositório nesse intervalo é absorvida. Se o git falhar ao conferir as tags da base, nada muda. Até essa passada, o status de um pin antigo pode mostrar divergência.
  • pin --base-commit exige um commit com tag que case o tag da source: sem tag sai 12 base_commit_sem_tag.
  • manifest publish sai 16 publish_nao_suportado_em_tag: publicar ali seria criar commit e tag, um processo de release. Leve o conteúdo ao repositório pelo release e, com a tag publicada, volte com discard. O publicavel dessas apps é false.
  • Com fixado, o fetch dessas apps poda tags e branches apagadas no remoto (--prune); sem fixado, a busca é a de antes.

Na 2.180.x esse caso era uma exceção: o fetch não comparava, e o status comparava com o head da branch.

O que acontece a seguir depende do --on-repo-change:

Valor O cron O deploy
deploy (padrão) Implanta o código novo com o fixado Conclui e sai com exit 3: pendência manifesto_repo_divergiu no RUNNER_RESULT (formato abaixo)
hold Não implanta. A app sai com status/last_error manifesto_repo_divergiu e aparece como SEGURADA no relatório do lote Um deploy manual segue como no deploy

Mudança de comportamento (2.178.0). Com a divergência marcada, todo deploy concluído sai com exit 3, inclusive runner deploy --force. O deploy está no ar: o 3 diz que a mudança do repositório não foi aplicada.

O exit 3 sai sempre. A linha RUNNER_RESULT só sai com RUNNER_RESULT_LINE=1 no ambiente, e a pendência tem esta forma (dados é aditivo):

{"passo": "manifesto_repo_divergiu", "erro": "outro",
 "mensagem": "implantado com o manifesto fixado (3a51b693); … — a mudança do repo não foi aplicada. Confira com `runner manifest status`",
 "dados": {"commit": "d4e5f6", "campos": ["healthcheck"]}}

dados.campos traz só os nomes das chaves de topo. Ver os exits e a linha RUNNER_RESULT em `runner deploy`.

A pendência continua até uma destas saídas:

  • manter o fixado: pin --base-commit <repo.commit>, com o mesmo conteúdo ou com o conteúdo ajustado;
  • ficar com o repositório: discard;
  • o repositório voltar ao manifesto da base ou passar a ter o manifesto fixado: o fetch limpa a marca sozinho. Exceção: na marca de repo_a_frente (feita no pin) a base já é o head do repo, então o repo seguir igual à base não limpa a marca.

`write --local` agora fixa

Mudança de comportamento (2.178.0). O manifest write --local chama o pin.

O manifesto gravado vale em todos os próximos deploys, até o discard. A resposta mantém os campos de antes (local, commit, path, sha256, backup) e traz também:

  • vigente, base, avisos e on_repo_change;
  • aviso (texto para o operador; só com changed: true);
  • publicavel, efeitos_no_salvar, deploy_eta_s e schedule;
  • avisos_aceitos, sempre [] (o write --local não confirma amarelos);
  • avisos_validacao (2.180.0+, abaixo).

O aplica_com, que já existia, mudou de valor: de deploy --local --force para deploy --force. Com changed: false ele sai da resposta.

Os erros e exits passam a ser os do pin. Na prática, o write --local:

  • sai 16 (fixacao_nao_suportada) em app compose (x-runner-source) ou com layout legado (.git na raiz da app), onde antes gravava a raiz;
  • sai 14 (candidato_reprovado) com qualquer componente vermelho da validação (ver "Gravidade", em manifest validate). Os amarelos não bloqueiam (seção seguinte);
  • espera a trava da app até 10 s, e sai 1 com app_lock_ocupado se um deploy ou o cron estiver em curso.

`write --local`: vermelho recusa, amarelo grava

Mudança de comportamento (2.180.0). O write --local passa pela validação do candidato, como o pin.

  • Vermelho sai 14 (candidato_reprovado), com o relatorio.
  • Amarelo não bloqueia e não pede confirmação. O manifesto é gravado e os amarelos voltam no campo novo avisos_validacao, um item por código ([] quando o candidato é verde, também com changed: false):
"avisos_validacao": [{"codigo": "rede_inexistente",
  "message": "rede publik não existe neste servidor (você quis dizer public?) — o runner a cria no deploy"}]

O write não tem --aceita-avisos: a confirmação por código vale só no pin, que é o caminho do editor. Quem usa o write --local precisa mostrar o avisos_validacao ao usuário, porque o amarelo foi gravado sem confirmação.

Exits

Exit error Quando
0 — Sucesso, inclusive changed: false
1 confirmacao_exigida, app_lock_ocupado, manifesto_ilegivel, manifesto_fixado_ilegivel, nota_invalida, manifesto_fixado_escrita, invalid_manifest_path Sem -y; trava ocupada; raiz ou pin.json ilegível; nota com mais de 200 caracteres; falha ao gravar os arquivos do fixado; manifest_path inválido no registro (também em status, pin e discard)
10 invalid_manifest O candidato não passa na validação do deploy, tem mais de 64 KiB ou não é UTF-8; --base-commit que não é um sha hex; o manifesto não existe naquele commit
11 manifest_conflict --base-sha256 ou --sha256 diferente do atual (current_sha256)
11 manifest_conflict publish: o manifesto mudou ou foi apagado no repo desde a base, o fixado não tem base, ou o push foi recusado por corrida. Traz current_sha256 e repo_commit
12 remote_unavailable --base-commit fora da história de origin/<branch>; discard sem repo e sem pull/; git fetch passou de 60 s no total (git_timeout); publish passou de 100 s no total, contados do início do comando (git_timeout) ou o repo não respondeu
12 base_commit_sem_tag App de source: type: tag: --base-commit sem tag que case o tag da source (2.181.0+)
13 manifesto_fixado write sem --local ou runner upgrade com fixado ativo. O write sem --local sai 13 também com pin.json ilegível (o upgrade, 1)
14 candidato_reprovado Componente vermelho; no pin, também amarelo não confirmado em --aceita-avisos; no publish, segredo literal, credencial da plataforma ou valor mascarado no fixado
15 sem_fixacao discard ou publish sem manifesto fixado
16 fixacao_nao_suportada App derivada de compose (x-runner-source; use runner reconcile) ou layout legado; no publish, também source sem git
16 publish_nao_suportado_em_tag publish em app de source: type: tag (2.181.0+)
17 manifesto_fixado_adulterado A raiz foi reescrita por fora do runner

Os erros saem como {"error", "message", "detail"}.


Contexto do ambiente — `manifest context` (2.179.0+)

Mostra o que existe neste servidor para quem está escrevendo um .deploy.yml: middlewares, entrypoints e certificados do Traefik, redes e volumes do Docker, domínios e portas em uso, as outras apps e os nomes das envs da app. É o que o editor de manifesto do CCS usa para sugerir valores e avisar de conflito antes de salvar.

runner manifest context <app> [-i|--instance <instância>] [-x|--exact]
Flag Descrição
-i, --instance Instância. Sem ela, vale a registrada da app (senão a do manifesto, senão production)
-x, --exact Só casamento exato do nome da app

A saída é sempre JSON no stdout. É só leitura: não pega a trava da app (responde com um deploy ou o cron em curso) e não toca container, rota nem rede. O CCS sabe que o runner tem o comando pela capacidade manifest.context.v1 em runner --capabilities.

Exemplo

Listas cortadas:

{
  "app": "loja-front",
  "instance": "production",
  "coletado_em": "2026-10-31T16:31:35Z",
  "duracao_ms": 868,
  "avisos": [],
  "fontes": {
    "traefik_file":   { "status": "ok", "dir": "/opt/traefik/dynamic" },
    "traefik_labels": { "status": "ok", "ativo": true },
    "traefik_api":    { "status": "indisponivel", "motivo": "api não configurada (sem `traefik.manager.api_url` e nenhum Traefik descoberto)" },
    "traefik_static": { "status": "ok" },
    "docker":         { "status": "ok" },
    "portas":         { "status": "ok" },
    "apps":           { "status": "ok", "total": 2, "ilegiveis": [] },
    "envs":           { "status": "ok" }
  },
  "traefik": {
    "middlewares": [
      { "ref": "crm-auth@docker", "tipo": "basicAuth", "origem": "docker" },
      { "ref": "vpn-only@file", "tipo": "ipAllowList", "origem": "file" }
    ],
    "tls_options": [ { "ref": "default@file" } ],
    "entrypoints": [ { "nome": "web", "endereco": ":80" }, { "nome": "websecure", "endereco": ":443" } ],
    "entrypoints_do_runner": [ "websecure" ],
    "certificados": { "origem": "traefik", "capacidades": [ "http-01" ] },
    "routers_com_erro": []
  },
  "docker": {
    "redes": [ { "nome": "public", "driver": "bridge", "ipv6": false } ],
    "volumes": [ { "nome": "loja_dados", "em_uso_por": [ "loja-front" ], "containers": [ "loja_production_web" ] } ],
    "runtimes": [ "runc" ],
    "arquitetura": "x86_64"
  },
  "dominios_em_uso": [
    { "dominio": "beta.exemplo.test", "caminho": null, "app": "loja-beta", "instancia": "production",
      "origem": "manifesto", "gerenciado": true, "propria": false },
    { "dominio": "crm.exemplo.test", "caminho": null, "app": null, "instancia": null,
      "origem": "router", "router": "crm@docker", "gerenciado": false, "propria": false },
    { "dominio": "loja.exemplo.test", "caminho": null, "app": "loja-front", "instancia": "production",
      "origem": "router", "router": "loja-production@file", "gerenciado": true, "propria": true }
  ],
  "portas": { "ocupadas": [ 22, 80, 443, 8081 ], "da_propria_app": [ 8081 ], "faixa_do_runner": "8000-8999",
              "sticky_do_state": 8081 },
  "apps": [
    { "app": "loja-beta", "projeto": "beta", "nome_base": "loja-beta", "instancias": [ "production" ],
      "instancia_registrada": "production", "pausada": false }
  ],
  "envs_da_app": { "chaves": [ "DB_NAME", "SMTP_PASSWORD" ] }
}
Campo Conteúdo
traefik.middlewares[] ref qualificada (nome@file, nome@docker, …), tipo (basicAuth, ipAllowList, …; null quando a fonte não diz) e origem (file, docker ou o provider da API). Nunca a configuração
traefik.tls_options[] As opções de TLS do file provider
traefik.entrypoints_do_runner Os entrypoints em que o runner publica as rotas
traefik.certificados capacidades (http-01, dns-01:<provider>, …). origem: traefik quando lidas da config estática; config_yml com aviso quando o runner caiu no traefik.cert_resolver do config.yml, sem conferir
traefik.routers_com_erro[] Routers que a API do Traefik marca com erro ou desligados
docker.volumes[] em_uso_por (apps) e containers (nomes). Nunca o caminho no host
dominios_em_uso[] Domínio e caminho em uso, com o dono: app (null quando o router não é de app do runner), router, origem (router = rota no Traefik; manifesto = só declarado no manifesto de uma app), gerenciado (escrito pelo runner) e propria (é desta app)
avisos[] Avisos sobre o pedido. Com -i de uma instância que o manifesto não declara: instancia_inexistente: … (o comando segue, com exit 0)
portas.ocupadas Portas TCP em uso no servidor. da_propria_app são as que esta app já ocupa (não são conflito): só as publicadas por um container da app que o runner vê. sticky_do_state é a porta que o registro guarda para a app; é só informação e não prova que a porta é dela
apps[] As apps do servidor, inclusive as pausadas (pausada: true). nome_base (2.185.0+) é o nome de container e de rota de cada uma; projeto é o agrupador (o project: do manifesto; desde a 2.185.0 sem o --slug). O dono de arquivo de rota, marcador, container sem uid e porta é decidido pelo nome_base
envs_da_app.chaves Os nomes das envs da app (.env e .env.<instância>)

Fontes: `fontes..status`

Cada fonte é lida em paralelo, com prazo próprio, e o comando inteiro tem teto de 5 s. Uma fonte fora do ar não derruba o comando: o resultado sai parcial, com exit 0, e a fonte diz o que faltou.

Fonte O que traz Prazo
traefik_file Middlewares, opções de TLS e routers do file provider (dir = diretório lido) 1 s
traefik_labels Middlewares e routers de labels de container (ativo: false quando o provider docker está desligado) 2 s
traefik_static Entrypoints e resolvers de certificado 1 s
traefik_api Middlewares e routers de qualquer provider, pela API do Traefik 3 s
docker Redes, volumes, runtimes e arquitetura 2 s
portas Portas em uso 2 s
apps As apps e seus domínios (total; ilegiveis = apps com manifesto que não parseia) 1 s
envs Os nomes das envs desta app 1 s
status Significado
ok Lida inteira
indisponivel Não deu para ler. O motivo diz por quê
parcial Lida em parte (por exemplo, a API parou numa página, ou há apps com manifesto ilegível). O motivo diz o que faltou
timeout Passou do prazo da fonte ou do teto

A API do Traefik vem do traefik.manager.api_url do config.yml ou do container do Traefik descoberto. Sem nenhum dos dois, a fonte sai indisponivel com o motivo "api não configurada". O runner nunca tenta 127.0.0.1:8080 às cegas, porque essa porta pode ser de uma app.

Para quem consome: uma fonte diferente de ok quer dizer "não verificado", não "não existe". Um middleware que não aparece com a API fora do ar pode existir no Traefik.

Segredos: só nomes, nunca valores

  • Das envs da app saem só as chaves. O .secrets cifrado não é lido.
  • De outras apps não sai nenhuma env.
  • Middlewares saem sem a configuração; volumes, sem caminho no host.
  • Os motivo passam pela máscara de segredos e não citam caminhos de apps.

Uso pelo editor do CCS

O editor chama o context ao abrir o manifesto e usa o resultado para:

  • sugerir routing.middlewares, tls_options, redes e volumes que existem;
  • avisar de domínio já em uso por outra app (propria: false) e de porta ocupada fora de da_propria_app;
  • completar {{::VAR}} com as chaves de envs_da_app;
  • mostrar, em outros_consumidores_locais (2.187.0+), as outras apps deste servidor que leem o mesmo manifesto (mesma regra do manifest publish).

Quando uma fonte não está ok, o editor mostra o campo como "não verificado" em vez de acusar erro.

Erros

Exit error Quando
0 — A app existe, mesmo com fontes fora do ar
1 app_inexistente Nenhuma app casa o nome
1 app_ambigua O nome casa mais de uma app. Use o nome exato ou -x
1 runner_erro Não deu para procurar a app (pasta de apps ausente, erro de leitura)
2 — Runner anterior à 2.179.0, sem o subcomando

Os erros saem como {"error", "message", "detail"}.


Validação do candidato — `manifest validate` (2.180.0+)

Diz se um .deploy.yml candidato funciona neste servidor antes de salvar: o que falta, o que colide com outra app e o que o salvar muda. É só leitura: não grava nada, não pega a trava da app e não toca container, rota nem rede. Responde mesmo com um deploy ou o cron em curso.

runner manifest validate <app> [-i <instância>] [-x] < candidato.deploy.yml
  • O candidato entra pelo STDIN, até 64 KiB. A saída é sempre JSON.
  • Sem -i, vale a instância registrada da app, depois a padrão do manifesto e, por último, production (a mesma resolução do pin).
  • O contexto do ambiente é recoletado a cada chamada, com os prazos do manifest context (teto de 5 s), mais os comandos docker do hosts_internos (2 s cada, com graça).
  • O pin roda esta mesma validação e recusa o que ela reprova (ver manifest pin, acima).

O CCS sabe que o runner tem o comando pela capacidade manifest.validate.v1 em runner --capabilities.

Exits

Exit Saída
0 Relatório com overall green ou warn, sem error
14 Relatório com overall red, mais error: "candidato_reprovado", message e detail no topo (candidato reprovado — <componente>: <detail> do primeiro vermelho, mais (+N)). Parse reprovado também sai 14: o componente parse fica vermelho (invalid_manifest) e o relatório traz o que deu para checar (ver components, abaixo)
10 invalid_manifest, só quando não há relatório possível: candidato acima de 64 KiB ou não UTF-8
1 app_inexistente, app_ambigua ou runner_erro

Relatório

{
  "app": "loja", "instance": "production", "sha256": "75198459…", "overall": "warn",
  "components": [
    {"name": "parse", "status": "green"},
    {"name": "refs_traefik", "status": "warn", "codigos": ["middleware_inexistente"],
     "detail": "middleware inexistente neste servidor: vpn-onli@file (você quis dizer vpn-only@file?) — o Traefik descarta o router que o referencia",
     "dados": {"inexistentes": [{"campo": "instances.production.routing.middlewares[0]",
       "ref": "vpn-onli@file", "sugestao": "vpn-only@file", "codigo": "middleware_inexistente"}]}},
    {"name": "docker", "status": "green"}
  ],
  "plano": {
    "campos": ["instances.production.routing"],
    "instancias": {"adicionadas": [], "removidas": [], "orfaos": []},
    "dominios": {"adicionados": ["{{::DOMAIN_STAGING}}"], "removidos": [],
      "templates": [{"campo": "instances.staging.domain", "literal": "{{::DOMAIN_STAGING}}",
                     "resolvido": null, "mudanca": "adicionado"}]},
    "efeitos_no_salvar": [{"codigo": "gatekeeper_sonda_dominio", "campo": "instances.staging.domain",
      "dominio": "{{::DOMAIN_STAGING}}", "template": true, "literal": "{{::DOMAIN_STAGING}}",
      "efeito": "o domínio vem do template {{::DOMAIN_STAGING}}, que ainda não resolve nesta pasta: o gatekeeper sonda o valor que o deploy resolver"}],
    "exige_deploy": true
  },
  "fontes": {"traefik_file": {"status": "ok"}, "traefik_api": {"status": "indisponivel", "motivo": "…"}, "…": "…"}
}
  • components: a lista varia. A ordem abaixo vale só para os presentes: parse, semantica, segredo_literal, credencial_plataforma, mascara, identidade, required, assets, refs_traefik, certificado, dominios, portas, docker, dependencias, hosts_internos e orfaos. O que não é verde traz codigos[], detail e dados. dados é um objeto agrupado por tipo de achado (ex.: inexistentes, sem_valor, ocupadas), e cada chave é uma lista de itens com o campo do manifesto, o codigo e, quando há um nome parecido, a sugestao. Não é uma lista no topo. Indexe por name, nunca por posição. Faltam componentes nestes casos:
    • parse reprovado: ficam de fora semantica, required, assets, refs_traefik, certificado, dominios, portas, docker e dependencias;
    • hosts_internos só aparece quando o candidato parseia e a checagem de rede do preflight roda;
    • candidato compose: só parse e identidade (abaixo);
    • pin sem conteúdo novo (só nota, --on-repo-change, --base-commit ou reparo): só parse, segredo_literal, credencial_plataforma, mascara e identidade.
  • overall é o pior componente, e o componente é o pior achado.
  • plano: os campos que mudam em relação à raiz atual, as instâncias adicionadas e removidas, os orfaos (instâncias registradas que saem do manifesto), os domínios adicionados e removidos e, em templates, os de {{::Var}}/${VAR} com o literal e o host resolvido (resolvido, null quando não resolve). Só a instância desta pasta resolve template, pelos arquivos dela; as outras instâncias e template cuja chave tem nome de segredo ficam pelo literal, os efeitos_no_salvar (version_muda_deteccao, gatekeeper_sonda_dominio) e se a mudança exige_deploy.
  • fontes: o status de cada fonte do contexto, como no context.

Gravidade

São dois níveis, sem terceiro. Vermelho é o que quebra o deploy ou outra app, e o pin recusa sempre. Amarelo é o que pode ser intencional ou não dá para provar, e o pin exige o código em --aceita-avisos.

Gravidade Códigos
vermelho invalid_manifest, semantic_<id> de erro, segredo_literal, credencial_plataforma, valor_mascarado, candidato_compose, nome_base_de_outra_app, instancia_registrada_removida, required_sem_valor, asset_required_vazio, dominio_de_outra_app, porta_ocupada
amarelo projeto_alterado, semantic_<id> de aviso, semantica_nao_verificavel, required_nao_verificavel, required_vazio_no_servidor, dominio_repetido_entre_instancias, middleware_inexistente, tls_options_inexistente, middleware_nao_verificavel, tls_options_nao_verificavel, capacidade_de_certificado_ausente, certificado_nao_verificavel, dominio_em_router_nao_gerenciado, dominio_dono_sem_prova, dominio_declarado_em_outra_app, dominio_com_caminho_diferente, dominio_template_nao_verificavel, dominios_nao_verificavel, portas_nao_verificavel, rede_inexistente, volume_inexistente, volume_de_outra_app, caminho_do_host_ausente, arquitetura_diferente, docker_nao_verificavel, dependencia_nao_registrada, dependencias_nao_verificavel, host_interno_sem_resolucao, hosts_internos_nao_verificavel, instancia_orfa, orfaos_nao_verificavel

Alguns detalhes:

  • dominio_de_outra_app só sai quando o domínio, no mesmo caminho, é servido por um router que o runner gerencia e cujo dono é provado (marcador managed-by: runner). Router de fora do runner dá dominio_em_router_nao_gerenciado, com o nome do router; dono só pelo nome do arquivo dá dominio_dono_sem_prova.
  • porta_ocupada não conta a porta publicada pela própria app.
  • required confere as chaves nas mesmas fontes do deploy: as camadas .env/.env.<instância>, o .secrets legado, os nomes do store de segredos v2 (sem decifrar) e o que o candidato fornece em environment: e nos environment_overrides da instância. O secrets: do candidato não conta, porque o deploy não o materializa. No v2, chave de environment: com nome de segredo sai required_nao_verificavel. Chave que existe vazia no servidor sai required_vazio_no_servidor: o deploy mantém o valor do servidor se o literal do manifesto não mudou desde o último deploy.
  • Candidato compose (x-runner-source na raiz): o relatório traz só parse e identidade com candidato_compose, vermelho. As demais checagens não rodam, porque o fluxo de edição não suporta compose (o pin sai 16).
  • dominio_repetido_entre_instancias é amarelo: as instâncias podem estar em servidores diferentes atrás de um LB. No mesmo servidor, os routers disputam o domínio. Domínio vazio (domain: "") não conta.
  • Domínio de template cuja chave tem nome de segredo não é resolvido e sai dominios_nao_verificavel.
  • semantica_nao_verificavel: build.context/dockerfile que não existe no pull/ atual, que pode estar atrás do repositório até o próximo fetch. O diretório é o do deploy (source_dir, ou instances.<inst>.source.directory).
  • semantic_build_arg_secret cita só o nome da variável, nunca o valor.
  • hosts_internos roda a checagem de rede do preflight com prazo de 2 s por comando docker. hosts_internos_nao_verificavel só sai quando o env cita host interno e o Docker não respondeu.

Fonte fora do ar nunca dá verde

Se a fonte que provaria a ausência do problema não respondeu, o componente sai amarelo *_nao_verificavel. Nunca sai verde, e nunca sai vermelho. Um middleware @file se prova pelo file provider ou pela API do Traefik; um @docker, pelos labels ou pela API; os demais providers, só pela API. Ref sem @provider também sai não verificável.

Diagnósticos por linha e `--estatico` (2.183.0+)

O relatório traz também diagnosticos[], aditivo: cada achado com codigo, gravidade, caminho, linha, coluna, fim, mensagem e sugestao (com aplicar quando a correção é mecânica). Os campos de antes não mudam.

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

O --estatico roda só as checagens que não consultam Docker, Traefik nem DNS, sem a trava da app. Serve para o editor checar a cada tecla. A capacidade manifest.lint.v1 em runner --capabilities anuncia as duas coisas. Formato, edições de texto, gravidade e o catálogo de regras: lint do manifesto.

Uso pelo editor do CCS

O editor chama o validate a cada mudança do candidato e mostra os componentes. Ao salvar, chama o pin com --aceita-avisos só depois de o usuário confirmar os amarelos na tela.


JSON Schema do manifesto — `manifest schema` (2.182.0+)

Imprime o JSON Schema do .deploy.yml que este runner aceita. É só leitura e não depende de app.

runner manifest schema

A saída é um envelope JSON com runner_version, schema_id, dialeto e schema. O editor guarda o schema em cache pelo schema_id e marca em vermelho o campo que o runner da app não conhece, sem removê-lo. O runner anuncia a capacidade manifest.schema.v1 em runner --capabilities.

Extensões x-runner-*, vocabulário do x-runner-ref e divergências conhecidas: `runner manifest schema`.


Use cases típicos

Debug pre-deploy:

# Confere se todos os templates resolvem
runner manifest meu-app --resolved 2>&1 | grep -E '<UNRESOLVED|⚠'

Diff entre apps:

runner manifest app-a --resolved --json > a.json
runner manifest app-b --resolved --json > b.json
diff a.json b.json

Inspeção remota via CCS:

runner manifest meu-app --resolved --json
# CCS frontend renderiza o manifest sem precisar de SSH no servidor

Veja também

By Borlot.com.br on 31/10/2026