`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-appImprime 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 --resolvedOutput:
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:
.runner/secrets.enc(decriptado com ckey) — backup encriptado.envplaintext — sobrescreve secrets.enc se presente.secretsplaintext — sobrescreve.envse 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_KEYIsso 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, oshowlê o manifesto da branch da app no repositório (git fetchantes) e owritecommita e faz push nela. O deploy vem depois, pelo cron ou porrunner deploy. - Com
--local, os dois usam o.deploy.ymlda raiz da app no servidor, sem git. Desde a 2.178.0 owrite --localfixa 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óximodeploy --local --force, e um deploy normal voltava ao manifesto do repositório. Desde a 2.180.0 owrite --localtambém passa pela validação do candidato: vermelho sai 14, amarelo grava e volta emavisos_validacao(ver "write --local: vermelho recusa, amarelo grava", abaixo). --base-sha256é osha256devolvido peloshow. Se o manifesto mudou desde a leitura, owriterecusa commanifest_conflict.- Com um manifesto fixado no servidor, o
writesem--localrecusa commanifesto_fixado(exit 13): o repositório não é o manifesto vigente. Com opin.jsonilegível ele também sai 13 (recusa por segurança), e omessagetraz o erro de leitura. - O
writerecebe 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, owriteresponde exit 1 com{"error": "confirmacao_exigida", "message", "detail"}, como owrite --locale opin. Na 2.180.0 owritesem--localsaí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-dnssemrouting.provider;- o mesmo
path_prefixduas vezes no mesmo domínio, ou duas rotas sempath_prefixno mesmo domínio; release:inválido (semcommand, duração inválida,target:sembuild:).
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:
- Salvar (
manifest pin). Só grava. Não deploya e não toca container, rota nem rede. - 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. - 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. - 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 -yEnquanto 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
fetchnão copia o manifesto do repositório. Quando ele mudou, só marca a divergência (vejaon_repo_changeabaixo); - o deploy não copia do
pull/; - a recuperação de uma raiz ilegível usa o manifesto fixado;
runner upgradeemanifest writesem--localrecusam commanifesto_fixado(exit 13). Com opin.jsonilegível os dois também recusam: owritesai 13; oupgradesai 1, commanifesto_fixado_ilegivelna mensagem;runner config-backups restorepula a raiz com aviso e restaura o resto (files_skippedno 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: saivigente: "local"comestado: "repo"elocal: null. A raiz é a única fonte do manifesto, não um fixado. Umdiscardnesse caso sai 15. pin.jsonilegível:vigente,estadoedivergenciasaemnull, com o motivo empin_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
secretstambém vira***, por exemplo um alias YAML (*senha) de uma âncora definida emsecrets. 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: truemesmo sem literal emsecrets:. - Falha ao re-serializar. Se o YAML mascarado não puder ser re-serializado,
o
contentsainullcomcontent_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
secretsdo 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 doscodigos[]dos componenteswarn. Faltou um, sai 14 comavisos_nao_confirmados.
Mudança de comportamento (2.180.0). O
pinsem--aceita-avisospassa 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-avisosnã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émcommitesha256do 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 sourcelocal,https,rsyncouregistry. Grava a raiz direto, com backup, epublicavelsaifalse.
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-changediferentes: só atualizam opin.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 respondebase_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, resourcesCAS 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: ostatusrespondevigente: "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.jsonque registrapublicado_em,publicado_por,commitenota; - 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, comchanged: false);provoca_deploy:truesó quando o conteúdo publicado temversion.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 mesmadist_branche o mesmomanifest_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 omanifest statuse omanifest contextdevolvem 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.jsonganha o campo aditivobase_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 (comwarnno 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, ostatusde um pin antigo pode mostrar divergência. pin --base-commitexige um commit com tag que case otagda source: sem tag sai 12base_commit_sem_tag.manifest publishsai 16publish_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 comdiscard. Opublicaveldessas 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
statuscomparava 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 nopin) 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 --localchama opin.
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,avisoseon_repo_change;aviso(texto para o operador; só comchanged: true);publicavel,efeitos_no_salvar,deploy_eta_seschedule;avisos_aceitos, sempre[](owrite --localnã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 (.gitna raiz da app), onde antes gravava a raiz; - sai 14 (
candidato_reprovado) com qualquer componente vermelho da validação (ver "Gravidade", emmanifest validate). Os amarelos não bloqueiam (seção seguinte); - espera a trava da app até 10 s, e sai 1 com
app_lock_ocupadose um deploy ou o cron estiver em curso.
`write --local`: vermelho recusa, amarelo grava
Mudança de comportamento (2.180.0). O
write --localpassa pela validação do candidato, como opin.
- Vermelho sai 14 (
candidato_reprovado), com orelatorio. - 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 comchanged: 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
.secretscifrado não é lido. - De outras apps não sai nenhuma env.
- Middlewares saem sem a configuração; volumes, sem caminho no host.
- Os
motivopassam 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 deda_propria_app; - completar
{{::VAR}}com as chaves deenvs_da_app; - mostrar, em
outros_consumidores_locais(2.187.0+), as outras apps deste servidor que leem o mesmo manifesto (mesma regra domanifest 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 dopin). - O contexto do ambiente é recoletado a cada chamada, com os prazos do
manifest context(teto de 5 s), mais os comandosdockerdohosts_internos(2 s cada, com graça). - O
pinroda esta mesma validação e recusa o que ela reprova (vermanifest 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_internoseorfaos. O que não é verde trazcodigos[],detailedados.dadosé um objeto agrupado por tipo de achado (ex.:inexistentes,sem_valor,ocupadas), e cada chave é uma lista de itens com ocampodo manifesto, ocodigoe, quando há um nome parecido, asugestao. Não é uma lista no topo. Indexe porname, nunca por posição. Faltam componentes nestes casos:- parse reprovado: ficam de fora
semantica,required,assets,refs_traefik,certificado,dominios,portas,dockeredependencias; hosts_internossó aparece quando o candidato parseia e a checagem de rede do preflight roda;- candidato compose: só
parseeidentidade(abaixo); pinsem conteúdo novo (só nota,--on-repo-change,--base-commitou reparo): sóparse,segredo_literal,credencial_plataforma,mascaraeidentidade.
- parse reprovado: ficam de fora
overallé o pior componente, e o componente é o pior achado.plano: oscamposque mudam em relação à raiz atual, as instâncias adicionadas e removidas, osorfaos(instâncias registradas que saem do manifesto), os domínios adicionados e removidos e, emtemplates, os de{{::Var}}/${VAR}com o literal e o host resolvido (resolvido,nullquando 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, osefeitos_no_salvar(version_muda_deteccao,gatekeeper_sonda_dominio) e se a mudançaexige_deploy.fontes: o status de cada fonte do contexto, como nocontext.
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_appsó sai quando o domínio, no mesmo caminho, é servido por um router que o runner gerencia e cujo dono é provado (marcadormanaged-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_ocupadanão conta a porta publicada pela própria app.requiredconfere as chaves nas mesmas fontes do deploy: as camadas.env/.env.<instância>, o.secretslegado, os nomes do store de segredos v2 (sem decifrar) e o que o candidato fornece emenvironment:e nosenvironment_overridesda instância. Osecrets:do candidato não conta, porque o deploy não o materializa. No v2, chave deenvironment:com nome de segredo sairequired_nao_verificavel. Chave que existe vazia no servidor sairequired_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-sourcena raiz): o relatório traz sóparseeidentidadecomcandidato_compose, vermelho. As demais checagens não rodam, porque o fluxo de edição não suporta compose (opinsai 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/dockerfileque não existe nopull/atual, que pode estar atrás do repositório até o próximo fetch. O diretório é o do deploy (source_dir, ouinstances.<inst>.source.directory).semantic_build_arg_secretcita só o nome da variável, nunca o valor.hosts_internosroda a checagem de rede do preflight com prazo de 2 s por comandodocker.hosts_internos_nao_verificavelsó 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.ymlO --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 schemaA 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.jsonInspeção remota via CCS:
runner manifest meu-app --resolved --json
# CCS frontend renderiza o manifest sem precisar de SSH no servidorVeja também
- `runner manifest schema` — JSON Schema do
.deploy.yml - Lint do manifesto —
diagnosticos[],--estaticoe o catálogo de regras - `runner secrets show` — apenas os secrets decifrados (não o YAML completo)
- `runner edit` — editar
.deploy.yml - `runner env set` — atualizar valores que aparecem em templates