Changelog

Todas as mudancas notaveis neste projeto sao documentadas neste arquivo.

O formato e baseado em Keep a Changelog, e este projeto adere ao Semantic Versioning.

[2.202.0] - 2026-11-09 — senha embutida em composto `{{::}}` é segredo literal

Corrigido

  • Sob secrets:, o texto fora dos {{::…}} de um valor composto e o default de {{::Rótulo?default}} passam a contar como segredo literal. Exemplo: postgres://u:senha@{{::Host}}/db. O manifest status mascara o valor inteiro e o manifest pin/publish recusam com segredo_literal (exit 14), sem mostrar o valor. Antes o valor passava como placeholder.
  • URLs continuam aceitas quando a senha do userinfo e o valor de parâmetro sensível da query vêm de um token, como postgres://app:{{::Senha}}@db/app e mysql://db/app?password={{::Senha}}. Usuário, host, porta e banco podem ser literais. https:// com texto literal no caminho é recusado.
  • {{::Senha?}} (default vazio) continua valendo.

[2.201.0] - 2026-11-08 — rollback manual com trava e rota desfeita

Corrigido

  • runner rollback que falha ao atualizar a rota depois de já tê-la gravado (modo verify_route: enforce) é desfeito: a versão anterior volta a rodar, a rota é regravada para ela com verificação e só então o container do rollback é removido. Se a versão anterior não estava rodando, ou se a regravação falhar, o container do rollback fica de pé e o erro diz para onde a rota aponta.
  • runner rollback e signal send --action rollback tomam a trava da app e mantêm o sinal de vida durante o rollback. Com a trava ocupada (um deploy do cron, por exemplo), recusam com lock_contended e dizem quem a segura, em vez de rodar em paralelo.
  • O 409 lock_contended de /api/v1/deploy tem o mesmo formato do webhook.

Adicionado

  • runner rollback --wait-lock: espera a trava liberar, como no deploy.
  • runner rollback -o json emite envelope no sucesso, na falha e na recusa, no mesmo formato do deploy. A recusa do deploy por trava também passa a trazer holder e deploy_id.

[2.200.0] - 2026-11-08 — rollback que reprova é desfeito

Corrigido

  • runner rollback que reprova (o container da versão anterior não sobe ou não fica saudável) é desfeito: a versão que estava no ar volta a rodar, o container do rollback é removido e o erro diz que a versão atual continua no ar. Se o container atual estava parado antes, ele continua parado. Se não der para religá-lo, a mensagem diz que ele ficou parado e sugere docker start <container> ou runner deploy <app> --force.
  • runner service serve reconsulta o IP do Traefik quando o filtro de origem recusa uma conexão (no máximo uma vez por minuto). O caso típico é o reboot, em que o Traefik sobe depois da API com outro IP. A troca só acontece com um único container candidato e quando o IP recusado é o dele.
  • O 409 lock_contended do webhook traz sempre holder e deploy_id (null quando não se aplica).

[2.199.0] - 2026-11-08 — correções de rollback, API remota e placeholders

Corrigido

  • runner rollback espera a saúde da versão anterior pelo modo da app (inclusive container_state e external_*, com a janela estável) antes de trocar a rota. Antes, "Running" bastava e o rollback podia anunciar sucesso com o site fora.
  • runner service serve relê o IP atual do Traefik ao subir (prazo de 10 s) e usa esse IP como proxy confiável. Antes valia o IP gravado no expose, e um Traefik recriado com outro IP deixava a API exposta fora do ar. Com zero ou mais de um container candidato, fica a lista do service.yml, com aviso.
  • O webhook de deploy responde 409 se um deploy pela CLI tomar o lock enquanto a imagem é resolvida, em vez de 202 seguido de falha.
  • {{:: __commit }} com espaço resolve em environment: e grava a revisão para o rollback, como a forma sem espaço.
  • A linha de tentativa do release: mascara valores de chaves com nome de segredo (ex.: *_PASSWORD) a partir de 8 caracteres.
  • runner adopt --synthesize-manifest --manifest-path X grava o manifesto em X, não mais na raiz.
  • O erro vpn_only_ausente do service expose cita o diretório dinâmico real do Traefik.

[2.198.0] - 2026-11-08 — driver Bitbucket Cloud

Adicionado

  • Repositórios do bitbucket.org com driver próprio. Clone, fetch e push do cofre usam o usuário x-token-auth, que vale para o access token de repositório e para o API token da conta. O arquivo de versão é lido pela API do Bitbucket, com o git como reserva. Ver Repositório em qualquer servidor git.

Corrigido

  • No Bitbucket, um caminho de arquivo que é diretório passa a dar caminho_e_diretorio, como nos outros provedores, em vez de ler a listagem como conteúdo.

[2.197.0] - 2026-11-07 — diferença de env em dados estruturados

Adicionado

  • runner preflight -o json: o componente env_drift amarelo passa a trazer dados, com cada chave divergente, o valor do manifesto, o padrão do placeholder e o valor em uso. Chaves sensíveis saem mascaradas. O texto de detail não muda. Ver runner preflight.

[2.196.1] - 2026-11-07 — `edit --show` com nome técnico e manifesto

Corrigido

  • runner edit <app> --show -o json passa a trazer nome_base (o nome técnico da app) e manifest_path (null = o .deploy.yml da raiz). As chaves de antes não mudam.

[2.196.0] - 2026-11-06 — driver Gitea e Forgejo

Adicionado

  • Driver Gitea/Forgejo (--provider gitea ou --provider forgejo): o arquivo de versão é lido pela API do servidor, com o git como reserva. Validade e escrita do token continuam provadas pelo git. Ver Repositório em qualquer servidor git.

[2.195.0] - 2026-11-06 — `runner app identidade`

Adicionado

  • runner app identidade: troca o nome técnico de uma app registrada (containers, rotas, imagens) sem tirá-la do ar. O nome antigo só sai depois que o novo está saudável e a rota nova está ativa. Ver runner app.

Modificado

  • Toda app registrada a partir de agora tem o nome técnico igual ao nome do diretório. Nenhuma app já registrada muda de nome.

[2.194.0] - 2026-11-06 — `runner app manifest-path`

Adicionado

  • runner app manifest-path: troca o manifesto da app dentro do repositório (monorepo). Antes de gravar, confere que o arquivo existe na branch rastreada, que o deploy o aceita e que declara a instância da app. O nome dos containers não muda. Ver runner app.

[2.193.0] - 2026-11-06 — `runner app origem` e `runner app instancia`

Adicionado

  • runner app origem: troca o repositório, o provedor e a branch rastreada num comando só. Confere a leitura do repositório de destino e a existência da branch nele antes de gravar. --dry-run mostra o que mudaria.
  • runner app instancia: leva a app para outra instância. Primeiro faz o deploy na nova, depois remove a antiga. Em falha, nada é removido.
  • Os dois respondem em envelope v2 (-o json) com o mesmo formato, para painéis e automação. Ver runner app.

[2.192.0] - 2026-11-06 — driver GitLab (gitlab.com e self-managed)

Adicionado

  • Driver GitLab para repositório do gitlab.com e de GitLab self-managed (--provider gitlab). Antes, os dois funcionavam como git genérico.
    • Clone por token com o usuário oauth2. Subgrupos, porta e caminho base ficam na URL.
    • version.source: file tenta o arquivo cru da API v4 (token com read_repository lê). Fora do 2xx, em falha de TLS ou de rede, o runner lê pelo git. Repositório por SSH lê sempre pelo git.
    • runner tokens test e tokens audit mostram escopos e validade do token do GitLab (scopes_auditaveis: true). Se a API do GitLab não responder 2xx, a prova é o git, sem marcar o token como inválido.
    • Barreira de token amplo no cadastro (add, edit, adopt, tokens set): api, sudo, admin_mode, create_runner, manage_runner e k8s_proxy barram sem --allow-broad-token. read_api, write_registry e read_user só avisam. Para deploy bastam read_repository e write_repository.

Corrigido

  • O token só vai para a API do próprio provedor do repositório. Repositório registrado que não se resolve não leva o token a API nenhuma.
  • A mensagem token_sem_escrita deixou de atribuir a recusa ao GitHub e traz a dica do GitLab.

[2.191.0] - 2026-11-05 — balanceador de domínio no Traefik (`runner lb`)

Adicionado

  • runner lb: um domínio do Traefik dividido por peso entre várias instâncias ou containers do servidor. Só com traefik.mode: local; em outro modo a escrita recusa balanceador_modo_nao_suportado (o status funciona).
    • runner lb criar <nome> --dominio <d> [--atende <id>]
    • runner lb inscrever --container <c> --balanceador <nome> [--servico <s>] [--forcar] (container de fora do runner, id custom:<container>)
    • runner lb peso <nome> <inscrito> <peso>
    • runner lb desinscrever <inscrito> [--forcar]
    • runner lb remover <nome>
    • runner lb reconciliar
    • runner lb status [<nome>]: só lê, com peso_efetivo (% do tráfego) de cada inscrito.
    • Todos aceitam --autor e -o json. Recusa sai com exit 1 e, no JSON, error.codigos e error.recusas. Capability lb.v1.
    • O arquivo gerado é lb--<nome>.yml na pasta dinâmica, com priority 5000.
  • balanceador: <nome> na instância do .deploy.yml: o deploy inscreve a instância (runner:<app>:<instância>) depois de aplicar a rota, e um erro no balanceador vira aviso sem derrubar o deploy. Na v1 vale para instância de rota única sem path_prefix; fora disso o deploy avisa (balanceador_rota_unica, balanceador_sem_rota, rota_com_path). Nome inválido barra o parse (nome_invalido).
  • Saída segura: unregister e destroy tiram a instância do balanceador antes de parar o container. Se ela for a única com tráfego, o comando recusa unico_com_trafego sem mexer em nada. Para passar mesmo assim: unregister --force ou destroy --force-lb.
  • O cron (fetch --deploy) reconcilia os balanceadores a cada passada e restaura um lb--*.yml apagado à mão. O runner traefik rollback não mexe nesses arquivos.
  • runner snapshot traz balanceadores[]. A instância inscrita traz balanceador: {nome, inscrito, peso, peso_efetivo}, também no runner status --json.
  • Lint do manifesto, cinco avisos novos que não barram o publish: balanceador_sem_rota, balanceador_rota_unica, rota_com_path, balanceador_inexistente e dominio_tem_balanceador. O manifest context traz traefik.balanceadores[].
  • Router de label Docker no mesmo domínio com priority acima de 5000 aparece como conflito_de_rota no lb status.

Corrigido

  • runner promote com traefik.manager.api_url configurada podia abortar com "Cannot drop a runtime…"; agora roda como o runner weight.

[2.190.0] - 2026-11-05 — snapshot: origem de compose, estado do lock e fixação

Adicionado

  • runner snapshot: cada instância traz origem_compose, ao lado de source_type. Vale true quando o .deploy.yml declara x-runner-source (app gerada de compose, a que o runner reconcile aceita) e false quando não declara. Fica ausente quando o manifesto não pode ser lido.
  • runner snapshot: cada app traz lock, ao lado de deploying, com o valor livre, em_uso ou parado. É a mesma leitura do deploy --force-unlock: parado é onde ele libera (dono sem atividade há 5 min) e em_uso é onde ele recusa. Um lock velho (dono morto ou 15 min sem atividade) sai livre, porque o próximo deploy o recupera sozinho.
  • runner snapshot: cada app traz fixacao_suportada e, quando o valor é false, fixacao_motivo (compose, layout_legado ou source_sem_git). O critério é o mesmo do manifest status. Em source_sem_git o manifest pin grava o manifesto direto.

[2.189.1] - 2026-11-05 — falha adiável no histórico, uma linha por episódio

Corrigido

  • Falhas adiáveis do deploy (source_em_mudanca, checksum divergente da source, tag_repostada, registry_limite e toomanyrequests no pull) passam a entrar no logs history. Entra uma linha FAILED por episódio, não uma por ciclo do cron.
  • A leitura remota do manifesto dá caminho_e_diretorio quando o caminho é uma pasta, inclusive com barra final. Ela avisa quando a leitura passa a falhar no mesmo commit e não corta o fetch para reservar tempo.
  • A saída do probe de healthcheck continua mascarando os valores dos .secrets mesmo quando o env da app não pode ser montado. Também mascara Bearer, JWT, password= e sk_live_. Nesse caso o ultimo_probe traz mascara: "padrao".

[2.189.0] - 2026-11-05 — healthcheck e secrets.enc explicados

Adicionado

  • Falha de healthcheck explicada: o erro, o log e o evento (ultimo_probe) trazem o modo, o alvo, o exit e a saída do último probe, com uma sugestão. Exemplos: a imagem não tem curl ou sh, localhost em Alpine, path atrás de autenticação.
  • Erro de decrypt do secrets.enc explicado: diz qual bundle falhou, se ele foi regravado depois do último deploy bom e se é compartilhado com outras apps deste servidor.

Corrigido

  • runner add --ckey v1 (ou c, s, 1, sym…) gerava como ckey o próprio texto do seletor. Agora gera uma ckey de verdade ou herda a do projeto. Apps registradas assim ficam com a ckey antiga até runner ckeys rotate-secret.
  • Com o lock do Traefik ocupado, o cron adia a app sem gastar tentativa e avisa uma vez por episódio. Passados 30 min com o mesmo dono, sai traefik_lock_preso (configurável em traefik.manager.apply_lock_preso_min).
  • A leitura do manifesto remoto em host git genérico baixa só o arquivo pedido.
  • Deploy que falha ao ler a versão (no_git_tags etc.) entra no logs history como FAILED, com o código.
  • runner add recusado na leitura de --token-file, --ckey-file, --env-file ou --token-stdin também entra em logs registros.
  • O REPO VER do runner fetch lê version.file em .toml e em texto puro, como o deploy já lia.
  • runner wizard com o .deploy.yml numa subpasta escreve o Caddyfile ao lado dele, com o volume e o root certos.

[2.188.0] - 2026-11-05 — histórico das tentativas de registro

Adicionado

  • Toda tentativa de runner add, runner adopt e add --from-compose fica em /opt/runner/logs/register-attempts.jsonl, registrada ou recusada, com o código estável do erro (no_git_tags, ckey_too_short…).
  • runner logs registros [--repo] [--app] [--resultado] [-o json] lista essas tentativas. Com add --deploy, a linha traz o desfecho do primeiro deploy. Feature logs.registros.v1 no --capabilities.
  • runner add aceita --trigger e --actor, gravados como origem da tentativa.

Corrigido

  • Número, booleano ou nulo num campo de texto do .deploy.yml (PORT: 8000 em environment, labels, hosts…) vale com a grafia escrita (1.10 fica "1.10") em toda forma de manifesto. Antes, chave de topo entre aspas, topo em fluxo ou âncora numa chave x- faziam o manifesto ser recusado com invalid type ... expected a string.

[2.187.0] - 2026-11-04 — prazos no deploy, wizard com Caddyfile, consumidores no status

Adicionado

  • manifest status e manifest context trazem outros_consumidores_locais já antes do manifest publish, com a mesma regra do publish. Feature manifest.status.consumidores.v1 no --capabilities.
  • --ckey aceita apelidos por nome e número: s, sym, symetric ou 1 pedem a simétrica; a, asym, assimetric ou 2, a assimétrica. Sem valor continua a assimétrica. v1 e v2 seguem aceitos.

Corrigido

  • O apply.lock do Traefik espera até 60 s. Depois falha com traefik_lock_ocupado, dizendo o pid, a operação e a origem de quem segura. Antes esperava sem limite.
  • A leitura remota do manifesto em host git genérico desiste em 10 s. Os docker da verificação de alias também, e o estouro vira aviso.
  • App sem .deploy.yml no repo avisa uma vez por commit novo, não a cada ciclo do cron.
  • O runner wizard gera front-static com Caddy já com o volume do Caddyfile. Se o repo não tem um, cria o mínimo: serve o dist/, SPA, /health e cache por exclusão.

[2.186.2] - 2026-11-04 — limpeza não apaga volume novo; uid no evento

Adicionado

  • O evento de deploy e o deploy-audit.json ganham o campo uid: o diretório da app (o mesmo do label ccs.systems/uid), ao lado do project.

Corrigido

  • A limpeza do fim do deploy não apaga mais a pasta de um volume que o commit acabou de declarar (./out:/srv/out, ./dist, ./public…), nem a de um volume que o commit removeu. O fim do deploy passa a usar o manifesto aplicado.
  • A verificação de alias de rede lê os aliases do docker inspect do container; falha da ferramenta vira aviso, alias que falta continua acusado.
  • manifesto_novo_invalido no cron vale em todo caminho (source: tag, .git na raiz, primeiro deploy), sem consumir a tentativa nem contar para o auto-pause.

[2.186.1] - 2026-11-04 — manifesto novo inválido derruba o deploy

Corrigido

  • Um .deploy.yml novo que chega pela branch e não parseia agora derruba o deploy com manifesto_novo_invalido (arquivo e motivo). Antes o deploy subia com o manifesto anterior e dizia "DEPLOY CONCLUÍDO". A raiz e o container no ar ficam intactos; no cron a app fica fora do ciclo até o conserto chegar ao repo, e aí implanta sozinha, sem -F.
  • O mesmo manifesto dá o mesmo resultado em qualquer forma de networks: (lista simples, inline ou mapeamento). Antes, backup: true em assets: era aceito na lista e recusado na forma inline.
  • O manifesto é relido sob a trava da app, e data.after.manifest.sha256 é o sha dos bytes que o deploy usou. Só vale como prova de "no ar" com data.after.status: completed.
  • segredo_literal não acusa mais URL de conexão sem credencial (REDIS_URL: redis://redis:6379/3). Com user:pass@ ou senha na query, continua vermelho; https:// nunca é isento.

[2.186.0] - 2026-11-04 — lint do manifesto: regras do servidor e o lint no deploy

Adicionado

  • Quatro regras do manifest validate que consultam o servidor, todas amarelas: healthcheck_imagem_sem_shell, healthcheck_localhost_alpine, url_dominio_de_outra_app e imagem_nao_verificavel (só quando o Docker não responde). Rodam sem --estatico. Ver Lint do manifesto.
  • O deploy avisa os achados do lint: uma linha ⚠ lint por achado na etapa PREPARE, e no cron um aviso (warn) no runner-cron.log. O campo lint entra no evento de deploy, no deploy-audit.json e no envelope -o json. O lint nunca bloqueia o deploy.

[2.185.0] - 2026-11-03 — o nome é da app; o project só agrupa

Modificado

  • O nome do container, do router e do service do Traefik, dos arquivos de rota, da imagem buildada, do canary e dos signals sai do nome_base da app, guardado no state. Numa app nova ele é o nome do diretório; o --slug continua aceito. Ver Nome da app e project.
  • O project: do .deploy.yml passa a ser só o agrupador: label ccs.systems/project, RUNNER_PROJECT, notificações e agrupamento no CCS. Ele pode repetir entre apps, na mesma instância inclusive, e o deploy, o rollback e o unregister de uma app não tocam na outra.
  • Apps já registradas não são renomeadas: na primeira leitura, o runner congela o nome que a app já usa (pelo container no ar). No primeiro deploy só o label ccs.systems/project pode mudar, nas apps em que ele divergia do project: (registradas com --slug: uma loja-api com project: loja passa de loja-api para loja).
  • Trocar o project: muda só o agrupamento. O amarelo projeto_alterado continua pedindo --aceita-avisos projeto_alterado e diz que nada é renomeado.
  • manifest context: apps[].projeto é o agrupador, sem o --slug.
  • Sem o bloco traefik.manager no config.yml, o journal.jsonl e o apply.lock do Traefik ficam em /opt/runner/state/traefik-manager, e não mais no diretório corrente. Os journals antigos ficam onde estavam, e o novo começa vazio.

Adicionado

  • Capability app.nome_base.v1 em runner --capabilities.
  • Label ccs.systems/nome-base nos containers.
  • nome_base no status --json, no instances --json, no snapshot e no manifest context.
  • Vermelho nome_base_de_outra_app: outra app registrada já tem o mesmo nome_base (com - e _ equivalentes) na mesma instância. Vale no runner add, no manifest pin/validate (exit 14, o --aceita-avisos não libera) e no deploy. Duas apps sem slug com o mesmo project: na mesma instância, que já derrubavam uma à outra, passam a ter o deploy recusado; registre uma delas de novo com outro --slug.

Removido

  • O vermelho project_de_outra_app_na_instancia da 2.184.0: repetir o project de outra app é o caso normal.

Corrigido

  • Remoção de rota pelo nome exato do arquivo ({dono}_{nome_base}__{instância}.yml): a app loja não leva mais a rota de loja-api. O dono do arquivo pode ter _ só quando é o dono do github.repo da app ou o default_user do runner; o marcador # managed-by: runner do arquivo só veta ou desempata, nunca torna da app um arquivo de outro nome. Um arquivo antigo de um dono com _ que não é nenhum dos dois deixa de ser podado sozinho: remova-o à mão.

[2.184.0] - 2026-11-03 — troca de project gerida pelo runner

Modificado

  • Trocar o project do .deploy.yml deixa de ser recusado. O vermelho projeto_trocado virou o amarelo projeto_alterado: o pin pede --aceita-avisos projeto_alterado.
  • No primeiro deploy verde depois da troca, o runner remove o container, o service e a rota do nome antigo desta app. Só sai o que tem o ccs.systems/uid dela. Container sem uid e arquivo de rota só saem se nenhuma outra app registrada usa aquele nome na instância.
  • O nome de cada app é o efetivo: o --slug do registro vence o project do manifesto. Uma app com slug, como a irmã que divide o project com outra, não é tocada pela troca da outra.

Adicionado

  • Vermelho project_de_outra_app_na_instancia (componente identidade): trocar para um project cujo nome de container colide com o de outra app registrada na mesma instância. O --aceita-avisos não libera. Vale no pin, no write --local e no validate. A troca que chega pelo repositório não passa por ele.

[2.183.0] - 2026-11-03 — lint do manifesto

Adicionado

  • runner manifest validate passa a trazer diagnosticos[]: cada achado com código, gravidade, caminho (JSON Pointer), linha, coluna, fim, mensagem e sugestão. Quando a correção é mecânica, a sugestão traz aplicar, edições de texto para aplicar sobre o arquivo original sem perder comentários nem ordem. A coluna conta caracteres Unicode e o fim é exclusivo.
  • runner manifest validate <app> --estatico: lint rápido, só com o texto e os arquivos da app. Não consulta Docker, Traefik nem DNS e não pega a trava.
  • Capacidade manifest.lint.v1 em runner --capabilities.
  • Treze regras novas: version_git_tag_em_dist, version_file_nao_estruturado, version_file_inexistente, image_mode_obsoleto, imagem_sem_image_mode, caddy_sem_caddyfile, healthcheck_dialeto_alheio, healthcheck_cmd_string, rede_alias_compartilhada, rede_alias_aninhado, secret_prompt_sem_required, secret_vazio e bloco_removido. Quatro são vermelhas e recusam o pin; as demais são amarelas e pedem --aceita-avisos. Catálogo e correções em lint do manifesto.
  • Novo componente lint no relatório do validate. Ele aparece quando o candidato vira YAML e não é compose. No pin sem conteúdo novo ele não aparece.

Modificado

  • Os campos de antes do validate e do pin (components[], codigos[], exits) não mudam. Nenhuma regra nova bloqueia o deploy.

[2.182.0] - 2026-11-01 — manifest schema

Adicionado

  • runner manifest schema: imprime o JSON Schema do .deploy.yml que o runner aceita, num envelope com runner_version, schema_id, dialeto e schema. É só leitura e funciona em qualquer servidor.
  • O schema_id (sha256: do JSON canônico) só muda quando o schema muda. Serve de chave de cache para o editor.
  • Capacidade manifest.schema.v1 em runner --capabilities.
  • Cada release publica o arquivo manifest-schema.json no GitHub Release.
  • O schema marca com extensões x-runner-* as referências ao servidor, as regras de unicidade, os campos sensíveis, a versão de origem e os aliases (type, environment na instância, tag_pattern).
  • Obsoletos levam deprecated: true: image_mode, github, type: docker-build, source.type: dist, o notify em mapa (aceito, mas sem notificação) e as grafias antigas de image_mode e env_sync (inclusive Remote, que o runner escrevia).
  • Número, booleano e nulo valem em campo de texto (PORT: 8000, interval: 30, file: null); o runner grava como texto. Lista, mapa ou bloco vazio vale como vazio.

Modificado

  • O runner passa a gravar env_sync em minúsculas (remote, skip) no manifesto que ele escreve (wizard --from-compose, reconcile). Continua lendo as duas grafias. Em app gerada por --from-compose antes da 2.182.0, o reconcile vê a grafia nova como diferença uma vez: o dry-run sai Pending e o --apply reescreve o arquivo. Não dispara deploy.
  • Algumas diferenças entre o schema e o runner são conhecidas, como chave desconhecida em bloco aberto. O editor deve confiar no manifest validate para o veredito. Número, booleano ou nulo em campo de texto, em manifesto com chave x-, é recusado pelo runner: issue #230, aberta. Lista em `runner manifest schema`.

[2.181.0] - 2026-11-01 — manifest publish e apps de source tag

Adicionado

  • runner manifest publish <app> --sha256 <local.sha256> -y [--actor]: leva o manifesto fixado ao repositório num commit na branch dist, sem force, com o assunto chore(deploy): .deploy.yml via CCS e o corpo só com os nomes dos campos. A app volta a seguir o repositório; a raiz não muda (só o modo volta a 644) e o fixado vai para o histórico.
  • O conflito é pelo conteúdo do manifesto: commit de código no repo entre o pin e o publish não atrapalha (o publish entra em cima dele). Manifesto mudado ou apagado no repo, fixado sem base ou corrida no push saem 11 (manifest_conflict), com current_sha256 e repo_commit.
  • Antes do push, as checagens de segredo do pin rodam de novo: segredo literal, credencial da plataforma ou valor mascarado saem 14, sem o valor.
  • Prazo total de 100 s, contado do início do comando (inclui a espera pela trava); estouro sai 12 (git_timeout).
  • A resposta traz provoca_deploy (true com version.source: git-commit) e outros_consumidores_locais (apps deste servidor que leem o mesmo manifesto do mesmo repo e branch).
  • Capacidade manifest.publish.v1 em runner --capabilities.

Mudança de comportamento

  • Apps source: type: tag deixam de ser exceção: pin, status, --base-commit, discard, write --local e o fetch comparam o fixado com o manifesto da tag resolvida, e o fetch marca e limpa repo_divergiu (o on_repo_change: hold passa a valer). O pin.json ganha o campo aditivo base_ref: "tag"; um pin antigo é reancorado na tag uma vez, na primeira passada do fetch, sem marcar divergência.
  • Nessas apps, pin --base-commit exige commit com tag do padrão (sem tag: exit 12 base_commit_sem_tag), o publish sai 16 publish_nao_suportado_em_tag e publicavel sai false. Com fixado, o fetch poda tags e branches apagadas no remoto.
  • A mensagem do exit 13, o aviso do write --local e os helps de manifest, pin, write --local e discard voltam a citar runner manifest publish; em app de tag, citam só o discard.

[2.180.1] - 2026-11-01 — correções no manifesto

Corrigido

  • A mensagem do exit 13 (manifesto_fixado) e os helps de manifest pin e manifest write --local deixam de citar runner manifest publish, que ainda não existe. Agora indicam runner manifest discard (voltar ao repositório) ou runner manifest pin (editar o fixado).
  • manifest validate e pin: candidato que nem é YAML válido não acusa mais instancia_orfa em falso; o plano sai vazio.
  • manifest write sem --local e sem -y responde em JSON (confirmacao_exigida, exit 1), como os demais comandos. Antes saía texto no stderr.
  • O help de manifest write --local explica a diferença para o pin: aviso amarelo grava sem confirmação e volta em avisos_validacao.

[2.180.0] - 2026-10-31 — validação do candidato (manifest validate)

Adicionado

  • runner manifest validate <app>: valida um .deploy.yml candidato (pelo STDIN) contra este servidor, sem gravar nada e sem a trava da app. Devolve um relatório por componente (parse, semântica, segredos, required, refs do Traefik, certificado, domínios, portas, Docker, dependências, hosts internos, órfãos), cada um com codigos[] estáveis, e o plano do que muda: campos, instâncias, domínios e efeitos no salvar.
  • Exits: 0 para verde ou amarelo; 14 para vermelho, inclusive parse reprovado, com o relatório do que deu para checar; 10 só para candidato acima de 64 KiB ou não UTF-8. A lista de componentes varia (parse reprovado, candidato compose): indexe por name, nunca por posição.
  • Gravidade com dois níveis. Vermelho (segredo literal, required sem valor, domínio servido por outra app do runner, porta ocupada…) recusa sempre. Amarelo (middleware, tls_options, rede ou volume inexistentes, domínio em router fora do runner, domínio repetido entre instâncias, que podem estar atrás de um LB…) exige confirmação no pin.
  • Fonte fora do ar nunca dá verde: o componente sai amarelo *_nao_verificavel.
  • Capacidade manifest.validate.v1 em runner --capabilities.

Mudança de comportamento

  • runner manifest pin roda a validação inteira. Vermelho sai 14 sempre. Amarelo sai 14 com avisos_nao_confirmados até que todos os códigos venham em --aceita-avisos. O pin sem --aceita-avisos passa a recusar amarelo. O mesmo conteúdo (só nota, --on-repo-change ou --base-commit) e o reparo de um pin interrompido não revalidam o ambiente. A validação roda antes da trava da app.
  • runner manifest write --local recusa vermelho com 14 e grava amarelo sem pedir confirmação, devolvendo os avisos em avisos_validacao.

Corrigido

  • O aviso semantic_build_arg_secret cita só o nome da variável, sem o valor padrão.

[2.179.0] - 2026-10-31 — contexto do ambiente (manifest context)

Adicionado

  • runner manifest context <app>: o que existe no servidor para quem escreve 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. Saída JSON, só leitura, sem a trava da app.
  • Oito fontes lidas em paralelo, cada uma com prazo próprio, e teto de 5 s. Fonte fora do ar não derruba o comando: o resultado sai parcial (exit 0) e fontes.<x>.status diz ok, indisponivel, parcial ou timeout.
  • Só nomes, nunca valores: das envs saem as chaves; de outras apps, nada.
  • App que casa mais de uma sai app_ambigua (exit 1); app inexistente, app_inexistente (exit 1).
  • Capacidade manifest.context.v1 em runner --capabilities.

Mudança de comportamento

  • runner preflight confere os middlewares e as tls_options de routing também em labels de container e na API do Traefik, não só no file provider. Middleware de label deixou de ser acusado como inexistente.
  • Deixou de dar falso aviso em routing.tls_options: x@file que existe.
  • O preflight lê o diretório do file provider descoberto pelo container do Traefik (antes, o paths.traefik_dynamic do config.yml). O deploy continua usando o paths.traefik_dynamic: mantenha os dois iguais (runner traefik info mostra o descoberto).
  • A gravidade continua aviso.

[2.178.0] - 2026-10-31 — manifesto local fixado (pin)

Mudança de comportamento

  • runner manifest write --local agora fixa o manifesto. Ele vale em todos os próximos deploys, inclusive os do cron, até manifest discard. Antes valia só no próximo deploy --local --force. A resposta ganha aplica_com, vigente, base e avisos, e os erros passam a ser os do pin.
  • Com um manifesto fixado e o repositório tendo mudado o manifesto depois dele, o deploy conclui com o fixado e sai com exit 3, com a pendência manifesto_repo_divergiu no RUNNER_RESULT. Isso vale para o cron, o runner deploy e o deploy --force.

Adicionado

  • runner manifest status: mostra o manifesto vigente (repo ou local), a base, o repositório, a divergência, os diffs e o que o último deploy aplicou. É só leitura e sai com exit 0 mesmo com o repo fora do ar.
  • runner manifest pin: valida e salva um .deploy.yml no servidor, sem deployar. Uma recusa vermelha sai com exit 14, com o relatório. As opções são --nota, --actor, --on-repo-change deploy|hold e --base-commit, que reconhece o repositório e limpa a divergência (base_atualizada). --aceita-avisos é aceito e entra em uso com os avisos amarelos, numa versão seguinte.
  • runner manifest discard: a raiz volta ao manifesto do repositório. O fixado vai para o histórico com o --actor.
  • on_repo_change define o que o cron faz quando o repositório muda o manifesto fixado:
    • deploy (o padrão): implanta com o fixado e marca a pendência;
    • hold: não implanta, e a app aparece como SEGURADA no relatório do lote.
  • Com um manifesto fixado, o fetch compara o manifesto do repositório com a base em todo ciclo que lê o remoto (app nunca implantada, com deploy pendente ou em dia), não só quando chega commit novo: a divergência aparece no primeiro fetch. Na 2.180.0 isso não vale para app com source: type: tag.
  • Eventos manifest_repo_divergiu (só com os nomes dos campos), manifesto_fixado_adulterado e manifesto_fixado_ilegivel.
  • O envelope de deploy traz after.manifest {vigente, sha256}.
  • Capacidade manifest.pin.v1 em runner --capabilities.

Segurança

  • Com um manifesto fixado, nada reescreve a raiz da app além do pin e do discard:
    • fetch e deploy não copiam do repositório;
    • upgrade e o write remoto recusam (exit 13);
    • config-backups restore pula a raiz.
  • Uma raiz reescrita por fora do runner faz o deploy, o rollback e o cron recusarem com manifesto_fixado_adulterado (exit 17), sem efeito nenhum.
  • git fetch com prazo sob a trava da app: pin, discard, write --local e manifest show|write desistem de remoto parado e têm prazo de 60 s no total. Remoto pendurado sai 12 remote_unavailable (git_timeout) em vez de segurar o cron e os deploys da app.
  • O status mascara (***) todo valor literal sob secrets:. As mensagens de invalid_manifest do pin e do manifest write trocam esses literais por <valor>.

Ver runner manifest.

[2.177.0] - 2026-10-31 — `manifest write` valida como o deploy

Mudança de comportamento

  • runner manifest write (repo e --local) recusa com invalid_manifest (exit 10) o que o deploy recusaria: acme-dns sem provider, path_prefix duplicado no mesmo domínio, release: inválido. Antes, o manifesto era gravado e só falhava no deploy. A recusa vem antes de tocar o arquivo ou o git. Ver runner manifest.
  • Capacidade manifest.validate_load.v1 em runner --capabilities.
  • runner add e runner adopt recusam --manifest-path absoluto, com .. ou vazio (invalid_manifest_path), antes de gravar qualquer coisa.

Adicionado

  • Os erros do manifest show|write trazem message. detail, current_sha256 e os exits 10/11/12 não mudam.

Corrigido

  • Os backups do manifesto (.runner/manifest-backups/) são criados com permissão 600, e a pasta com 700. Antes saíam legíveis por todos.
  • Em app de monorepo (manifest_path), a recuperação do .deploy.yml da raiz, o switch-instance, o edit --instance e o migrate-source usam o manifesto do serviço, e não o .deploy.yml da raiz do repositório.

Segurança

  • O manifest_path registrado não sai mais da pasta do repositório. Um valor absoluto fazia o deploy copiar um arquivo qualquer do servidor para a raiz da app. Valor inválido (de registro antigo) agora é pulado com aviso ou recusado (manifest show|write, secrets push, switch-instance, edit --instance), nunca trocado pelo .deploy.yml da raiz.

[2.176.0] - 2026-10-29 — retenção dos logs configurável

Adicionado

  • runner logs rotation status|set: a retenção dos logs do Runner no /etc/logrotate.d/runner passa a ser configurável, em dias, com --audit-days (histórico de deploy) e --cron-days (log do cron), de 1 a
    1. O CCS mostra e edita esses valores em Detalhes do server › Subsistemas. O status informa também quantos dias de histórico existem de fato e avisa de arquivo em /etc/logrotate.d que repete os caminhos do Runner. Ver runner logs.

Comportamento

  • Só o set escreve no arquivo. Nem fetch, nem self-update tocam nele.
  • Na primeira escrita, o arquivo manual é guardado em /opt/runner/backups/logrotate/runner-AAAA-MM-DD (com -N, sem sobrescrever). Só a linha rotate muda; su, maxsize e os caminhos ficam.
  • O arquivo novo é validado com logrotate -d no diretório inteiro antes da troca. Reprovado, nada muda.

[2.175.3] - 2026-10-29 — o que mudou, visível na notificação de deploy

Adicionado

  • Os commits automáticos do Runner (bundle de secrets e .deploy.yml) trazem no corpo os NOMES do que mudou (adicionada:, alterada:, removida:, .deploy.yml alterado:). O subject não muda e o valor nunca aparece. Ver Notificações.
  • Deploy por dist: a notificação mostra a lista Commits: que o builder 0.5.3 grava no corpo do commit deploy: vX.Y.Z (manchete, itens e autores). Commit de dist antigo, sem a lista, sai como antes.
  • Linha "configuração atualizada pelo runner: …" na notificação. Com commits reais no deploy, o auto-commit fica fora da contagem e de "Por:".

Alterado

  • A linha de configuração mostra até 10 nomes e depois "e mais N chaves", e sai uma por mensagem (+N para as demais). "Por:" mostra até 5 autores. A mensagem tem orçamento de 4000 caracteres (Discord: 2000), com corte numa quebra de linha e … no fim.

Limitações conhecidas

  • Só o esquema v2 de secrets tem detalhe por chave. Bundle legado traz a linha secrets atualizados (bundle legado: ...) até runner secrets migrate.
  • O re-key da ckey e a remoção do bundle do repo continuam commitando sem corpo.

[2.175.2] - 2026-10-29 — pendências do cutover e dos E2E

Corrigido

  • runner manifest --resolved avalia __commit, __commit_short e __version; sem valor sai <no-deploy:...> em vez de <UNRESOLVED:...>. Ver `runner manifest`.
  • runner rollback de app registrada com --slug usa o project do slug no container, no histórico e nos eventos (deploy_rollback e rollback_success).
  • runner service expose --vpn-only falha na hora com vpn_only_ausente quando o host não tem o middleware vpn-only, em vez de esperar 60 s.

Documentação

  • ip_permitidos_invalido não cobre CIDR duplicado: a duplicata é removida.

[2.175.1] - 2026-10-29 — eventos de registry e de rollback no audit

Adicionado

  • image_tag e image_digest nos eventos de deploy de app registry (deploy_started, deploy_success, deploy_failure).
  • Evento deploy_rollback no audit, UM por rollback em qualquer desfecho, com from_version, to_version e status. O rollback_success legado segue sendo emitido. Ver `runner audit`.

Corrigido

  • Rollback que falhou entra no runner logs history como falha (rollback: ).
  • before.container_name do deploy em JSON usa o nome real do container.
  • runner unregister remove os logs de falha e as imagens por digest sem outro uso; runner service unexpose zera api.trusted_proxies.

[2.175.0] - 2026-10-29 — Runner Server: header de autenticação, restrição por IP e erros com código

Adicionado

  • runner service config --auth-header <NOME> --secret-stdin: header próprio de autenticação da API, com segredo lido do stdin (32 a 4096 bytes). O modo é exclusivo: o X-API-Key passa a dar 401. --auth-padrao volta ao padrão e gera uma rk_ nova. O status traz api.auth.
  • --ip-permitidos, --limpar-ip-permitidos e --atras-de-proxy (também no service expose): lista de CIDRs da API e dos hooks, com 403 ip_nao_permitido fora dela. O status traz api.ip_permitidos.
  • Erros de runner service * em -o json: um objeto {"error":"<codigo>"} no stdout e exit diferente de zero. Tabela em `runner service`.

Alterado

  • --secret-stdin num terminal é recusado (segredo_stdin_terminal).
  • Com a API exposta, config --ip-permitidos reescreve a rota: o vpn-only@file sai e a faixa da VPN entra na lista. API exposta por versão anterior pede reexpor_necessario.
  • mTLS não pode ser combinado com a lista de IPs.

[2.174.1] - 2026-10-29 — retorno do cutover do CRM e do E2E da ccs-sys#187

Corrigido

  • secret_scan: private_key só é High com corpo base64 de 40+ caracteres depois do cabeçalho. Cabeçalho sem corpo vira aviso. Arquivo ilegível conta como High, e erro de leitura do grep vira aviso sem perder os achados. vendor/, third_party/ e *.min.js não são varridos.
  • __commit e __commit_short com source: local num clone git vêm do HEAD. Árvore suja gera aviso.
  • runner audit e runner activity leem os arquivos rotacionados (.1, .N, .N.gz) até o --limit. truncated_older_gz indica .gz que não abre.
  • Container sem HEALTHCHECK no Docker 29: o JÁ IMPLANTADO voltou a disparar, sem reimplantar a mesma versão nem deixar o rollback em failed.

Adicionado

[2.174.0] - 2026-10-28 — achados do cutover do CRM

Adicionado

  • release: no .deploy.yml: comando (migração) que roda depois da imagem e antes da troca do container, com target, retries, retry_delay e timeout. Falha aborta o deploy com release_falhou e o container no ar fica intacto. O rollback não roda o release. Ver Migração antes da troca do container.
  • Placeholders {{::__commit}}, {{::__commit_short}} e {{::__version}} em build.args e environment.
  • healthcheck.hostname, com aviso para o modo docker_labels.
  • Aviso de rota quebrada no resumo do deploy e no campo avisos do -o json.
  • runner traefik: scan, inspect domain, routers-list e domains-list mostram routers de label; doctor ganha traefik_regra_duplicada; router-create --priority.
  • runner deploy --instance|-i: confere a instância e falha com instancia_divergente.

Corrigido

  • A mensagem do FETCH de app com source de arquivo não fala mais em pull/.
  • tls-watch separa backend indisponível (backend_indisponivel) de certificado padrão.

Documentação

  • build.target (existe desde a v2.23.13) e security.allow_sensitive entram na referência do .deploy.yml.

[2.173.0] - 2026-10-27 — deploy por imagem de registry

Adicionado

  • Deploy por imagem de registry, sem repositório git: runner add --image, runner deploy --image-tag e rollback pela imagem gravada. Ver Deploy por imagem de registry.
  • runner registry login|logout|test: credencial de leitura do registry, cifrada por app.
  • O cron detecta tag nova (tags) ou digest novo (track) no registry.
  • Webhook assinado de deploy e runner hook secret. O segredo vai para um arquivo 600, nunca para a tela. runner service expose --hooks-publicos abre o hook fora da VPN.
  • Action devborlot/runner-deploy-action@v1 para avisar o runner depois do build.
  • image_tag e image_digest no status --json e no snapshot.
  • tags ignora pré-release (-rc.1, -beta) por padrão; só um padrão com - (ex.: v*-rc*) as escolhe.
  • Tag de versão repostada com outro digest não é implantada sem --force (tag_repostada): o webhook responde 409, o cron avisa sem pausar.
  • O webhook responde 422 (com message) para credencial recusada e tag ou repositório inexistente, 409 app_pausada para app pausada e 503 para o limite do registry. O cron respeita o Retry-After do 429 (5 min a 6 h).
  • pull_policy é recusado em app registry (registry_pull_policy).
  • O rollback de app registry vale até o registry publicar uma tag/digest novo que o tags/track pegue; status e snapshot mostram a imagem da versão no ar.

Modificado

  • runner add --repo sem --branch agora falha com branch_required. Antes era erro do clap, com exit 2.

[2.172.0] - 2026-10-25 — gatekeeper de domínio

Adicionado

  • Estratégia de certificado por instância (routing.tls, provider, dns_gate, proxy). O resolver do Traefik é escolhido pelo tipo de desafio e provider, nunca pelo nome. Ver Domínios e certificados.
  • Gatekeeper no cron: o certificado é ativado sem deploy quando o domínio passa a apontar, nos modos file e labels.
  • Sonda de origem para domínio atrás de proxy, com diagnóstico de 525/526 e de redirect em loop.
  • runner route status e runner route activate, e runner deploy --skip-dns-gate.
  • dominios[] no snapshot, no status --json e no preflight.
  • Wizard: passo de apontamento depois do domínio.

Modificado

  • App sem routing.tls segue igual: o portão de DNS do deploy e o tls-watch de sempre.

[2.169.0] - 2026-10-25 — saída de máquina e correções de sucesso falso

Corrigido

  • runner edit: branch inexistente, remoto inacessível ou app sem clone agora falham com exit ≠ 0 e código (branch_inexistente, repo_inacessivel, app_sem_clone, branch_sem_manifesto, instancia_com_container). Antes saíam 0 sem mudar nada.
  • runner reconcile --apply e runner secrets reset-bundle: push que falha conclui com pendência (exit 3); antes saía 0.
  • deploy --force-unlock recusa dono em atividade (lock_vivo).
  • secrets diff não revela valor: cada lado sai com a impressão digital; valores só com --reveal (auditado).
  • secrets reset-bundle --json exige --yes.
  • runner init --set grava o valor com o tipo do campo e recusa chave desconhecida.

Adicionado

  • -o json em edit (--show e mudanças), tokens set e tokens test. Com saída de máquina, erro sai como {"error": "<código>", "message"}.
  • reconcile: erros com código e push: {status, erro} no envelope.
  • lock show --json traz alive.

[2.168.0] - 2026-10-24 — rota de domínio pendente continua HTTPS

Corrigido

  • Domínio que ainda não aponta para o servidor tinha o router criado sem TLS, e o HTTPS dava 404. Agora a rota segue HTTPS com o certificado padrão do Traefik (autoassinado) até o DNS apontar; aí rode runner deploy <app> --force para emitir o certificado.

[2.167.0] - 2026-10-24 — rotação de ckey: op_id, geração, lock e ckeys hash

Adicionado

  • ckeys rotate-secret: --op-id, --generation (com ja_aplicada e geracao_obsoleta) e -i. A rotação roda sob o lock da app (app_lock_ocupado); app v2 recusa com esquema_nao_suportado; com -o json, erro sai como {"error": "<código>"}.
  • runner ckeys hash <app>: hash da ckey do runner, geração e esquema; com --bundle, se o bundle da rotação decifra.
  • Ver runner ckeys.

[2.166.0] - 2026-10-24 — bloco paused no snapshot

Adicionado

  • runner snapshot: cada item de projects[] traz o bloco paused (reason, note, at, by, origin) quando a app está pausada, igual ao do runner status --json. fetch_status e paused_reason não mudam.

[2.165.0] - 2026-10-24 — a pausa diz por quê, quem e desde quando

Adicionado

  • runner pause <app> --motivo "<texto>" (alias --reason).
  • Toda pausa (manual, automática e da adoção) registra a nota, o autor, a origem e o instante. O histórico fica em logs/pause-events.jsonl.
  • runner status e runner list mostram [PAUSED]. O status sugere runner unpause em vez de runner restart.
  • JSON: bloco paused no status, paused: true no list, e os campos da marca no pause e no unpause.
  • Ver runner pause / unpause.

[2.164.0] - 2026-10-23 — rotação de ckey publica o bundle certo

Corrigido

  • runner ckeys rotate-secret publica no repositório o mesmo bundle que re-cifrou (o bundle: do manifesto, senão o do target, senão secrets.<instância>.enc). Antes, com bundle por instância, ia o secrets.enc da raiz, ainda na ckey antiga.

Adicionado

  • A prova da rotação traz bundle_path, branch, push_commit e generation.

[2.163.0] - 2026-10-23 — sources de arquivo

Adicionado

  • Source local com detecção automática. O cron implanta quando a pasta muda (detect: tree, padrão) ou quando um arquivo de controle muda (detect: control_file). path: copia de uma pasta fora da app.
  • Source https. Baixa um .tar.gz, .zip ou arquivo único, com checksum_url e assinatura minisign opcionais, e detecta versão nova por control_url (ETag/Last-Modified sem ela).
  • Source rsync. Sincroniza por ssh ou rsync://, com arquivo de controle.
  • App de arquivo sem version: recebe a versão <tipo>-<12 hex da revisão>.
  • status --json e snapshot trazem source_type e revision.
  • Ver Sources de arquivo e Deploy local.

Atenção no upgrade

  • Apps type: local existentes passam a ser implantadas sozinhas quando o conteúdo de pull/ muda. Para manter só o deploy manual, use detect: manual. O primeiro ciclo depois do upgrade pode fazer um redeploy.

Corrigido

  • tokens test --json traz provider e scopes_auditaveis também no erro.

[2.162.0] - 2026-10-22 — repositório de qualquer servidor git

Adicionado

  • runner add, edit --repo, adopt e wizard aceitam URL de qualquer servidor git (HTTPS, SSH ou formato scp), com --provider para servidor próprio. owner/name continua significando GitHub. Ver Repositório em qualquer servidor git.
  • snapshot, status e list trazem provider e repo_url.

Segurança

  • O token só vai para o host do repositório: GITHUB_TOKEN só para github.com, e --token-from recusa token de outro host.
  • Repositório por SSH usa a chave do servidor; o token é ignorado com aviso.

[2.161.0] - 2026-10-22 — API do runner publicável com segurança; modo server nasce desativado

Modificado

  • O modo server nasce desativado. runner service init instala a unit sem habilitar nem iniciar, e a API nasce desligada. Para ligar: runner service init --enable-api (ou depois service config --enable-api) e runner service start, que inicia e habilita no boot.

Adicionado

  • runner service expose --domain <d> [--vpn-only] e unexpose. Publicam a API num domínio do Traefik, com TLS. Só o Traefik e o próprio host falam com a API. Ver API remota.
  • Deploy pela API assíncrono. 202 com deploy_id e GET /api/v1/deploy/{id} para o status. 409 quando a app já está em operação.

Corrigido

  • A API não sobe sem chave nem com chave vazia. O webhook sem secret não escuta mais em 0.0.0.0. O corpo dos POST é limitado a 64 KiB.
  • O mTLS incompleto falha ao subir, em vez de cair para HTTP. A CA é relida sem restart.
  • systemctl stop do serviço espera o deploy da API terminar, em vez de matá-lo no meio.
  • Container da mesma versão com outro nome de geração deixou de ficar órfão.

[2.160.0] - 2026-10-22 — aviso de host interno que não resolve; `deploy --wait-lock`

Adicionado

  • Aviso de host interno que não resolve no runner preflight (componente rede) e no deploy. Só aviso, não bloqueia. Ver runner preflight.
  • runner deploy --wait-lock[=SEG]: espera o lock da app em vez de falhar com lock_contended.

Corrigido

  • env unset sem -i agora diz em que camada a chave está quando ela foi gravada na camada da instância.

[2.159.0] - 2026-10-21 — `ccs.mitigation` no `.deploy.yml`

Adicionado

  • ccs: { mitigation: <modo> } no topo do .deploy.yml e em instances.<nome>. O valor da instância vence o do topo, e vira o rótulo ccs.systems/mitigation do container. Modos: never, limit-only, limit-then-freeze e freeze-first. Qualquer outro valor para o deploy com invalid_manifest_syntax.
  • Runner anterior à 2.159.0 recusa o campo (unknown_manifest_field): atualize o runner do servidor antes de declarar ccs:.

[2.158.0] - 2026-10-21 — token sem escrita barrado; rollback não derruba a app; logs mascarados

Corrigido

  • Token que só lê passava como "ok" e o cofre não subia. Agora:
    • tokens set, tokens test e tokens audit testam a escrita quando a app tem cofre;
    • o deploy confere a escrita antes de comitar o cofre e termina pendente (secrets_push_token_rejected) em vez de "CONCLUÍDO". Ver runner tokens.
  • runner rollback -v <versão no ar> parava o container. Agora recusa com rollback_para_versao_no_ar.
  • Logs de auditoria:
    • os arquivos são criados em 0640;
    • tudo que é gravado passa por máscara de token;
    • o erro de build guarda só as últimas 60 linhas.
  • env unset sem nada a remover saía com 0. Agora sai com erro chave_nao_encontrada. Falha no push do cofre também é erro (cofre_nao_propagado).
  • A ajuda do runner restart prometia aplicar env. Não aplica: use runner deploy <app> --force.
  • runner lock show mostrava "pull" durante o build. Agora mostra as fases prepare, secrets e image.

Adicionado

  • runner logs scrub [--dry-run]: mascara tokens já gravados nos logs. Ver runner logs.
  • build.secrets no runner generate-config. O recurso existe desde a v2.135.0.

[2.157.0] - 2026-10-19 — nome de exibição da app; `keep_versions` corrigido

Adicionado

  • runner app rename <app> --display-name "<x>" e --clear. Guarda no servidor um nome de exibição da app, que vale para todas as instâncias. Ver runner app.
  • display_name no runner snapshot -o json, em projects[] e em instances[]. Fica null quando a app não tem nome.
  • runner --capabilities. Manifesto em JSON com comandos, features e formatos de saída. Ver runner version.

Corrigido

  • keep_versions não limpava versões semver sem v. Pastas como 1.56.0 se acumulavam sem limite. Agora a limpeza:
    • reconhece semver com e sem v;
    • ordena por versão;
    • nunca apaga a versão em deploy nem a que está no ar. No primeiro deploy depois de atualizar, as versões acumuladas são apagadas de uma vez, com container e imagem.
  • O keep_versions do config.yml do runner não tinha efeito. A instância sem valor no manifesto usava 3 fixo. Agora usa o valor global. Ver .deploy.yml.

[2.156.1] - 2026-10-16 — o git do runner ignora o credential helper do host

Corrigido

  • Com credential.helper store guardando um token antigo, o fetch e o secrets push usavam esse token e falhavam com Repository not found. Quando o runner tem token, o git dele roda sem credential helper.

[2.156.0] - 2026-10-16 — `manifest show/write --local`

Adicionado

  • runner manifest show --local e write --local leem e gravam o .deploy.yml da raiz da app no servidor, sem git. A escrita confere o sha256 do arquivo, guarda backup e troca o arquivo de forma atômica.

Modificado

  • runner tokens set testa o token com git ls-remote. Token sem permissão de leitura do conteúdo do repo é recusado com token_sem_contents.

[2.155.1] - 2026-10-16 — `tokens set` limpa a credencial antiga da URL do remote

Corrigido

  • Clones com o token antigo embutido na URL do remote continuavam usando esse token depois do runner tokens set. Agora o comando limpa a URL.

[2.155.0] - 2026-10-15 — preflight acusa pasta sem escrita; deploy local volta a funcionar (#218, #219)

Adicionado

  • runner preflight <app> ganha o componente volumes. Para cada pasta montada rw dentro da app, diz se o USER da imagem consegue escrever ou se o deploy vai corrigir o dono. Só leitura. Ver Permissão de escrita nas pastas montadas.

Corrigido

  • Deploy local (source: { type: local }) voltou a funcionar. O código fica em <app>/pull/, sem git. Ver Deploy de repositório local.
  • Mensagem de falha sem container anterior não fala mais em "redeploy de mesma versão" quando não houve redeploy.

[2.154.1] - 2026-10-14 — a limpeza de pastas antigas não apaga mais volume (#217)

Corrigido

  • A pasta de um volume declarado podia ser apagada, com os dados, ao fim do deploy, quando tinha nome de pasta de build (out, public, dist, build, docs, content…), por exemplo out:/srv/out. Agora a origem de todo volume e asset do manifesto é preservada.

[2.154.0] - 2026-10-14 — posse dos volumes corrigida automaticamente (#216)

Alterado

  • Quando a imagem declara um USER não-root, o Runner corrige sozinho a posse das pastas montadas em que esse usuário não conseguiria escrever, e avisa no log do deploy:
    • antes do build, com o USER do Dockerfile ou da imagem;
    • na subida do container, se o teste de escrita ainda falhar.
  • Só pastas dentro de /data/apps/<app>, sem seguir links. Se a escrita continua falhando depois da correção, a mensagem diz que a causa não é posse (somente leitura, ACL, SELinux).

[2.153.1] - 2026-10-14 — a causa real da falha chega ao evento e ao histórico (#212, #213)

Corrigido

  • A causa real de uma falha de deploy era descartada. O evento de falha guardava só o resumo genérico ("container could not start…"), igual em toda falha de container. Agora o evento traz cause (o erro real, por exemplo a pré-checagem de volumes com o caminho e o uid) e phase. O logs history, a saída do cron e o last_error gravam a mensagem inteira.
  • runner debug last-error mostra a causa e a fase, e categoriza a falha por ela. Duas categorias novas: volume sem permissão de escrita e variáveis de required:.
  • Log arquivado de container que nunca existiu. Quando a falha acontece antes de o container existir, o runner não grava mais a resposta de erro do Docker como se fosse log da aplicação.

[2.153.0] - 2026-10-13 — snapshot com o estado do deploy

Adicionado

  • runner snapshot passa a trazer, por app: app_status (o status do registro), deploying (se há um deploy em andamento agora) e last_deploy_error (primeira linha do último erro, sem credencial).
  • Repositório no snapshot sem github: no manifesto: vem do registro e, na falta dele, do remote do clone, sempre como dono/nome. Campos vcs_provider e repo_source.
  • runner tokens audit mostra um trecho identificador de cada token (início e fim), sem revelá-lo.

[2.152.1] - 2026-10-10 — a instância nova de uma app v2 ganha o cofre dela (#210)

Corrigido

  • Depois de edit -i, o env set --secret pedia uma ckey numa app v2. A instância nova ficava sem cofre. Agora o edit -i cria o cofre da instância nova, e o env set --secret também o cria para registros que já ficaram nesse estado.
  • env set sem instância no registro passa a usar a mesma resolução do deploy, e não production.

[2.152.0] - 2026-10-10 — identidade no preflight, aviso de instância não fixada, `secrets copy` para v2

Adicionado

  • runner preflight <app> mostra a identidade resolvida do registro: instância e de onde veio, projeto efetivo, cofre, container, router(s), domínio(s) e branch. Ver runner preflight.
  • Aviso de instância não fixada em list, status, preflight e doctor, quando o registro não fixa a instância e o manifesto declara mais de uma.
  • secrets copy com origem ou alvo no esquema v2, sem passar valor pela saída. Novas flags --from-instance e --to-instance.

Alterado

  • add --token-from de token com escopo amplo não é mais barrado: o token já está neste servidor. Os escopos sensíveis são listados e a herança fica no audit log. Token novo (--token) continua exigindo --allow-broad-token.

Corrigido

  • preflight conferia o roteamento sempre em production. Agora usa a instância do registro.
  • edit --branch garante a branch de destino com a credencial do registro antes de gravar a troca. Se não conseguir, nada muda.

[2.151.5] - 2026-10-09 — leituras mostram o container que está no ar (#204)

Corrigido

  • Depois de um rollback, status, instances, list, snapshot e health podiam mostrar [WARN] e a versão revertida, que fica parada ao lado da que voltou. Agora, na mesma instância, vale o container que está rodando.

[2.151.4] - 2026-10-09 — rollback atualiza o state e o histórico (#204)

Corrigido

  • Depois de um rollback, o state continuava na versão revertida. Agora passa para a versão no ar, e o logs history registra o rollback. O cron segue sem desfazer o rollback: só um commit novo dispara deploy.

[2.151.3] - 2026-10-09 — restart e container logs acham o container certo (#203)

Corrigido

  • restart respondia "Restarted" sem reiniciar nada em instância com hífen (staging-b). Depois de um rollback, mirava o container parado da versão revertida. container logs tinha os mesmos dois defeitos. Os dois acham agora o container pelo registro e pela instância.

[2.151.2] - 2026-10-09 — comandos de app usam a instância do registro (#203)

Corrigido

  • health, restart, versions, container logs, config get e config diff usavam production quando -i não vinha. Agora usam a instância do registro, como deploy e rollback.

[2.151.1] - 2026-10-09 — list e snapshot também separam os registros do mesmo repo (#203)

Corrigido

  • list e snapshot davam ao segundo registro de um repo o container do primeiro quando o diretório do primeiro é igual ao project:. O fallback pelo project: agora só acha container legado (sem uid).
  • env validate -i <instância> fora do manifesto passa a recusar com Instance '<nome>' not found.

[2.151.0] - 2026-10-09 — registros do mesmo repo não cruzam containers; rollback na instância do registro (#203, #202, #200)

Corrigido

  • Dois registros do mesmo repo (--force-duplicate --slug) enxergavam os containers um do outro (#203). instances, status, list, health, versions e unregister casam container pelo ccs.systems/uid (o diretório do registro); o project só vale para container antigo sem uid. Antes, o unregister de um registro podia parar o container do outro.
  • Pausa do operador sobrevive a qualquer escrita do state (#203). Com paused_reason: manual ou adocao, o status fica paused até resume / finalize-adoption, e o cron pula a app.
  • edit --branch valida a branch com a credencial da app (#203). Falha de acesso vira não consegui consultar o remoto, não "branch inexistente".
  • env validate por instância (#203): mesmas camadas e mesmo arquivo v2 do deploy; nova flag -i/--instance.
  • rollback usa a instância do registro (#202), como o deploy; nova flag -i/--instance. Vale também para o signal de rollback e a tool MCP runner_rollback.
  • snapshot manda o domínio resolvido (#200), não o template {{::Var}}.

Documentação

[2.150.0] - 2026-10-07 — credencial efetiva, fonte única

Adicionado

  • Pull, secrets push e tokens audit resolvem a mesma credencial, nesta ordem: state (cifrada) → GITHUB_TOKEN → credencial embutida no remote do clone → credential helper do git. Só a fonte é logada.
  • runner tokens adopt-clone <app>: migra a credencial embutida na URL do remote do clone para o state cifrado e limpa a URL.
  • tokens audit -o json e secrets push -o json informam a fonte da credencial (credential_source).

Corrigido

  • secrets push dava push_no_token para app que fazia pull com a credencial do clone. Erros de git pull/fetch/push mascaram credencial embutida em URL (https://***@host).

Aviso

  • Credencial embutida na URL do remote fica em texto puro no .git/config do clone. Migre com runner tokens adopt-clone <app>.

[2.149.0] - 2026-10-07 — deploy com pendência pós-deploy sai com exit 3 (#199)

Alterado

  • Deploy concluído com o push de secrets falho sai com exit 3 (antes 1, igual a deploy falho). Deploy falho segue 1. No cron e no deploy --all, a pendência conta como deploy com aviso: não vira falha nem soma retentativa.
  • Linha RUNNER_RESULT no fim do stdout dos comandos de deploy, com RUNNER_RESULT_LINE=1 no ambiente.
  • data/ e logs/ automáticos têm o dono alinhado ao USER da imagem mesmo com conteúdo, sem seguir links nem sair da árvore da app.

[2.148.1] - 2026-10-07

Corrigido

  • Falha intermitente "could not read Username" ao autenticar o git com token.

[2.148.0] - 2026-10-07 — `preflight --env-file`

Adicionado

  • runner preflight --env-file <path> (- = stdin): valores de env/secret de uma app ainda não registrada, só em memória, para checar o manifesto antes do registro.

[2.147.0] - 2026-10-06 — preflight de DNS confere o autoritativo e o destino (#198)

Corrigido

  • O preflight de DNS consulta o servidor autoritativo da zona. Nome que não existe vira vermelho, com sugestão de nome parecido, em vez de "pode estar propagando".
  • Verde só quando o registro aponta para este servidor (ou para o proxy da Cloudflare). Outro IP vira vermelho.
  • Com DNS vermelho, o deploy não pede certificado ao Let's Encrypt, poupando a cota. Corrigido o DNS, runner deploy <app> --force.

[2.146.1] - 2026-10-06

Sem mudança de comportamento para o operador (processo de build do release).

[2.146.0] - 2026-10-06 — volumes com o dono do `USER` da imagem (#197)

Corrigido

  • Volumes automáticos nasciam root:root e imagens com USER não-root entravam em crashloop. Agora a pasta é entregue ao usuário da imagem.
  • Teste de escrita antes de subir: montagem declarada que nega escrita aborta o deploy com mensagem clara, sem crashloop.
  • Variável *_FILE apontando para arquivo inexistente na montagem de secrets aborta o deploy; runner preflight ganhou a checagem secret_paths.

[2.145.0] - 2026-10-06 — segredos por arquivo em vez de argv (#195)

Adicionado

  • Formas por arquivo para segredos que iam na linha de comando (visíveis no ps): add --token-file, add/adopt --ckey-file, add --env-file, preflight --token-file, secrets paste-encoded --value-file.

[2.144.2] - 2026-10-06 — secret v1 por instância não sobrescreve o da outra (#196)

Corrigido

  • env set <app> KEY -i <instância> --secret numa app com ckey simétrica gravava no .secrets único da app, e a mesma chave em production e staging colidia. Agora recusa com secret_por_instancia_v1 e orienta a migrar a app (runner secrets migrate <app> -i <instância>). Ver Secrets diferentes por instância.

[2.144.1] - 2026-10-03 — adopt e env status dizem a verdade no terminal (#193, #194)

Corrigido

  • adopt no esquema v2 dizia → .secrets para secret que foi para o bundle (#193). O resumo e a lista por chave passam a dizer bundle v2, e o JSON ganha secrets_destino (secrets | bundle_v2).
  • env status sem -o json não dizia se o container está desatualizado (#194). Nova linha no fim: container: <nome> — DESATUALIZADO, roda valor antigo em N chave(s): … → runner deploy <app> --force, ou em dia com o server, ou nenhum no ar. Mesma comparação por hash da #192, sem rede.

[2.144.0] - 2026-10-02 — manifesto que muda chega ao server, container desatualizado aparece, release no padrão do builder (#191, #192)

Corrigido

  • v2: valor alterado no environment: do .deploy.yml não chegava ao server quando a chave já existia (#191). O deploy só acrescentava chave nova; o container subia com o valor velho e o deploy ficava verde (um cliente, N8N_WEBHOOK_TRACKING). Agora o manifesto vence quando a linha dele MUDOU desde o último deploy (registro por instância em .runner/manifest-env.<instância>.json, só hashes), e o valor vai para a camada onde a chave mora (.env ou .env.<instância>). Linha igual à do último deploy não pisa num env set do server. Sem registro anterior, só avisa a divergência. Valor gerado ou de prompt nunca sobrescreve.

Adicionado

  • env status: container_stale por chave e container.stale / container.stale_keys (#192). Compara o valor de cada variável no container em execução com o valor atual do server, por hash e só em memória; nada de valor ou hash sai no JSON. Acusa o container que segue com valor antigo depois de um env set ou de uma mudança no manifesto sem deploy --force. Secrets do esquema v2 ficam null (o valor atual é cifrado).

Mudado (release)

  • make publish no padrão do /proj/builder. Pega a trava de build uma vez, como release (trava --release: os agentes cedem); grava uma linha de métrica por rodada, com fila e execução por fase, em /proj/runner/.release-metrics.jsonl; e no fim avisa se o release passou de 600 s ou se uma fase cresceu (tempo-relatorio --orcamento). A suíte completa do release roda no perfil test (antes --release): mesmos 2.364 testes e zero falhas, 490–536 s → 28–184 s.

[2.143.1] - 2026-10-02 — adopt v2 com envs capturadas (#189)

Corrigido

  • adopt --ckey v2 com envs capturadas falhava na re-encriptação (#189) (has no ckey configured: o passo era o reset-bundle do v1) e deixava a app registrada sem as envs; o deploy seguinte subia verde sem elas. No v2 os secrets capturados agora entram cifrados (só com a chave pública) no arquivo da instância, nada em .secrets em texto, e o arquivo é empurrado pelo secrets push --force. As envs comuns seguem no .env.
  • Falha depois do registro (v1 ou v2) agora diz adopt_incompleto: '<app>' FICOU registrada, mas … com o comando para completar, em vez de apontar para runner doctor/runner add.

[2.143.0] - 2026-10-02 — secrets push confere o remoto antes de dizer "nada a pushar" (#190)

Corrigido

  • secrets push respondia "idêntico no repo" comparando só com o clone local (#190). O bundle era comparado com o índice/HEAD do pull/, sem fetch, e o caso de um cliente ficava preso em "nada a comitar" com o CCS acusando que o remoto diverge. Agora, quando não há diff local, o runner faz fetch e compara com origin/<branch>:
    • commit local que nunca subiu (push anterior rejeitado/interrompido) → pusha;
    • remoto com OUTRO bundle por cima → alinha ao remoto, regrava o bundle local (a autoridade) e pusha;
    • igual ao remoto → "nada a pushar", como antes. Sem token, ou se o fetch falhar, segue o comportamento anterior com aviso.

[2.142.2] - 2026-10-02 — re-registro v2 não apaga secrets do repo em silêncio (#188)

Corrigido

  • Re-registrar uma app no esquema v2 apagava os secrets do repo sem aviso (#188). O registro gera uma identity nova e o arquivo local nasce vazio; no primeiro push ele sobrescrevia o arquivo do registro anterior. Os valores sumiam do repo, o deploy ficava verde e o container subia sem eles. Agora o add/adopt em v2 olham, antes de gravar o state, o arquivo da instância no repo: se é de OUTRA identity e tem secrets,
    • add recusa com bundle_de_outra_identity, listando as chaves, e desfaz o diretório criado; --discard-remote-bundle aceita o descarte;
    • adopt descarta (o container é a fonte da verdade) e avisa quais chaves deixam de existir. Arquivo sem secrets, ou da mesma identity, segue como antes.

[2.142.1] - 2026-10-02 — descarte de bundle em app sem envs (#187)

Corrigido

  • --discard-remote-bundle (e o adopt) falhava em app sem envs: não há texto claro para cifrar o bundle substituto (No .env / .env.<instance> / .secrets content to encrypt). Nesse caso o bundle antigo é removido do repo; o primeiro deploy não encontra bundle remoto e grava o novo na fase 8.

[2.142.0] - 2026-10-02 — registro com bundle de outra ckey (#187)

Corrigido

  • Re-registrar uma app com ckey nova deixava no repo o bundle da ckey antiga (#187). O registro não tinha bundle local para empurrar (sem envs para cifrar), o bootstrap push não mudava nada e o primeiro deploy morria na fase 1.4 (failed to decrypt remote secrets.enc). Agora o add/adopt testam, antes de gravar o state, se a ckey do registro abre o bundle do repo para a instância (mesma resolução de arquivo do deploy):
    • add recusa com bundle_de_outra_ckey e desfaz o diretório criado — passe a ckey original para recuperar os secrets, ou --discard-remote-bundle;
    • adopt regrava sempre (o container é a fonte da verdade, como já era com envs capturadas);
    • ao descartar, o bundle é regravado com a ckey nova no MESMO arquivo que o deploy lê e empurrado antes do clone de pull/ — também com --deploy, em que o bootstrap comum é pulado. Sem token, recusa antes do registro.

Adicionado

  • runner add --discard-remote-bundle (#187).

[2.141.2] - 2026-10-02 — corte de novo depois do revert (#186)

Corrigido

  • Depois de revert-adoption não havia como cortar de novo: o corte exigia pendente, e refazer o adopt --corte-pendente é recusado no warm quando o container não tem envs. deploy --replace-adopted passa a aceitar a adoção revertido (o adotado já está de volta no ar), e o deploy comum segue barrado (adocao_em_andamento) também nesse status.

[2.141.1] - 2026-10-02 — corte de adoção: pausa e checagem de container (#186)

Corrigido

  • adopt --corte-pendente não deixava a app pausada: a gravação final do adopt (status: running) sobrescrevia a pausa. A marcação de adoção pendente passa a ser a última escrita do comando.
  • inspect de container/imagem inexistente devolvia sucesso: o 404 do Docker ({"message":"No such container"}) era aceito como objeto. Com isso o corte acusava corte_em_andamento sem existir __ccs_antes. Inspect sem Id agora é erro, com a mensagem do daemon.
  • adoption status de app não registrada respondia como se não houvesse adoção; agora falha com app_desconhecida.

[2.141.0] - 2026-10-02 — GitHub App por app e adoção com corte (#185, #186)

Adicionado

  • --github-app <APP_ID> em add, adopt e edit (#185). Escolhe a credencial de GitHub App já na instanciação do deploy. No add/adopt a instalação do App no repo é resolvida na hora (falha cedo se a chave do App não está registrada ou o App não está instalado no repo) e a referência ghapp:v1:… vai cifrada no state; edit --github-app equivale a tokens set --github-app. Conflita com --token/--token-from/ --token-stdin (e --remove-token no edit); no adopt exige --repo.
  • Adoção com corte — parada e substituição (#186).
    • adopt … --corte-pendente: registra a adoção como pendente e pausa a app (paused_reason: adocao); o cron não deploya e um deploy comum é recusado com adocao_em_andamento.
    • deploy <app> --replace-adopted --janela <s> [--antigo-id <id>] [--dados compartilhar|copiar|mover|sem]: para o adotado, trata os dados, renomeia-o para <nome>__ccs_antes (restart no), sobe o novo e espera o health dentro da janela. Falha → reverte sozinho (prefixos revertido, janela_estourada, reversao_falhou); outros guardas: container_mudou, corte_em_andamento, colisao_de_volume, adocao_nao_pendente. Sucesso → cortado, antigo preservado parado e app ainda pausada.
    • revert-adoption <app>: remove o novo, devolve nome e restart policy ao adotado, religa-o e tira o roteamento do novo (revertido).
    • finalize-adoption <app>: destrói o __ccs_antes (e os volumes de origem com --dados mover), limpa replaces_container e despausa (finalizado).
    • adoption status <app> [--json].

[2.140.0] - 2026-10-02 — adoção sem tropeços e branch por instância no snapshot (#181–#184)

Adicionado

  • Snapshot com a branch efetiva de cada instância (#184). Cada instância passa a ter branch (resolvida pela mesma ordem do deploy: state da app → source.branch → github.dist_branch → main), source_type, source_tag (tag resolvida em type: tag), image e commit_full. Na lista flat, a branch da instância ocupa o campo branch da entrada. Staging e PR deixam de aparecer sem branch, e a produção deixa de mostrar o dist_branch do manifesto quando o runner add --branch gravou outra.

Corrigido

  • adopt --synthesize-manifest com --token-from em repo privado (#181): a gravação do manifesto ia sem credencial (remote_unavailable). Passa a resolver o token como o registro, inclusive credencial de GitHub App.
  • Manifesto sintetizado sem as portas do container (#182): sem domínio no Traefik a porta é o único acesso, e a app adotada ficava inacessível. Agora as portas publicadas vão para ports: (IP e protocolo preservados), com o aviso ports_copied sobre a janela em cada deploy. Com domínio, segue sem copiar (ports_published).
  • adopt comum não registrava o container adotado (#183): o adotado sobrevivia ao primeiro deploy. Container sem label do runner passa a ser gravado em replaces_container, e o primeiro deploy o substitui (parando antes, se compartilhar volume ou porta — #180).

[2.139.0] - 2026-10-02 — nunca dois containers gravando no mesmo volume (#180)

Corrigido

  • Blue-green com volume gravável compartilhado (#180). O container novo subia ao lado do antigo e os dois escreviam no mesmo volume até o health — em banco de dados isso corrompe (MariaDB no E2E do DeployAlly, com container adotado). Agora, quando o novo e o que ele substitui gravam no mesmo volume Docker nomeado, ou na mesma pasta quando o antigo é o container adotado, o runner para o antigo antes de subir o novo e o religa se o novo não subir ou não ficar saudável. Há janela de indisponibilidade nesses casos; data//logs/ auto-montados continuam no blue-green sem janela.
  • Primeiro deploy pós-adoção com porta publicada (#180): o container adotado não era parado (sem label do runner nem versão anterior) e a porta colidia. Passa a ser tratado como o container anterior.

Na atualização: apps com volume nomeado gravável passam a ter uma janela curta de indisponibilidade em cada deploy, em troca de nunca haver dois processos no mesmo volume.

[2.138.0] - 2026-10-01 — versão git-commit sem pingue-pongue entre servidores (#179)

Corrigido

  • version.source: git-commit contava commits de secrets de outro servidor como versão nova (#179). Dois servidores no mesmo repo (produção e tester) se redeployavam a cada env set/secrets push do outro. A versão passa a ser o primeiro commit, a partir do HEAD, que é de código ou que toca o bundle que este servidor lê; commits do runner que só mexem no bundle de outro servidor são pulados. env set no próprio servidor continua disparando o redeploy pelo cron, como antes.

  • O pull/ passa a ser clonado com histórico de 50 commits (era 1), para a conta acima funcionar já no primeiro clone.

  • Audit do --allow-broad-token sempre rotulado como add (#164): o rótulo passa a ser --allow-broad-token; o comando real (tokens set, adopt, edit, add) segue no argv gravado.

Na atualização: app com git-commit cujo HEAD é commit de secrets de outro servidor muda de versão uma vez (passa a ser o commit de código) e é redeployada uma vez com o mesmo código.

[2.137.0] - 2026-10-01 — credenciais git: GitHub App, PAT fine-grained e token clássico sob alerta (#164)

Adicionado

  • GitHub App como credencial da app (#164). runner github-app register --app-id <N> < chave.pem guarda a chave cifrada com a mkey e confere com o GitHub que ela é deste App. runner tokens set <app> --github-app <N> acha a instalação no repo da app, prova que dá para escrever e grava a referência no lugar do token. A cada uso o runner gera um installation token de 1 h, restrito ao repo da app e a contents: write + metadata: read, com cache cifrado até 5 min antes de vencer. Nada de credencial de longa duração em disco além da chave do App. github-app list|remove.
  • Push de credencial de App só na branch de deploy (o GitHub não restringe token por branch; a trava é do runner, e um ruleset do lado do GitHub completa).
  • runner tokens audit: para cada credencial, o tipo (clássico, fine-grained, GitHub App), os escopos além do necessário e a validade. Uma chamada ao GitHub por token distinto. Também em -o json.
  • tokens list mostra o tipo de cada token (offline, pelo formato) e avisa quando há clássico; tokens test alerta escopo além da medida.

Alterado

  • Token clássico continua aceito, sob protesto: tokens set e adopt passam a exigir --allow-broad-token para token com escopo sensível (delete_repo, admin:org, workflow…), como add e edit já exigiam, com o alerta de segurança e registro no audit.
  • add --token-from de uma app que usa GitHub App reaponta a referência para o repo novo (instalação do mesmo App), em vez de copiar a do repo de origem.

Corrigido

  • Mensagem do deploy com lock ocupado (#178): dizia "já existe um deploy em execução" mesmo quando quem segurava era um fetch do cron. Agora diz que o app está ocupado por outra operação e aponta runner lock show; o detalhe de quem é vem logo em seguida.

[2.136.0] - 2026-10-01 — lock que diz quem está usando o app (#178); limpeza completa na transição de rotas

Adicionado

  • Contenda de lock legível (#178). O lock registra a operação, a origem (cron/cli/api/webhook e o ator do CCS), a fase atual e a última atividade. Quando outra operação está usando o app, a mensagem diz quem é, há quanto tempo, em que fase e o veredito: em andamento, possivelmente travado (5 min sem atividade) ou runner antigo sem fase. Continua começando por lock_contended:, que é como o CCS reconhece a classe.
  • runner lock show <app> (também -o json, com verdict, holder, elapsed_s, idle_s) e runner lock release <app> --force.

Corrigido

  • "Ver log completo" vazio na contenda de lock (#178): o fetch passa a registrar a contenda, com o dono do lock, no logs/deploy.log da app.
  • Contenda de lock contava para o auto-pause (#178): agora é falha transitória, como as de rede.
  • Lock perdido por deploy longo: a idade para considerar um lock abandonado (15 min) passa a contar da última atividade, não do início.
  • Transição domain: ↔ routes: deixava o service da versão anterior no arquivo do Traefik (achado na validação da 2.135.0 no tester): router, service e leafs do modelo antigo saem na mesma transação.
  • runner status, instances, snapshot e health mostravam — para instância só com routes:: usam o domínio da primeira rota.

[2.135.0] - 2026-09-30 — segredos fora de log e history, identidade do deploy no container, roteamento com `routes:` (#170, #172–#177)

Adicionado

  • Identidade do deploy dentro do container e no notify (#177). Todo container sobe com RUNNER_APP, RUNNER_PROJECT, RUNNER_INSTANCE, RUNNER_VERSION, RUNNER_CONTAINER e RUNNER_DEPLOY_ID (só leitura, sem socket nem hook). Os eventos deploy_success/deploy_failure levam container_id (12 chars = HOSTNAME do container), container_name, previous_container_name e previous_container_id (JSON do webhook, audit deploy-events.jsonl e variáveis de template do notify:), e o deploy_id deles é o MESMO RUNNER_DEPLOY_ID do container.
  • Segredo fora do history do shell (#176).
    • --token sem valor (em add, adopt, edit, preflight, wizard) pede o token num prompt oculto com eco de *. Sem terminal, erra apontando --token-stdin em vez de travar.
    • runner shell-hook imprime (e --install grava em /etc/profile.d/runner-history-mask.sh) um gancho bash/zsh que troca, no history, o valor de env set/secrets set/--token/--ckey/--value por ********. Um processo não alcança o history em memória do shell que o chamou; quem mascara é o gancho. --scrub mascara o que já está nos arquivos de history.
    • Valor sensível digitado na linha de comando: o runner mascara as linhas antigas nos arquivos de history e, sem o gancho, avisa que a sessão atual ainda guarda o valor.
    • runner env set <app> <KEY> sem valor já pedia o valor oculto (#124); o prompt agora diz (oculto).

Corrigido

  • Token em build arg vazava no log quando o build falhava (#175). Build arg cujo valor veio de {{::VAR}}/${VAR} ou tem cara de token (ghp_, github_pat_, gho_, ghs_, ghu_, glpat-) é trocado por *** na linha de comando logada e no stderr do docker, antes de virar last_error, history, notify ou JSON. Novo build.secrets (BuildKit --secret, arquivo temporário 0600, fora do argv e da imagem); valor vazio falha o build. O validate avisa quando um build arg recebe segredo.
  • routes: sem domain: era ignorado (#173). A fase de roteamento olhava só domain:: o deploy dizia "Sem domínio", não gerava router e o anterior ficava apontando para a versão removida (502/504 com DEPLOY CONCLUÍDO). O gate agora é domain: OU routes:; status, list e notify mostram o domínio da primeira rota. Instância sem rota nenhuma (modo local) remove o roteamento que o runner escreveu antes.
  • Trocar traefik.mode deixava o router do modo anterior vencendo (#174). Em docker_labels/none, o deploy remove o arquivo do file provider da instância que sobrou do modo local (ele vencia as labels → 504).
  • Comandos de reparo do traefik em host sem API (#172). Sem API respondendo, a verificação pós-escrita cai para none com aviso, em vez de falhar e fazer rollback (foi esse rollback que agravou o incidente da #171).
  • Falha do push de secrets pós-deploy parecia falha do deploy (#170). O aviso sai antes do bloco de sucesso, em WARN, dizendo que o deploy está no ar e que só o passo de publicar os secrets cifrados falhou. A execução segue terminando com erro (#148), com a mesma explicação na mensagem final.

[2.134.2] - 2026-09-28 — incidente 28/09: routers órfãos na transição single↔multi-route (#168, #171, #169, #166)

Corrigido

  • Transição domain: ↔ routes: apagava nada do modelo anterior (#168). Ao acrescentar routes: a uma app que já rodava com domain: singular, o router legado {p}-{i} sobrevivia no arquivo apontando para a versão que o próprio deploy removeu, com a MESMA regra Host() do -{port}-0 novo → 502 por sorteio (sys.deployally.com caiu 6 min em 28/09 17:24Z). No sentido inverso (revert para domain:) os -{port}-{n} ficavam órfãos. Agora, antes do apply, os routers desta instância (só os do arquivo dela) que não estão no conjunto novo recebem RouterDelete; o weighted legado recebe ServiceDelete; a poda de órfãs leva as leafs.
  • Rollback do traefik-manager gravava a config estática DENTRO do dynamic_dir e reescrevia o diretório inteiro (#171) — traefik-manager v0.3.4. O dynamic/traefik.yml intruso fazia o file provider do Traefik abortar o callback (template {{ normalize .Name }}) e ficar com a memória velha: o disco correto nunca chegava ao Traefik (503 por ~40 min). A estática volta para o caminho dela; restore só reescreve o que mudou.
  • runner rollback falhava com "No previous version found" mesmo com versões guardadas (#169): o alvo do rollback agora sai da listagem de versões em src/<instância>/, não só do .previous_version — é a mesma fonte que o status mostra como kept.
  • runner snapshot reportava o container de outra app do mesmo projeto (#166): a busca usava project: do manifesto (compartilhado por jogo, jogo-web…); agora usa a identidade da app (uid), project: só como fallback para container legado.

[2.134.1] - 2026-09-28

Fixed

  • Inspect de imagem tipado (#165): inspect_image_config rodava docker inspect <ref> sem tipo. Com um container de mesmo nome da referência (ex.: --name redis rodando a imagem redis), o Docker devolvia o CONTAINER, e o deploy lia dele o healthcheck, os VOLUME (proteção contra volume anônimo) e o User (chown). Agora usa docker image inspect, montado num ponto só (image_inspect_args), que também serve o image_exists_locally. Os demais docker inspect do código visam container por nome e não mudaram.

[2.134.0] - 2026-09-27

Added

  • adopt --dry-run manda captured.image_ref (#162): a referência EXATA da imagem inspecionada, o .Config.Image do container adotado (é também o image: do manifesto sintetizado). Formato: string, ex. "postgres:16-alpine"; ausente só quando o container não informa imagem. Contrato de image_has_healthcheck explicitado: fala da imagem image_ref, a do container ADOTADO, não da que o deploy vai usar (resolve_container_image). Se o manifesto trocar a tag ou for para build, o consumidor não deve usar o booleano. Nenhum campo existente mudou.

Fixed

  • Dry-run e deploy decidem "a imagem tem HEALTHCHECK" pela mesma função (#161): ImageHealthcheck::from_image_config, usada por parse_image_config_json (deploy) e por captured.image_has_healthcheck (dry-run). Antes divergiam em Test: ["CMD"] e em Test com elemento não-string. Agora os dois contam como ausentes: prefixo sem comando (["CMD"], ["CMD-SHELL"], só elementos em branco depois do prefixo) e qualquer elemento não-string. Muda o deploy: uma imagem com essas formas (só config forjada; Dockerfile não produz) deixa de ter o HEALTHCHECK honrado, e o runner injeta o probe do .deploy.yml.

  • --network-alias não vaza mais para a rede principal (#163). O docker run emitia --network-alias para os aliases de TODAS as redes; o Docker os aplica na rede principal (o primeiro --network), e o network connect --alias depois só acrescentava o alias na rede declarada. O alias ficava nas duas. Em prod, o deployally (declarado só na internal_ccs_da, --internal) resolvia pela public, a rede do Traefik. Agora o run leva só os aliases da rede principal; os das outras redes vão só no network connect --alias da própria rede.

Atenção no upgrade

Quem hoje resolve um alias pela rede errada deixa de resolver no primeiro deploy com este runner (o alias só some quando o container do dono do alias é recriado). Levantamento em prod de 27/09 (somente leitura), apps com alias numa rede que não é a principal:

App Alias Rede declarada Vazava para Consumidores achados
deployally-sys deployally internal_ccs_da public ccs-sys (DEPLOYALLY_API_BASE=http://deployally:5000): já está na internal_ccs_da desde a 2.196.0 e alcança o IP interno (200). Hoje resolve pelo IP da public; depois, pelo interno.
jogo jogo-api jogo_operacao public jogo-manager, já na jogo_operacao
jogo-match jogo-match jogo_operacao public jogo-manager, já na jogo_operacao
jogo-manager jogo-manager jogo_operacao public nenhum achado
inboxer-worker inboxer-worker mta mysql nenhum achado (inboxer-sys está na mta)

Nenhum consumidor achado depende do vazamento. O levantamento olhou env de container e o dynamic do Traefik; config embutida em imagem ou em arquivo não foi varrida. Antes do upgrade, confira se algum app fala com esses aliases por fora da rede declarada e ligue-o à rede certa primeiro.

[2.133.0] - 2026-09-26

Added

  • adopt --dry-run manda captured.image_has_healthcheck (#158): se a IMAGEM declara HEALTHCHECK — só o booleano, nunca o comando. No deploy o HEALTHCHECK da imagem vence o do manifesto; com isso, quem revisa o dry-run sabe se o healthcheck levado do container vai valer. Inspect da imagem indisponível → campo ausente (não false).

Fixed

  • adopt leva o StartPeriod do healthcheck do container (#159). Sem ele o manifesto caía no default (30s) e um container que boota devagar falhava o healthcheck e revertia no primeiro deploy.
  • unregister remove os backups do state e o .lock (#160). A cadeia .backups/<app>/ guarda o github_token cifrado e ficava no servidor depois de remover a app, sem aparecer no relatório; o .lock sobrevivia e passava para uma app recriada com o mesmo nome. Agora saem os dois (incluindo instâncias <app>@…) e o relatório os lista. Nome exato: app não leva o app2.

[2.132.1] - 2026-09-26

Fixed

  • runner validate (categoria docker) reprovava image_mode: prebuilt com "Unknown image_mode" — o manifesto que o próprio adopt da 2.132.0 sintetiza (#157). O validate mantinha uma lista paralela de modos; agora decide pelo mesmo deserializer do deploy (config::ImageMode), e aceita as mesmas grafias.

[2.132.0] - 2026-09-26

Fixed

  • Adoção de imagem pronta não esconde mais o código da app (#157). A síntese do adopt gerava type: image sem image_mode, e o default bind-mount montava o checkout da branch (que só tem o manifesto) sobre /app, forçava -w /app e montava data//logs/ ali. Numa imagem pronta com o código em /app, a app sumia e o deploy saía verde — o healthcheck TCP passa com a porta aberta. Reproduzido no tester: / respondia 404 e o container servia o README.md do repositório. Novo image_mode: prebuilt, que a síntese passa a emitir: não é build, não monta nada do runner em /app, respeita o WORKDIR da imagem; só entram os volumes: declarados. Exige image: e recusa build:. Manifestos existentes não mudam (o default continua bind-mount).

[2.131.0] - 2026-09-26

Added

  • adopt --dry-run manda captured.image_default_keys (#154): os NOMES das variáveis descartadas por serem iguais ao default da imagem (PATH, NODE_VERSION…). Quem lê o dry-run passa a distinguir o que segue na imagem do que se perdeu. Só nomes, nunca valores.

Changed

  • adopt leva o healthcheck do container para o manifesto (#155). Antes a síntese gravava sempre healthcheck: path: /, e num banco adotado a sonda HTTP falhava e o deploy revertia. Config.Healthcheck com CMD vira cmd: com o mesmo argv; CMD-SHELL vira ["sh", "-c", "<string>"]; interval, timeout e retries são copiados. Sem healthcheck no container, o default por tipo do gerador (HTTP para nginx/caddy/httpd/traefik, TCP para o resto).
  • Validação estrita do .deploy.yml — malformado falha (#156). Recusa, com campo e motivo, todos de uma vez: healthcheck.mode desconhecido (skip sugere skip_check), modo sem o campo que exige, modo em conflito com a cadeia cmd > tcp > path, campo desconhecido dentro de healthcheck, volume fora da forma (partes demais, destino relativo, modo fora de ro|rw, nome inválido, volume: sem destino) e destino duplicado. Atenção ao atualizar: manifesto que hoje passa com campo desconhecido no healthcheck deixa de passar. Em produção (levantamento de 26/09, 88 apps) só deployally-front/-sys tinham healthcheck.type, já removido das dist.
  • runner validate reprova o manifesto que o deploy recusaria — antes pulava calado as checagens quando o parse falhava.
  • O gerador por compose não emite mais port: dentro de healthcheck (nunca foi campo; seria recusado pela validação estrita).

Fixed

  • external_tcp sem tcp: não entra mais em pânico no deploy (recusado na leitura; se chegar ao resolve_mode, a sonda falha fechada).
  • Manifesto da raiz que não parseia é restaurado do pull/ também sem --force. Com o parser mais estrito, a cópia canônica da raiz (só reconstruída em deploy de versão nova) passava a ser recusada com o repositório já corrigido, e o cron travaria a app até um fetch --force à mão.

[2.130.0] - 2026-09-25 — `--from-compose` sem `--ckey` é v2, com identity única por bundle

Alterado

  • runner add --from-compose sem --ckey agora usa v2 (assimétrica), com uma identity para o bundle inteiro. Era o único caminho de registro que ainda caía em ckey simétrica gerada — o add simples é v2 por padrão desde a v2.124.0. A identity é gerada uma vez e repassada a cada serviço (shared_identity no add_app): continua sendo uma unidade de custódia e de recuperação (runner ckeys export <qualquer-app> devolve o bundle todo), como a ckey compartilhada era no v1. Compartilhar a identity não abre nada a mais: o envelope de cada valor é amarrado a `repositório + app
    • variável, então ciphertext de um serviço não abre em outro. --ckey

    ou--ckey v1` seguem dando simétrica compartilhada, como antes.

[2.129.1] - 2026-09-25 — suíte não pendura mais; DOMAIN resolvido no list/status (#152, #153)

Corrigido

  • Suíte de testes pendurava em wizard::journal_tests (runner-client#152). O ramo sem TTY de read_line_escapable lê o stdin do processo; sob cargo test rodado em background o stdin é um socket que nunca fecha, e o read_line ficava esperando para sempre (8h em 24/09; ninguém sem o contorno --skip wizard::journal rodava a suíte). Em teste a entrada agora é roteirizada (line_edit::scripted) e o stdin real não é tocado; fora de teste nada muda.
  • runner list / runner status mostravam {{::VAR}} literal na coluna DOMAIN (runner-client#153) mesmo com o roteamento certo — parecia defeito de rota e não era. A coluna passa a exibir o valor resolvido pelo .env/.secrets do app (InstanceConfig::display_domain), caindo no literal só quando não há valor.

[2.129.0] - 2026-09-25 — `edit --token-stdin`; bundle de secrets por instancia (#149, #150)

Adicionado

  • runner edit --token-stdin e --allow-broad-token (runner-client#149, itens 1 e 3). Trocar o token de uma app já registrada — o caso de toda rotação — obrigava a passar o segredo em argv (visível no ps e no history), e o edit aceitava token de escopo largo sem a guarda que o add impõe desde o #82: era a porta dos fundos do guarda. Agora o edit lê do STDIN como o add (printf %s "$TOKEN" | runner edit <app> --token-stdin) e valida os scopes ANTES de gravar, recusando token com admin:org/workflow/... sem --allow-broad-token.

Alterado

  • Bundle de secrets: escrita e commit sempre por instância (runner-client#150 e #149 item 2). O flat .runner/secrets.enc era o mesmo arquivo em toda branch: staging (deploy de dev) e prod (deploy de main) reescreviam o mesmo binário cifrado e todo merge de release conflitava — sem diff legível, e resolver pelo lado da dev punha segredo de staging em produção em silêncio. Agora a fase 1.6, a fase 8 e o secrets push gravam/commitam secrets.<instância>.enc (write_enc_filename); o flat que já está no repo fica como está e ninguém mais o reescreve, então o merge não tem dois lados. App v1 que só tem o flat: o primeiro push o promove (cópia byte a byte) para o nome da instância. A leitura já preferia per-instance → flat, então nada muda para quem lê.
  • runner secrets set/generate/delete/... (v1) gravam no bundle da instância, não mais no flat — a instância ativa lia secrets.staging.enc e o set escrevia em secrets.enc, e o valor novo nunca chegava ao container (incidente do sysadmin-backend: backend healthy com senha de banco recusada).
  • runner secrets migrate aposenta o .secrets plano em .secrets.pre-v2-<ts> (não apaga). O v2 não o lê, mas ele sobrava no disco com valores velhos e confundia env show e quem depurava — o operador acabava movendo os arquivos à mão.

[2.128.0] - 2026-09-24 — `middlewares:` por rota em `routes[]`

Adicionado

  • middlewares: por rota em instances.<x>.routes[]. Ate aqui routing.middlewares valia para todos os routers da instancia — proteger um dominio (ex.: allowlist de IP no admin.relay.*) trancava os outros, e a saida era router escrito a mao no file provider, fora do manifesto. Agora a rota declara os seus: nao vazio substitui o de routing: naquele router, ausente herda (mesma regra de substituicao de routing: por instancia — sem soma). Vale nos tres modos (file, docker_labels, external) e no single-route. runner preflight passa a conferir refs @file declarados em rota. Manifesto sem o campo gera labels/YAML identicos. Ver routes[].middlewares.

[2.127.0] - 2026-09-24

Corrigido

  • runner adopt transformava volume Docker nomeado em pasta vazia (runner-client#151). A síntese copiava HostConfig.Binds como estava, e pgdata:/var/lib/postgresql/data caía no deploy como caminho relativo ao app: /data/apps/<app>/pgdata, uma pasta nova e vazia. O app adotado subia saudável e sem os dados. Nenhum runner adopt havia rodado em produção.

Adicionado

  • Forma explícita de volume nomeado no .deploy.yml: volume:<nome>:<destino>[:modo]. Monta o volume Docker pelo nome (o mesmo que o container original usava). Nome sem barra continua sendo pasta relativa ao app: manifestos de produção usam isso de propósito (Caddyfile:…, uploads:…).
  • runner adopt lê Mounts, que traz o tipo de cada montagem:
    • volume nomeado ou anônimo vira a forma explícita (o anônimo antes era descartado);
    • bind mantém o :ro;
    • tmpfs e outros tipos geram aviso (montagem_nao_copiada) em vez de serem copiados em silêncio.
  • Validator: volume: com nome inválido para o Docker ou com destino não absoluto dá o erro volume_nomeado_invalido.

Prova no tester, com Postgres real, volume nomeado e uma linha gravada: o adopt --dry-run desta versão emite volume:adopt151_pgdata:/var/lib/postgresql/data, enquanto o 2.126.0 emitia adopt151_pgdata:/var/lib/postgresql/data. O deploy real, que monta o volume no container novo, está coberto por teste unitário e não foi exercido ponta a ponta.

[2.126.0] - 2026-09-24

Alterado

  • Deploy concorrente: o lock ocupado agora se explica (runner-client#148, item 2a — decisão do dono em 23/09: "se um deploy já estiver rodando, o runner retorna lock e avisa que já temos um deploy em execução"). O abort continua (try_acquire), mas a resposta diz o que aconteceu, pra humano e pra máquina: modo texto imprime já existe um deploy em execução para `<app>`; aguarde terminar ou veja `runner status <app>` (com a causa — arquivo/PID do lock — logo abaixo); -o json leva a mesma frase em data.after.error, o código estável lock_contended em data.after.error_code (campo novo, plano) e em data.after.diagnosis.code (o que o CCS já lê). Exit code segue 1 — o projeto não tem convenção de código distinto por classe de falha.
  • Fase 8 sem token de repositório: rejeita, não tolera (runner-client#148, item 2b — decisão do dono: "se ele não tiver um token válido não pode ter fallback; vai rejeitar o commit até que um novo token seja registrado, de preferência um PAT. Nada de usar global — global não existe"). Antes, sem github_token no state, a fase 8 comitava localmente, não pushava e avisava em warn!; desde 2.125.1 o estado registrava o HEAD de antes, mas o bundle seguia sem ir pro repo, em silêncio. Agora, com mudança de secret pra publicar e sem token, não nasce commit nenhum (nem local; o stage é desfeito) e a execução termina em erro explícito: sem token de repositório para `<app>`: os secrets cifrados não foram commitados. Registre um PAT com `printf %s "$TOKEN" | runner tokens set <app>` . O deploy não é desfeito — o container está no ar e o estado é gravado com a versão nova; o que falha é a fase de secrets: envelope success:false, data.after.status:"failed", error_code/diagnosis.code = secrets_push_no_token, diagnosis.phase = secrets_push, exit 1. O histórico (deploy-events) registra o deploy como sucesso com o erro no campo error. Nenhum fallback pra outra credencial.
  • Token registrado mas recusado pelo remoto — mesmo tratamento, com a classificação que o runner tokens set já usa: antes de comitar, a fase 8 sonda o remoto (git ls-remote com o token); Authentication failed / Invalid username or token (HTTP 401/403) é token_recusado, Repository not found (404) é repo_invisivel — os dois viram secrets_push_token_rejected, sem commit. Falha de rede/DNS não é token: segue no commit + push best-effort de sempre (estado registra o HEAD de antes se o push não sair).
  • runner secrets push (manual) segue a mesma regra: sem token válido o comando falha com a mensagem acima em vez de "commitado localmente (sem token GitHub — não pushado)".

Para o CCS

  • Front precisa exibir lock_contended (já pinta em warning) com a frase de data.after.error, e os dois códigos novos secrets_push_no_token / secrets_push_token_rejected (data.after.diagnosis traz título, explicação e sugestões; data.after.version é a versão que SUBIU).

[2.125.1] - 2026-09-23

Corrigido

  • Deploy pela API seguido de redeploy da mesma versão pelo cron ~1 min depois (runner-client#148). O relato era "dois deploys ao mesmo tempo"; a evidência de produção (630 deploys desde 08/09, zero sobreposições) mostrou que era sequencial. Causa: a fase 8 commita chore: update encrypted secrets e, em app sem github_token no state (21 de 86 em produção), não consegue pushar — mas o estado gravava esse commit local-only em local_commit, e o cron via remoto ≠ local e deployava de novo. Agora, sem push, o estado registra o HEAD de antes da fase 8 — o que o remoto de fato tem.
  • Chave do lock por app com fonte única (AppLock::lock_key), usada pelo deploy e pelo fetch, com teste de igualdade como guarda. As duas derivações antigas (nome do diretório × result_app_name cortado no @) davam o mesmo arquivo na prática, mas nada garantia isso.

Pendente de decisão

  • try_acquire (deploy pela interface aborta com lock_contended) × acquire (cron espera): operador que clica durante um ciclo do cron leva erro em vez de fila. É uma linha, mas é escolha de comportamento.
  • Apps sem github_token: o bundle de secrets cifrados continua não indo ao repo (família do #120). Fallback para token global ou aviso visível no CCS.

[2.125.0] - 2026-09-23

Adicionado

  • runner tokens set <app> — troca o token da app sem re-registrar. Antes a única saída era runner add --token, o que o CCS não consegue fazer pela tela: o operador ficava preso no push_no_token (ccs-front#91).
    • Token vem do STDIN, nunca de argv (que vaza em ps, histórico e logs). Nada imprime o valor — só a impressão digital.
    • Verifica antes de gravar, no REPO (permissions.push), não só no GET /user: token sem push passa no /user e só falha depois, no secrets push, longe de quem trocou a chave. Recusa distingue 401/403 ("credencial não vale") de 404 ("vale, mas não enxerga o repo"), e 200 sem permissions.push também recusa — ausência de dado não é permissão.
    • Em caso de recusa o token ANTERIOR fica intacto.
    • --skip-verify para rede fechada/Enterprise, explícito.

[2.124.0] - 2026-09-22

Adicionado

  • env status passa a informar o esquema de secrets da app (scheme): id, rótulo legível, legado e a EVIDÊNCIA de onde a conclusão saiu. O CCS consome isso na aba Env do deploy — antes o operador inferia pelo nome de um arquivo, e inferia errado com facilidade: o arquivo do esquema NOVO tem cabeçalho runner-secrets:v1 e o esquema novo é o v2 (runner-client#147).
  • Apelidos legíveis no --ckey: c/sym = simétrica, a/asym = assimétrica. Comparação exata — a é apelido, abc é ckey literal.

Alterado

  • --ckey sem valor agora gera v2 (assimétrica), era v1. Quem cai no padrão sem escolher passa a cair no mais seguro: a privada é do servidor por construção, alterar um secret não reabre os demais e o diff fica auditável. O v1 não foi removido nem depreciado: app existente continua igual (a detecção é por conteúdo do arquivo), --ckey v1/literal seguem dando simétrica, e a conversão silenciosa continua barrada por scheme_conflict (runner-client#147).

Corrigido

  • domain: "{{::VAR}}" ia CRU para o label do Traefik em traefik.mode=docker_labels: Host({{::VAR}}) nunca casa, e o deploy terminava ✓ DEPLOY CONCLUÍDO, health healthy e 404 no domínio. A v2.30.13 corrigiu Local/External e deixou este modo — que é o único que funciona com Traefik em rede do host (runner-client#146).
  • Placeholder sem valor agora aborta o deploy dizendo qual domínio não resolveu, em vez de subir sem rota em silêncio. App publicada por porta não é afetada.
  • env set recusa chave fora de [A-Za-z_][A-Za-z0-9_]*. O rótulo legível de um placeholder É a chave lida de volta; o operador respondia o rótulo, o runner gravava, e o deploy morria no docker run (invalid env file: ... contains whitespaces) apontando um log que não existia — o container nunca chegou a ser criado.
  • O aviso de DNS do add respeita --instance e não consulta DNS de placeholder. O log de produção mostrava DNS check for {{::Distributor domain (production)}}: not resolved yet.

[2.123.0] - 2026-09-17

Adicionado

  • Wizard: detecção de porta e health path pela crate port-identify v0.1.0 (git dep pinada na tag, como o traefik-manager) — runner-client#145, fase B. O prompt de porta virou um select montado a partir das candidatas detectadas (código, Dockerfile, compose, Procfile, .env, config de runtime), com "Outra…" (entrada livre 1–65535) sempre presente, inclusive em confiança máxima. Candidatas published-host (porta do host) e dependency (banco, redis) não entram na lista; o alvo do Traefik vem de routing.container_port, nunca do published_port. Só port_status == resolved pré-seleciona — em ambiguous/unknown a lista aparece sem endosso e o cursor fica em "Outra…". Mesmo padrão para o health path ("Outro…"). Avisos da crate (ambiguous-port, expose-conflicts-runtime, behind-http-frontend, health-on-management-port, …) viram linha curta ao lado do prompt.
  • runner detect passou a publicar port_suggestion (o que o wizard oferece) e port_detection (saída crua do port-identify). Os campos históricos do relatório continuam no mesmo nível do JSON — o WASM do CCS não vê diferença.
  • Adapter de filesystem com tetos de entrada (src/wizard/port_scan.rs): arquivo acima de 1 MiB é pulado, no máximo 200 arquivos de código (também aplicado em ScanOptions::max_source_files), 2000 arquivos e 32 MiB de snapshot. A crate não tem teto próprio; sem isso um repo hostil ou um bundle minificado encheria a memória do Runner.

Notas

  • answers.port / --answers continua com precedência absoluta sobre qualquer detecção (idem healthcheck_path). Portas que o pipeline já conhece (service do compose escolhido pelo operador) entram como env_overrides da crate, de maior precedência.
  • Limites conhecidos da v0.1.0 da crate, não contornados aqui: sem Java/Spring, .NET e Ruby/Rails; não classifica stack pela imagem base do Dockerfile.

[2.122.0] - 2026-09-17

Corrigido

  • fetch --deploy (tls-watch, #144): a sonda de certificado não faz mais uma requisição HTTP contra o app. Era um curl -k -v https://<dominio>/, ou seja um GET / real: o app respondia 401 e cada ciclo do cron entrava no ledger de autenticação dele (288 rejeições/dia) e no access log do Traefik. Agora é só o handshake TLS (rustls, SNI = domínio, conexão para 127.0.0.1:443), lendo o certificado apresentado e fechando — nenhum byte de HTTP é enviado. Como não há mais status HTTP, não existe mais 401/403 para interpretar.
  • fetch --deploy (tls-watch, #144): a passada olha SÓ o que o runner administra. Route file de terceiro (router escrito à mão pelo operador no mesmo dynamic dir — o caso do relatório, um domínio legado mantido vivo durante a migração) é ignorado, e domínio que não pertence a nenhuma instância ATUAL de app registrado também. Antes o runner sondava o nome antigo para sempre, poluindo a análise de quem ainda usava aquele domínio.

Adicionado

  • Route files escritos pelo runner recebem o marcador # managed-by: runner (formato ManagedMarker do traefik-manager, preservado pelos applies seguintes) — é a prova de propriedade do filtro acima. Instalações anteriores não regridem: o nome determinístico …_{project}__{instance}.yml de uma instância registrada continua valendo como prova.
  • fetch --deploy imprime UMA linha do tls-watch (domínios conferidos, no certificado padrão, retentativas, arquivos de terceiros ignorados, domínios fora das instâncias atuais). A checagem era invisível na saída do fetch.

[2.121.10] - 2026-09-16

Corrigido

  • env status (campo manifest): lê o .deploy.yml do clone do repositório (pull/) antes da cópia de /data/apps. A recuperação é pelo repositório, e um commit do .deploy.yml sem mudança de versão (ex.: env gravada pelo CCS em instances.<inst>.environment) só chegava à cópia local no próximo deploy — o marco ficava velho.

[2.121.9] - 2026-09-16

Adicionado

  • env status (trilho): campo manifest por chave — o que o .deploy.yml (environment/secrets + override da instância) recupera se o server for recriado: absent, same, differs, declared, generative ({RANDOM}/${GENERATE}: recriar gera OUTRO valor) ou prompt ({{::…}}/ref). Compara só o valor das camadas .env com o literal; nada de valor sai.

Corrigido

  • Esquema v2: env nova declarada no environment: do .deploy.yml depois do registro nunca chegava ao container — o v2 pulava a fase 1.5 legada inteira. Agora o deploy acrescenta ao .env as chaves do manifesto que não existem em camada nenhuma nem no arquivo de secrets. Nunca sobrescreve; nome de secret não vai em texto (aviso); placeholder vazio é descartado.

[2.121.8] - 2026-09-16

Corrigido

  • Trilho da credencial (env status), esquema v2: env comum saía com bundle e repo verdes, porque o registro do push lista todos os nomes do plaintext. No v2 o arquivo runner-secrets:v1 só guarda secrets — o valor de env comum mora só nas camadas .env do server. Agora essas chaves saem com bundle: false, repo: false e o campo novo scope: "server" (secrets e todo o esquema legado: scope: "bundle"). Consumidor deve tratar scope: "server" como "não se aplica", não como "faltando" (CCS front ≥ 2.197.7).

[2.121.7] - 2026-09-16

Corrigido

  • Deploy que falhava antes do container (gate de required, secrets, scan, build/pull da imagem) deixava o symlink current apontando para a versão que nunca subiu. O #86 só devolvia o current em falha de start/healthcheck. Com o current numa versão fantasma, o trilho do env status procurava o container dessa versão, devolvia container: null e a aba Env do CCS marcava TODAS as chaves como "não no container" (um cliente em staging, 16/09: current em 8be4f76, container no ar em 60868fb). Agora qualquer falha entre o prepare e o container devolve o current para a versão anterior.

[2.121.6] - 2026-09-16 — esquema v2: `required:` confere o env mesclado, não só o arquivo de secrets

Corrigido

  • No v2, o único gate de required: (fase 1.5) conferia só o arquivo de secrets. Desde a 2.121.4, env comum vai pro .env da instância — então uma variável required que virou env comum era dada como ausente e o deploy abortava com ela no disco (PROVIDER_STAGING_ENVIRONMENT_ID, um cliente em staging). Agora confere o env MESCLADO que o container recebe (.env + .env.<instância> + secrets decifrados), via v2_missing_required.
  • O gate v2 passa a respeitar --allow-missing-required, como o legado. Antes o bypass não tinha efeito em app v2.

[2.121.5] - 2026-09-15 — esquema v2 com `target:`: o arquivo de secrets volta a ser encontrado

Corrigido

  • Em servidor com target: no config.yml, o arquivo v2 da instância só era procurado pelo nome de ESCRITA (secrets.<inst>.<target>.enc, #50). App v2 cujo arquivo é secrets.<inst>.enc caía em "não existe": is_v1 respondia false e tudo voltava ao legado — chaves v2 fora do env status, env set no lugar errado, marco bundle vazio e, na fase 1.5 do deploy, secrets NÃO materializados no runtime.env. Agora store_v1::path_for resolve pela mesma cadeia do deploy (bundle: explícito → per-target → per-instância → legado), aceitando só candidato com o header # runner-secrets:v1; sem nenhum, segue no nome de escrita (app nova). Achado no ccs-goal1-dummy (tester) ao validar a 2.121.4.
  • Com isso a guarda bundle_target_mismatch do #126 (família secrets *) deixa de ser alcançável: leitura e escrita convergem por construção. O bail fica como rede de segurança e o teste passou a afirmar a convergência — bundle v2 no nome legado é lido E escrito no mesmo arquivo.
  • secrets status: a linha bundle (escrita) mostra o alvo REAL (no v2, o resolvido por store_v1::path_for) e o read_write_mismatch parou de ser acusado em app v2 coerente — era ele que mandava "alinhar o bundle ao target" um app que já estava alinhado. No v1 a detecção continua, com teste próprio.

[2.121.4] - 2026-09-15 — esquema v2: `env set` respeita o tipo (env × secret)

Corrigido

  • Numa app v2, todo env set gravava no arquivo cifrado de secrets, com ou sem --secret: env comum virava secret, trocar secret→env pela aba Env não tinha efeito e o --if-changed respondia "igual" (um cliente em staging). Agora, sem --secret, o valor é plano: vai pro .env da camada (--instance/ativa/--shared, como no legado) e sai do arquivo de secrets se estava lá. --if-changed só responde "igual" quando a chave não está no arquivo de secrets e a camada já tem o mesmo valor. Com --secret, nada muda.

[2.121.3] - 2026-09-15 — trilho: marco `container` enxerga o redeploy de mesma versão

Corrigido

  • env status: o marco container procurava só o nome canônico (<projeto>_<instância>_<versão>). O redeploy de mesma versão (#127) sobe <nome>__rN e remove o canônico depois do health — o trilho dizia "sem container" com a app no ar (um cliente em staging após deploy --force). Agora escolhe, entre os containers em execução, o canônico ou a geração mais alta.

[2.121.2] - 2026-09-15 — trilho v2: dedup do `.env` não apaga o bundle nem acusa repositório desatualizado

Corrigido

  • Esquema v2: o marco bundle considera só o arquivo de secrets da instância (que É o bundle). Deduplicar/editar o .env deixava bundle vazio em todas as chaves, porque a checagem comparava o mtime com o .env.
  • Esquema v2: hash de drift = só o arquivo de secrets. .env e .secrets não vão pro repositório no v2; com eles no hash o dedup acusava "o bundle de secrets no repositório não é o deste server" e o push respondia "nada a comitar". App legada: hash inalterado. App v2: o primeiro env status depois do upgrade pode marcar drift uma vez — o próximo "Comitar todos" regrava o hash.

[2.121.1] - 2026-09-15 — trilho: marco `repo` das chaves v2 com registro de push antigo

Corrigido

  • Chave v2 presente no repositório aparecia com repo vazio (e no aviso "só no server") quando o último push com commit foi feito por runner < 2.121.0, cujo secrets.keys não listava chaves v2. "Comitar todos" respondia "nada a comitar" e o marco não mudava (um cliente em staging).
    • env status: arquivo v2 local byte-idêntico ao commitado no clone conta as chaves dele como presentes no repo.
    • secrets push sem diff regrava secrets.commit/secrets.keys com o HEAD do clone e as chaves atuais.

[2.121.0] - 2026-09-15 — esquema v2 na aba Env: chaves visíveis, `--instance` respeitado, conflito no trilho

Corrigido

  • env set --secret --instance <inst> numa app v2 grava no arquivo runner-secrets:v1 da instância PEDIDA. O ramo v2 usava sempre a instância do state — o CCS pedia staging e o valor ia para o arquivo da ativa (mesma classe do incidente de um cliente). Sem --instance (ou com --shared), segue na ativa: não existe arquivo v2 compartilhado.
  • env status -o json lista as chaves do esquema v2. Lia só .env/.secrets, então nada gravado por env set numa app v2 aparecia no trilho (caso um cliente em staging, 15/09 — ccs-front#79).
  • Hash de drift e secrets.keys do push incluem o arquivo v2 da instância: secret v2 novo ou trocado passa a marcar pendência. App legada: hash byte-idêntico ao anterior.

Adicionado

  • env status → v2_secrets { exists, count, keys } (só nomes, nada decifrado).
  • env status → keys[].conflict: a chave está no .env (camadas) e também como secret; o secret vence em runtime.

[2.120.7] - 2026-09-12 — cron: guard #98 enxerga a instância de uma duplicata (`--slug`)

Corrigido

  • fetch --deploy não pula mais uma app registrada com --force-duplicate --slug cuja instância já roda sob ela (#143). O guard #98 procurava as instâncias materializadas pelo project: do manifesto, mas os containers são rotulados com o projeto efetivo (o slug). Duas apps do mesmo repo no mesmo droplet — caso real: makemail-front (dev) e makemail-front-stable (stable) — faziam a duplicata enxergar só o dev da original, cair em "troca de instância" e ficar em needs_update a cada ciclo, com o próprio container rodando. O guard agora usa effective_project, a mesma regra do unregister (#123). A proteção original do #98 fica igual: instância que nunca rodou sob ESTE registro, com outra rodando, continua bloqueada no cron.

[2.120.6] - 2026-09-11 — trilho: `bundle.remote_check` (upstream, HEAD local, ls-remote cru) e cache invalidado no push

Adicionado

  • env status -o json → bundle.remote_check { upstream, local_head, ls_remote, cached }: de onde veio remote_commit. Caso de um cliente: push "persistido (60868fb)" e o ls-remote da dev seguia em 49e3e04 — sem isso não dá pra saber se o push foi para outra branch ou se o remoto respondeu outra coisa.

Corrigido

  • Push que passa apaga .runner/remote.cache: o env status logo depois do "Comitar todos" não lê mais o HEAD remoto de até 60 s atrás.

[2.120.5] - 2026-09-11 — trilho: repo com bundle diferente é `in_sync: false`, não "sem confirmação"

Corrigido

  • Quando o bundle commitado no repositório tem conteúdo comprovadamente diferente do bundle do server (repo_check: plaintext-differs), in_sync passa a ser false — o CCS mostra "repositório desatualizado → Comitar todos" em vez de "sem confirmação". Falha de decifra vira decrypt-failed e continua null. Caso real: um cliente em staging, o push do dev (rebase) levou um bundle antigo por cima do que o runner tinha pushado.

[2.120.4] - 2026-09-11 — trilho: `bundle.repo_check` diz por que o repo não foi confirmado

Adicionado

  • env status -o json → bundle.repo_check: record, bytes-equal, plaintext-equal (confirmado) ou o motivo de não afirmar — committed-missing:<path>, plaintext-differs, no-key, local-missing. Sem isso, "sem confirmação do repositório" era opaco (um cliente, 11/09).

[2.120.3] - 2026-09-11 — trilho: fallback do `repo` também no esquema v2

Corrigido

  • O fallback por plaintext (2.120.2) só existia para ckey v1; app no esquema v2 (identity assimétrica, # runner-secrets:v1) — caso de um cliente — seguia "sem confirmação". Agora BundleKey::V2: parse dos dois arquivos, chaves com o mesmo ciphertext contam iguais, o resto é unseal + comparação de valor; identity errada ou falha de decifra não afirmam nada.

[2.120.2] - 2026-09-11 — trilho: fallback do `repo` compara PLAINTEXT, não bytes

Corrigido

  • O fallback da 2.120.1 comparava bytes do bundle local com o commitado no clone — e a fase 1.6 do deploy re-cifra o bundle local com nonce novo depois de cada push, então nunca batia (o cliente seguia "sem confirmação"). Agora decifra os dois com a ckey (v1) e compara o plaintext, como a fase 1.4 faz desde o v2.30.8. Esquema v2 (identity) continua sem fallback: in_sync: null.

[2.120.1] - 2026-09-11 — trilho: bundle pushado por runner antigo conta como `repo`

Corrigido

  • env status dizia last_push_commit: null (e o CCS concluía "N chaves só no server") em app cujo último push foi feito por runner < 2.120.0, antes de existir .runner/secrets.commit. Agora, sem registro, compara o bundle local com o commitado no clone pull/: byte-idêntico → o repo tem este bundle (last_push_commit = HEAD do clone, chaves = plaintext atual). Visto na um cliente em staging com o 2.120.0 recém-instalado.

[2.120.0] - 2026-09-11 — `env status` com o trilho da credencial (bundle · repo · container)

Adicionado

  • runner env status <app> -i <inst> -o json ganha, aditivamente, bundle (file, hash, last_push_commit, remote_commit, in_sync, fresh), branch, container (name, version, started_at) e keys[] (key, secret, server, bundle, repo, container) — os quatro marcos de cada chave, só por NOME, nunca valor. repo só é verdadeiro quando o último push do bundle é o HEAD da branch na origin (ls-remote com timeout de 3 s e cache de 60 s em .runner/remote.cache) e o plaintext não mudou desde então; sem confirmação do remoto, in_sync: null. container lê o Config.Env do container current da instância (nomes).
  • secrets push -o json devolve commit (hash curto) e branch; o push que passa grava .runner/secrets.commit e .runner/secrets.keys (nomes). Plano: /docs/proj/CCS/CICD_SECRET_LIFECYCLE_PLAN.md (F1).

[2.119.0] - 2026-09-11 — push de secrets com remoto à frente: fetch + rebase antes de desistir

Corrigido

  • secrets push (manual e fase 8 do deploy) desistia no primeiro ! [rejected] … (fetch first) — bastava alguém ter pushado no repo depois do último fetch do clone pull/. O bundle ficava só local, e o deploy seguinte (git reset --hard origin/<branch> + fase 1.4, em que o repo é soberano) sobrescrevia o .secrets do servidor com o bundle VELHO do repo: envs recém-adicionadas perdidas sem aviso (um cliente em staging, 11/09). Agora push_with_rebase: rejeitado por non-fast-forward → fetch origin <branch> + rebase origin/<branch> + push; rebase em conflito no próprio bundle (outro servidor ou o operador também pushou secrets) → reset --hard pro remoto, regrava o bundle atual deste servidor, commit, push — o plaintext local que o operador acabou de editar é a autoridade. Outras falhas (auth, rede, HEAD destacada) seguem como antes: warn e commit local preservado.

[2.118.0] - 2026-09-10 — `notify.watch_alerts`: liga/desliga os alertas dos watchers

Adicionado

  • notify.watch_alerts (default true): classe própria pros alertas do cron — tls-watch, doctor-watch e auto-pause/retomada do fetch. false silencia o chat e mantém o audit log. Independe de notify.enabled (que é só deploy). runner settings set notify.watch_alerts false.
  • Documentados watch_realert_hours (intervalo entre re-alertas do mesmo problema, default 6 h) e o funcionamento do tls-watch em Notificações.

Corrigido

  • tls-watch: domínio que saiu dos route files (app desregistrada) sai do estado em vez de ficar pra sempre.

[2.117.1] - 2026-09-10 — alerta dos watchers chegava sem o texto

Corrigido

  • O corpo dos alertas (campo details) nunca era renderizado no Telegram/Discord: chegava só "🩺 Alerta de saúde do runner / Deploy: runner / Instance: tls-watch". Agora sai como parágrafo, com _ * [ escapados pro Markdown do Telegram. Linha de versão omitida quando vazia.

[2.117.0] - 2026-09-10 — certificado padrão do Traefik detectado e ACME retentado

Adicionado

  • Probe TLS no deploy: depois de verificar as rotas, o runner sonda o issuer de cada domínio; TRAEFIK DEFAULT CERT vira aviso dizendo se o DNS não resolve ou se o Traefik só não retentou.
  • tls-watch ao fim de todo runner fetch --deploy: DNS ausente → alerta (não retenta); DNS ok mas cert padrão → reescreve o route file (# tls-retry: <ts>) pra forçar o reload e o ACME, mínimo 60 min entre tentativas; certificado emitido → "✅ Certificado ACME emitido". Estado em <state_dir>/.tls-watch-state.json, re-alerta a cada watch_realert_hours.

[2.85.0] - 2026-08-21 — `.env` por instancia + barreira de procedencia no `required:`

Adicionado

  • Env em camadas por instancia. O env efetivo passou a ser .env + .env.<instancia>, a instancia vencendo. A base guarda o comum (TZ, LOG_LEVEL); a camada da instancia guarda o que nao pode ser comum (endpoint de banco, bucket, chave de API). App com so .env funciona igual.
  • Barreira de procedencia no required:. Chave obrigatoria tem que resolver de fonte escopada por instancia. Resolver so da base compartilhada avisa no cron e bloqueia no interativo/switch-instance. Previne o caso do DATABASE_URL de staging satisfazer o required: de production. Escape pontual: RUNNER_ALLOW_SHARED_REQUIRED=1.
  • runner env set --shared / --instance. env set escreve na instancia ativa por padrao; --shared compartilha na base (deliberado); --instance <nome> mira outra. env status mostra as camadas resolvidas e a origem de cada chave.

Limitacao conhecida

  • O bundle cifrado (.runner/secrets.enc) e app-level e nao custodia .env.<instancia> — esses valores nao sao restaurados por deploy --all num servidor novo. Reprovisionar no disaster recovery (fase 2).

[2.84.0] - 2026-08-21 — `runner git push`

Adicionado

  • runner git push <app> [branch]. Caminho suportado para push manual do clone pull/ de um app. O clone nao persiste o token (fica fora do .git/config e do ps aux), entao um git push a mao falhava com could not read Password. O comando injeta o token so na invocacao, sem persistir. Sem branch, usa a branch corrente do clone.

[2.83.0] - 2026-08-20 — bloqueio da troca silenciosa de instancia

Corrigido

  • fetch --deploy nao materializa mais uma instancia nova por efeito de um commit. App sem instancia fixada, rodando staging, cujo manifesto ganhava outra instancia como default tinha essa outra materializada (container + rota novos) no proximo ciclo do cron. Agora o cron pula e avisa; autorize a troca com runner fetch --deploy --allow-new-instance ou fixe com runner edit <app> --instance <nome>. A deteccao e conservadora: primeiro deploy e recovery da mesma instancia seguem normais.

[2.82.0] - 2026-08-20 — aviso de selecao silenciosa de instancia

Adicionado

  • Aviso quando o app nao tem instancia fixada e o manifesto declara mais de uma. App registrado sem instancia deploya a primeira declarada; reordenar instances: mudava qual ambiente subia sem aviso. O runner passa a avisar em todos os caminhos de deploy — fixe com runner edit <app> --instance <nome>.

[2.81.0] - 2026-08-20 — healthcheck do manifesto chega ao Traefik

Corrigido

  • healthcheck.interval e healthcheck.timeout do manifesto agora atravessam ate o router do Traefik. Antes eram aplicados ao Docker e descartados no health check do router, que usava 10s/5s fixos. Declarar 30s para reduzir a frequencia de probe nao surtia efeito — num incidente real o router aplicou 10s e triplicou a taxa (134 MB/dia). Requer traefik-rs v0.3.0.
  • switch-instance validava contra o manifesto errado. Passa a aceitar a instancia declarada em qualquer um dos dois .deploy.yml (o do pull/ ou a copia do app) e roda a validacao antes de qualquer escrita.

[2.80.2] - 2026-08-20 — `switch-instance`, e tres travas de superficie

Adicionado — `runner switch-instance`

Move um app ja deployado para outra instancia. A instancia entra no NOME do container ({project}_{instance}_{versao}) e no arquivo de rota do Traefik, entao trocar so o state.instance nao renomeia nada: o proximo deploy sobe o segundo conjunto ao lado do primeiro, que continua servindo.

Ordem EDIT → DEPLOY → DESTROY, deliberada: a alternativa trocaria uma falha recuperavel por indisponibilidade. Deploy quebrado devolve o state ao original e a instancia antiga segue no ar. Sem --force, imprime o plano e nao altera nada.

Ver switch-instance.

Guarda no runner edit --instance: recusa a troca com container no ar, nomeando os containers e as duas saidas.

Seguranca — token do bot vazava no log (#96)

http_webhook logava a URL completa, e canais cuja credencial vive no PATH escreviam o segredo em texto claro — o Telegram usa .../bot<TOKEN>/sendMessage, uma linha por notificacao. Medido em producao: 47 linhas em 15 arquivos.

O caminho de falha vazava tambem: o erro do reqwest embute a URL na mensagem dele. Mascarar so no log de sucesso deixaria o segredo escapar justamente pelo caminho que mais aparece em log de incidente.

O token em uso deve ser considerado comprometido e rotacionado.

Adicionado — `test/` e `tests/` saem da copia de deploy (#95)

O driver nao e credencial, e superficie: codigo-fonte da suite dentro de um artefato de runtime, servido por static server ou assado na imagem pelo COPY . ..

docs/ fica deliberadamente de fora — existe type: docs, e app de documentacao serve docs/ legitimamente.

Corrigido — parcial: healthcheck do Traefik ignora o manifesto (#94)

healthcheck.interval/timeout valem para o Docker, mas o router usa 10s/5s fixos da crate. Meia correcao: o conserto real exige campo novo em traefik-rs. Ate la, o runner passa a avisar em vez de aceitar calado.

[2.80.0] - 2026-08-19 — `runner destroy` apagava a frota inteira (#97)

Entradas 2.70–2.79 ainda nao migradas para este changelog; ver CHANGELOG.md no repo.

Corrigido — CRITICO

  • runner destroy <app> removia containers de TODOS os projetos do servidor. ContainerFilter::to_docker_args usava project como flag booleana: testava is_some() e filtrava so por managed-by=runner, nunca pelo nome do projeto. Medido em producao, o filtro devolvia 79 containers de 73 projetos.

    A causa nao era codigo ausente — era codigo nao propagado: o refino de identidade existia e estava correto em list_containers_detailed, mas a funcao irma usada pelo destroy nao o tinha. Duas listagens respondendo diferente a mesma pergunta. A correcao e delegacao, para que a divergencia nao possa voltar.

    Verificado em producao: destroy nfse -i production listava 79 containers, passou a listar 1.

Adicionado — travas de raio

  • Trava no destroy: se um container da lista nao casar com {project}_{instance}_, aborta sem remover nada e nomeia o intruso.
  • Dry-run no destroy: sem --force, lista os containers e conta as rotas que seriam removidas. Antes so dizia "Use --force to confirm" — o operador confirmava as cegas.
  • Gate de amplitude no deploy --all e deploy-all: imprime as familias afetadas, confirma em terminal e exige --yes em cron. Ver deploy-all.
  • Gate de fronteira de projeto: uma query que casa alvos de projetos distintos agora aborta — --yes nao autoriza o cruzamento, de proposito. Vale para deploy, fetch, restart, cleanup, unregister e reconcile.

BREAKING

  • runner deploy-all passa a exigir -y, --yes em contexto nao-interativo (cron/CI). Scripts existentes precisam da flag. Ver deploy-all.

[2.69.0] - 2026-08-06 — Falha de deploy diz onde procurar o bundle; esquema de secrets divergente para o deploy

Adicionado

  • Reconciliação de esquema de secrets antes do deploy. O state do servidor e o arquivo de bundle respondiam separadamente "qual esquema é este", e nada checava se concordavam. Com o state declarando ckey assimétrica e o bundle no disco em formato simétrico, o deploy subia verde, com o container sem nenhum secret — sem erro, sem aviso, sem log.

    Agora as duas fontes são cruzadas. Divergência interrompe o deploy, citando o que cada fonte declara, o caminho do arquivo inspecionado e como reconciliar. Ver ckey simétrica e assimétrica.

  • environment: aceito na instância como sinônimo de environment_overrides:. Era a grafia que operadores escreviam e o runner ignorava em silêncio.

Corrigido

  • Falha por secret obrigatório passa a dizer ONDE o bundle foi procurado. A mensagem informava que faltava valor, mas não se o runner deixara de encontrar um bundle que o operador acredita existir — falhas diferentes com o mesmo sintoma. Agora lista os caminhos candidatos na ordem de resolução, com a origem de cada um, e marca quais existem no disco.

    O cabeçalho reflete o que foi verificado: havendo bundle, aponta o conteúdo; não havendo, diz que nenhum foi encontrado. Ver Secrets não resolvidos.

  • Seis classes de erro deixaram de cair em texto cru (várias em inglês): falha ao decifrar bundle, repositório inacessível, version.field inexistente, repositório sem tags, branch inexistente e instância inexistente — esta passando a listar as instâncias declaradas.

Deprecado

  • source: {type: dist} — não usar em deploys novos; prefira type: branch + branch: dist. Nada muda de comportamento e não há data de remoção. Ver Recursos deprecados.

[2.68.0] - 2026-08-05 — Labels CCS obrigatorias no deploy; campo `labels:` funciona

Adicionado

  • Labels de identificacao CCS em todo container. O runner passa a emitir ccs.systems/type (derivada de system:), ccs.systems/env (derivada da instancia) e ccs.systems/uid no caminho de deploy principal — antes so o fluxo de staging as aplicava.

  • Campo labels: no .deploy.yml, repassado ao container. Ate a v2.66.0 era ignorado em silencio; na v2.67.0 passou a avisar; agora funciona.

    Quatro labels sao reservadas e nao podem ser sobrescritas: managed-by, project, instance e version. O runner le as quatro de volta para casar container com app, varrer orfaos e adotar container perdido. Um version sobrescrito faria o runner remover o container recem-implantado. Tentativa de sobrescrever e ignorada com aviso.

Corrigido

  • runner preflight validava o manifesto errado em app com multiplos manifestos. Ele assumia pull/.deploy.yml enquanto o deploy resolve pelo caminho registrado no state, entao emitia parecer confiante sobre um arquivo diferente do que seria deployado — aprovando sem olhar o que importa, ou reprovando app saudavel. Como o preflight sai com codigo nao-zero em red e serve de gate em CI, o impacto era direto.

Notas

  • ccs.systems/uid recebe o nome da app. Quem declarar uid em labels: sobrescreve.
  • ccs.systems/env e omitida quando a instancia nao mapeia para dev|prod|staging|shared (ex: v1, homolog). Label ausente e melhor que label mentindo para quem filtra.

[2.67.0] - 2026-08-05 — Campo desconhecido no manifesto deixa de passar em silencio

Adicionado

  • Campo desconhecido no .deploy.yml gera aviso em vez de ser ignorado. Todos sao coletados de uma vez, com sugestao de correcao quando possivel:

    2 campo(s) desconhecido(s) no .deploy.yml — IGNORADOS pelo runner:
         - repo          -> voce quis dizer `github.repo`?
         - labels        -> nao suportado nesta versao

    O dano era silencioso e real: apps declaravam blocos inteiros que nunca chegaram a container nenhum, e ninguem percebia por falta de aviso.

    Esta versao apenas avisa. Bloquear e uma fase futura — quem tem manifesto antigo tem tempo de migrar.

  • runner preflight ganhou dois componentes:

    • env_drift — divergencia entre o .env/.secrets efetivo e o environment: do manifesto. Divergencia de valor e warn (o .env vencer o manifesto e o contrato documentado); PORT divergente e red, porque o Traefik roteia para a porta do manifesto enquanto o app escuta a do .env — resulta em 503 com container saudavel.
    • manifest_sync — compara o .deploy.yml canonico com o do pull/. A comparacao e semantica (ordem de chave e comentario nao disparam alarme) e a mensagem diz qual vence no proximo deploy.

[2.66.0] - 2026-08-05 — Wizard deixa de descartar respostas; `.env` das apps em 0600

Adicionado

  • Navegacao "voltar" no wizard. ESC volta uma pergunta (repetido, volta mais) e as perguntas seguintes vem pre-preenchidas com o que ja havia sido respondido. Responder n no "Confirma e salva?" abre um menu de edicao por campo, em vez de descartar as respostas e obrigar a recomecar.

    ESC e a unica tecla usada: Backspace ja apaga caractere e a seta-para-cima e o historico do campo — sequestrar qualquer uma quebraria a edicao normal.

Alterado

  • ESC no "Confirma e salva?" cancela o wizard, em vez de voltar uma pergunta. E o unico ponto onde ESC descarta respostas — aquele confirm e a fronteira em que o wizard comeca a escrever em disco. Para corrigir um campo a partir do preview, use n.

Corrigido

  • .env das apps agora nasce 0600 (era 0644, legivel por qualquer usuario do sistema), em todos os pontos de escrita, incluindo o deploy de staging. O arquivo acumula chaves herdadas de template, incluindo placeholders de SECRET_KEY, JWT_SECRET_KEY e SMTP_PASSWORD. Nao ha custo operacional: quem le e o runner e o Docker, ambos como root.

  • Mensagem do backoff de deploy dizia waiting for new push, sugerindo que so um commit novo destravaria. O backoff afeta apenas o cron: um runner deploy <app> manual nunca foi bloqueado e, ao ter sucesso, zera o contador. A mensagem agora nomeia as duas saidas.

[2.65.0] - 2026-08-05 — `secrets:` deixa de implicar obrigatorio; `required:` vira o gate unico

Alterado

  • secrets: significa "e sensivel", nao "e obrigatorio". Declarar uma chave sob secrets: bloqueava o deploy com valor vazio — falso positivo em segredo legitimamente vazio (por exemplo REDIS_PASSWORD num Redis sem auth). O unico contorno era mover a chave para environment:, o que a tira do .secrets (0600, cifrado) e a joga no .env: a saida de emergencia levava a menos seguranca. Agora secret vazio gera aviso, nao bloqueio.

  • required: e o gate unico de obrigatoriedade, com flag dedicada --allow-missing-required (em vez do --insecure, que desliga todas as protecoes).

Adicionado

  • required: por instancia, com uma regra: declarou = essa e a lista final; nao declarou = herda a base.

    YAML na instancia efeito
    (ausente) ou required: vazio (null) herda a base
    required: [] nenhuma obrigacao
    required: [A, B] exatamente A e B (substitui)

    Quando a lista da instancia omite chaves da base, o deploy avisa: a substituicao e intencional, o enfraquecimento nao pode ser silencioso.

  • runner add recusa manifesto sem instances:, que registrava um app impossivel de deployar — e o erro so aparecia comandos depois, citando um nome de instancia que nao existe no manifesto do operador.

Corrigido

  • Troca silenciosa de instancia. Com o state pedindo uma instancia inexistente e o manifesto tendo default, o runner trocava de instancia sem alarde. State pedindo homolog num manifesto so com production publicava producao em silencio. Agora e WARN nomeando as duas e sugerindo runner edit --instance.
  • O gate de required: nao cobria o cron. Era checado so no comando runner deploy; fetch --deploy (caminho desassistido) nao checava nada. O gate foi para o pipeline, cobrindo manual, cron e PR.
  • Falha de pre-flight reembalada como falha de container — o erro subia como "container could not start" e arquivava log de um container que nunca existiu. Agora roda antes de qualquer acao de container.

[2.64.0] - 2026-08-03 — Fingerprint de token utilizavel; custodia visivel

Adicionado

  • runner tokens list mostra o fingerprint completo. A lista identificava tokens por um valor que nao dava para digitar (a forma abreviada com ...), e nenhum comando aceitava aquele identificador. Agora ha coluna com a forma completa, no humano e no --json (fingerprint_full).
  • --token-from aceita fingerprint alem de nome de app, com match exato — forma abreviada e prefixos sao recusados com erro que ensina o formato. O token sai selado de um state e entra selado no outro, sem virar plaintext.
  • unregister avisa ao remover a ultima copia de um token. O token vive copiado no state de cada app (nao ha vault central), entao remover a ultima portadora apagava o valor em silencio. O aviso sugere --token-from ou state backup.

[2.63.0] - 2026-08-02 — `fetch` para de mostrar manifesto velho; `--local` zero-download

Corrigido

  • runner fetch agora reparseia o manifesto cacheado. A deteccao de update nao tocava em disco: o .deploy.yml da raiz do app (e portanto runner manifest <app>) continuava mostrando a config antiga depois de um fetch que ja reportava o commit novo. O refresh so acontecia quando um deploy rodava — dando a impressao de "atualizou sozinho minutos depois" e divergindo entre apps.
    • Manifesto que nao parseia no head remoto nao substitui um cache valido.
    • Apps geradas de compose nao sao sobrescritas (o manifesto delas e gerenciado por runner reconcile).

Adicionado

  • runner deploy --local agora e zero-download. Antes, --local mantinha o .deploy.yml da raiz soberano mas ainda rodava o git pull. Agora o pull e pulado por completo e o deploy usa os artefatos ja em disco. Sem pull/ (ou com ele vazio), erro claro no_local_artifacts em vez de deployar o nada.

[2.62.0] - 2026-08-02 — Secrets assimetricos; middlewares do Traefik sobrevivem ao redeploy

Adicionado

  • Esquema assimetrico de secrets, valor a valor. Arquivo textual com a chave legivel e so o valor cifrado, uma linha por variavel. Alterar um secret nao abre nem recifra os demais (diff de uma linha). A chave privada fica no servidor e a publica vai no header, entao runner env set cifra sem acesso a privada. Cada valor carrega um envelope validado no decrypt: ciphertext copiado de outra app, instancia ou variavel e rejeitado. Suporta valores multilinha (PEM), que o .env nao comportava.

    Registro com runner add --ckey v2; migracao com runner secrets migrate. Apps legados seguem inalterados. Ver ckey simetrica e assimetrica.

  • Passthrough de middlewares e tls.options. Bloco routing: cujos refs o runner apenas repassa aos routers gerados:

    routing:
      middlewares: [vpn-only@file, cloudflare-only@file]
      tls_options: mtls-aop@file

    Os objetos continuam definidos pelo operador no Traefik; o runner so referencia. Com isso, protecoes de infra (mTLS, IP allowlist) sobrevivem ao redeploy em vez de sumirem quando os routers sao regenerados. runner preflight avisa quando um ref @file nao existe no Traefik.

Corrigido

  • --slug nao sobrevivia ao deploy. O override valia so no registro; o deploy relia o manifesto do repo e voltava ao valor original, fazendo dois apps do mesmo repo gerarem o mesmo nome de router (colisao no Traefik).
  • Arquivo de secrets assimetrico nascia 644 e so virava 600 no primeiro env set.
  • Erro de app ambiguo listava um identificador identico entre dois registros do mesmo repo. Agora lista o nome do app, unico e digitavel.

[2.61.0] - 2026-07-31 — scrub de arquivos sensíveis + skip-last + Traefik imperativo

Adicionado

  • Scrub de artefatos sensíveis no deploy: .env*, .secrets*, .deploy.yml, docker-compose*, configs de CI (.github/, .gitlab-ci.yml, Jenkinsfile…), material de chave (*.pem/*.key/*.enc…) e configs de credencial (.npmrc, .netrc…) são excluídos automaticamente do artefato deployado — bots varrem esses paths. Fecha bind-mount (IMAGE) e contexto de build (BUILD) num mecanismo só; Dockerfile* preservado. Escape hatch por app: security.allow_sensitive: [nomes] no .deploy.yml.
  • runner fetch <app> --skip-last: marca a alteração pendente como considerada sem deployar. Novo push volta a "disponível"; deploy --force deploya o pulado. Ver runner fetch.
  • Roteamento Traefik imperativo (modo local): a fase ROUTING descobre o Traefik em execução e usa o dynamic_dir que ele lê (warn em divergência), valida entrypoints/cert_resolver contra o static config real e usa o nome real do container. Modo docker_labels segue suportado, com alerta recomendando o modo dynamic.

[2.60.1] - 2026-07-29 — `edit --branch` volta a funcionar no layout `pull/`

Corrigido

  • runner edit <app> --branch <b> abortava com "No git repository found" em todo app registrado no layout v1.2.3+: a validação procurava .git na raiz do app (/data/apps/<app>), mas o clone vive em <app>/pull/. Na prática não havia como apontar um app já registrado para outra branch (ex.: migrar de main para dist) sem unregister + add. Ver runner edit.
  • Troca de branch falhava em clone single-branch. O pull/ nasce de clone --branch X --depth 1, cujo remote.origin.fetch cobre só a branch rastreada; git fetch origin <outra> atualizava apenas o FETCH_HEAD e o reset --hard origin/<outra> seguinte morria em "unknown revision". O fetch do edit e o do deploy passam a usar refspec explícito +refs/heads/<b>:refs/remotes/origin/<b>.
  • --pull deixava o clone na branch antiga quando o worktree estava sujo — o state dizia dist e o clone continuava em main (apontando pro commit certo, o que tornava o sintoma difícil de ler). Agora é checkout --force -B <b> origin/<b>.

[2.60.0] - 2026-07-28 — `set-ckey --generate` headless

Adicionado

  • runner set-ckey <app> --generate: gera e anexa a ckey sem prompt (chama ensure_ckey), idempotente, com envelope JSON ckey.ensure. Permite provisionar app nova em automação, sem wizard interativo.

[2.59.0] - 2026-07-28 — `ensure_ckey` no deploy (failover de ckey)

Adicionado

  • Todo deploy garante a ckey antes de montar o bundle (ensure_ckey, com failover local via attach_ckey_to_app, idempotente). Mata a classe de falha "deploy sem ckey" em app registrada sem --ckey.

[2.58.0] - 2026-07-24 — `fetch --deploy` detecta tags (canal de release por tag)

Adicionado

  • fetch --deploy agora detecta tags novas para instâncias source: type: tag — completa a feature da v2.57.0. Glob (v*) e vazio funcionam como canal de release automático (tag nova em commit novo → deploy sozinho); pin (v1.2.2) nunca auto-deploya. Ver Deploy a partir de git tag.

[2.57.0] - 2026-07-24 — deploy a partir de git tag (`source: type: tag`)

Adicionado

  • source: type: tag: deploya o código de uma git tag resolvida relativa a uma branch (só tags alcançáveis por ela). Um campo tag com 3 modos — glob (v* → última que casa), pin (v1.2.2 → exata), vazio (última por data) — mais sort: semver|date. Versão do deploy = nome da tag. Ver Anatomia do .deploy.yml.

    Escopo (v2.57.0): deploy manual. O auto-deploy no cron chegou na v2.58.0 (ver acima).

[2.55.2] - 2026-07-24 — default de instância determinístico

Corrigido

  • instances no .deploy.yml virou IndexMap (era HashMap), preservando a ordem do YAML. Quando não há production nem state.instance, o default de instância passa a ser a primeira instância declarada — determinístico, resolvendo a nota conhecida da v2.55.1.

[2.55.1] - 2026-07-24 — `runner deploy` honra a instância registrada

Corrigido

  • runner deploy e runner deploy --all agora honram state.instance (a instância com que o app foi registrado via runner add --instance <N>), igual ao runner fetch --deploy. Antes os deploys manuais miravam sempre a instância default do manifesto. Ordem de resolução documentada em cada comando (deploy, deploy-all, fetch). Ver também Dois deploys da mesma branch.

Nota conhecida (resolvida na v2.55.2)

  • Default de instância sem production era não-determinístico nesta versão (instances era HashMap). A v2.55.2 troca por IndexMap e o default passa a ser a primeira instância declarada no YAML. Recomendação continua: declarar production, registrar com --instance, ou usar um manifesto por deploy.

[2.55.0] - 2026-07-22 — hierarquia de secrets (`secret.enc > env > deploy.yml`) + diagnosis envelope

Adicionado

  • Envelope terminal com diagnosis estruturado (data.after.diagnosis) em falhas de deploy no modo -o json, incluindo aborts precoces (secrets/preflight) — consumers exibem causa raiz estruturada em vez de só stderr.

Corrigido

  • Secrets shadowing: placeholders {{::KEY}} sem valor não são mais materializados como linha KEY= vazia (Fix A), e o merge do bundle não deixa uma linha local vazia sombrear o valor real do secrets.enc (Fix B). Garante a hierarquia secret.enc > env > deploy.yml. Ver Hierarquia env/secrets.
  • Envelope success reflete o resultado real do deploy, não o mero fato do comando ter rodado.

[2.54.0] - 2026-07-22 — `--skip-health` no deploy/add + secrets mascarados no wizard

Adicionado

  • --skip-health em runner deploy e runner add --deploy: pula a validacao de saude so naquela invocacao, equivalente a healthcheck: mode: skip_check porem sem tocar no manifesto do repo. Para o caso em que o container sobe mas ainda nao ha Traefik/healthcheck valido no server — a app e declarada OK sem gating, sobe, e um redeploy futuro (ja com o ambiente pronto) volta a validar a saude normalmente, sem reinstalar. Emite o evento de notificacao HealthSkipped. Veja `runner deploy` e Modos de healthcheck.

Corrigido

  • Inputs de secrets: no wizard do primeiro deploy agora entram mascarados: os prompts de secret resolvidos como "{{::...}}" eram lidos em texto puro, ecoando o valor no terminal/scrollback. Agora entram como *** (nunca ecoam). Campos de environment: (plain) seguem em texto claro. Veja Classificacao de env/secrets.

[2.53.4] - 2026-07-18 — cleanup --build-cache respeita o config

Corrigido

  • cleanup --build-cache passa a respeitar o config: o CLI tinha um 72h hardcoded que ignorava docker.auto_prune_build_cache. Quem configurasse 24h (ou all) ainda via 72h no comando manual — e como o filtro de idade nao pega cache recente, o operador rodava o cleanup e "nao recuperava nada". Precedencia agora: flag explicita → config → 72h.

[2.53.3] - 2026-07-18 — build cache limitado por tamanho + pressao de disco

Adicionado

  • Teto de tamanho do build cache (docker.auto_prune_build_cache_max, default 5GB): o auto-prune pos-build aplica docker builder prune --max-used-space, mantendo o cache recente (builds seguem rapidos) e evictando o excedente. Corrige o buraco do filtro de idade — 72h so evicta cache mais VELHO que a janela, entao em box que deploya varias vezes por dia o cache crescia sem limite (~1 GB por deploy) ate encher o disco.
  • Gatilho de pressao de disco (docker.build_cache_min_free_mb, default 2048): se o livre cair abaixo do minimo ANTES de um build, o runner poda o cache antes de buildar. O prune normal roda so depois do build. 0 desabilita.
  • cleanup --images tambem aplica o teto do cache: na pratica o build cache costuma ser maior que as imagens.

[2.53.2] - 2026-07-18 — secrets show/pull per-instância/per-target

Corrigido

  • secrets show/pull enxergam apps per-instância/per-target (#58): liam .runner/secrets.enc root hardcoded, invisível a apps cujo bundle é secrets.<instance>.enc (ou secrets.<instance>.<target>.enc) — retornavam vazio. Agora resolvem o bundle real (per-target → per-instance → legado).

[2.53.1] - 2026-07-18 — namespace do bundle por target

Adicionado

  • Namespace do bundle de secrets por target (#50): novo campo target: no config.yml (opt-in). Quando setado, o bundle vira secrets.<instance>.<target>.enc — cada server tem seu slot, cifrado com a sua ckey. Resolve o clobbering quando 2 servers deployam a mesma instância do mesmo app. Read prefere per-target → per-instance → legado (self-healing). Sem target: = flat inalterado.

[2.53.0] - 2026-07-17 — rotate-secret manual + rede de recuperação

Adicionado

  • ckeys rotate-secret --to-plain (#43): modo manual — rotaciona a ckey de um app a partir de uma ckey nova em texto puro via STDIN (CCS ou usuário), sem passar pelo cofre. Re-encode atômico (failover/rollback), push estrito, prova JSON; a ckey nunca é ecoada/logada.
  • Rede de recuperação + auto-heal: o rotate preserva a ckey anterior (state.ckey_previous). Se o bundle novo não chegou ao repo (--no-push, push falho, drift por pull), o deploy auto-heala — reconhece 'repo pré-rotação', não aborta, e re-pusha o bundle local. Sem perda de conteúdo.
  • runner validate --app <name>: valida app registrado por nome.

Corrigido

  • rotate-secret mira o bundle que o deploy lê (secrets.<instance>.enc quando existe, root senão).

[2.52.0] - 2026-07-17 — deploy --local

Adicionado

  • deploy --local (#57): deploya o estado que esta no disco sem re-sincronizar do repo — pula o git reset do pull/ e o refresh do .deploy.yml a partir do pull/. Fecha a perda silenciosa em que uma edicao local era revertida pra versao do repo no deploy --force. Naquele deploy o disco e soberano (nao persiste); combina com --force, nao combina com --all. Complementa o env_sync: skip (que cobre so .env/.secrets).

[2.51.0] - 2026-07-17 — Rede Docker IPv6/dual-stack no manifesto

Adicionado

  • Rede Docker IPv6/dual-stack a partir do manifesto (#56): networks: aceita a forma-objeto com ipv6:/subnet_v6:/subnet_v4:/bridge_name:. Quando a rede nao existe, o runner faz docker network create --ipv6 --subnet ... (ULA RFC 4193 deterministica do nome se subnet_v6 omitido → portavel entre hosts). Resolve o caso "alcancar destino IPv6-only" (Supabase) de forma declarativa, sem docker network create manual. Drift de EnableIPv6 → WARN (nunca recria). O runner nao escreve sysctl — so le net.ipv6.conf.all.forwarding pra um aviso; o Docker habilita o forwarding ao subir a bridge --ipv6. Retrocompativel: networks: como lista de strings segue identico.

[2.50.2] - 2026-07-17 — cleanup: --project não gateia fases globais

Corrigido

  • cleanup: over-gating do --project (#49): fases globais explícitas (--expired/--orphans/--prs/--images/--build-cache) não gateiam mais a confirmação multi-alvo — cleanup --project X --images em cron (sem --yes) abortava por uma confirmação que não protegia nada. Com fases globais, --project é ignorado com WARN explicativo; o gate e a fase de versões do projeto só entram com --project sozinho ou --reset-root.

[2.50.1] - 2026-07-17 — Panics de runtime aninhado no CLI do traefik

Corrigido

  • traefik doctor / validate / weight não panicam mais (#55): os handlers sync carregam runtimes tokio internos (resolver DNS do TM-DNS, cliente HTTP blocking do TraefikApi) e eram chamados do contexto async do main — o block_on interno panicava (Cannot start a runtime from within a runtime) e o teardown panicava com exit 101. Agora rodam sob block_in_place. Efeito prático: o doctor completo executa (o check TM-DNS nunca tinha conseguido rodar) e doctor --check / validate --apply saem com exit 0.

[2.50.0] - 2026-07-17 — Incidentes de produção: prune de órfãs no Traefik, logs de container falho, mask de secrets

Adicionado

  • Prune automático de services órfãs no Traefik (#52): no modo file-provider, cada deploy blue-green deixava a service da versão anterior órfã no arquivo dinâmico — com healthCheck ativo batendo num container que não existe mais, acumulando indefinidamente. Após promover a versão nova, o runner agora poda automaticamente as órfãs do próprio app (escopo {project}-{instance}-; nunca toca services de outros apps ou arquivos do operador). Novo check TM-ORPHANS no runner traefik doctor lista órfãs em qualquer arquivo.
  • Logs do container falho arquivados antes do rollback (#54): quando o deploy falha no start ou no health check, o rollback destruía o container levando junto os logs que diagnosticariam a falha. Agora as últimas 200 linhas de docker logs são salvas em <logs_dir>/failures/<app>/<version>_<timestamp>.log antes do remove; a mensagem de erro e o runner debug last-error apontam pro arquivo.
  • traefik validate --verify none|loaded|http: aplica ops sem consultar a API do Traefik (--verify none) — útil pra poda manual de órfãs acumuladas (- op: service_delete / name: <service>).
  • Snapshot com source por instância (#53): a lista flat instances[] do runner snapshot carrega slug/repo/branch/vcs_provider denormalizados do project (a coluna SOURCE do CCS dependia disso).

Corrigido

  • Vazamento de segredo no aviso de divergência (#52): runner env set --secret imprimia o valor do secret em claro no aviso manifesto declara X mas o runtime usa Y. Valores de keys sensíveis (do .secrets, declaradas em secrets: ou com nome secret-like) agora saem mascarados (prefixo de 4 chars + tamanho).
  • deploy --force respeita pull_policy: never (#52): em modo IMAGE com imagem carregada localmente (docker load), o force pulava direto pro pull que sempre falhava com WARN ruidoso. Agora skipa o pull com log informativo.
  • Ops de delete serializáveis: service_delete/router_delete não eram expressáveis no traefik validate --ops-file (falha de serialização); corrigido.

[2.49.0] - 2026-07-12 — Origem (trigger/actor) nos deploy-events + canary history

Adicionado

  • Origem do deploy (trigger/actor): o histórico de deploy passa a registrar quem/de onde o deploy veio, dimensão distinta dos logs de execução (result/error). DeployTrigger = cli (deploy manual), cron (fetch --deploy agendado), api (dispatch do CCS), webhook ou unknown. Novas flags --trigger/--actor em deploy (default cli) e fetch (default cron); a rota de API de deploy aceita trigger/actor no body. O logs history carrega os campos no envelope e ganha a coluna ORIGIN. Backward-compat via serde default (audit antigo → unknown).
  • Origem também nos deploy-events: além do audit (deploy-audit.json), o DeployEvent (started/success/failure) em deploy-events.jsonl carrega trigger/actor. Como é desse arquivo que o CCS renderiza o histórico (via runner_audit_read), a origem chega ao front sem mudança no client.
  • runner canary history: novo subcomando que expõe a timeline de mudanças de peso do canary (envelope canary.history) — contraparte do logs history do deploy.

[2.48.0] - 2026-07-09 — Preflight, manifest-stdin e secrets local-only

Adicionado

  • runner preflight <app> (P4): readiness de deploy por componente (manifest / secrets / ports / dns / image-args) num relatório estruturado verde/amarelo/vermelho — sem subir container. É o gate do passo 4 do Deploy Wizard do CCS. Agrega checks já existentes (validate_project, validate_secrets_have_values, ports_in_use, dns_check::check_one, preview_run_args) sem reimplementar nada. --json emite {app, overall, components[]} pro consumo programático. Exit code != 0 apenas quando o overall é red (problema bloqueante); warn e green são sucesso.

  • runner add --manifest-stdin (R1): ingere um .deploy.yml gerado externamente (deploy wizard do CCS) via stdin. O runner clona o repo, grava o manifesto ingerido no clone antes da validação e reusa o fluxo existente (load → validação → bootstrap_push → deploy). Incompatível com --from-compose; stdin vazio falha com manifest_stdin_empty. Sem a flag, o comportamento é idêntico ao repo-based atual.

  • instances.<nome>.local_secrets (#44): marca uma instância como local-only — o deploy não pusha o secrets.enc pro repo nem sobrescreve .env/.secrets locais a partir do remoto. Resolve o caso "secret local numa instância, mesmo repo sobe normal noutra" sem .gitignore.

  • runner secrets push <app> --force (#44): o comando secrets push ganhou a flag --force, que passa a forçar commit+push do secrets.enc sob demanda (ignora o short-circuit de "não mudou"); aceita -i <instância> e -x (match exato); recusado em instâncias local_secrets. Sem --force, secrets push mantém o comportamento antigo de bootstrap.

Corrigido

  • self-update estourava memória (OOM) em box memory-tight: o download usava response.bytes(), que carregava o binário inteiro (~15MB) no heap e abortava em máquinas apertadas sem swap. Agora o download é streamado direto pro disco via response.chunk() (memória constante) e a assinatura minisign é verificada por mmap (páginas file-backed, reclamáveis, sem heap). Mesma classe do fix aplicado no ccs-client.

[2.47.0] - 2026-07-08 — Resolução inteligente de referência de app (#46)

Adicionado

  • Resolução multi-identidade de <app> (#46): todo comando que recebe <app> passa a aceitar slug do projeto, project/system, nome exibido no runner list, repo (owner/name ou basename) e domínio da instância como sinônimos — case-insensitive, -/_ equivalentes. Índice construído em memória por invocação a partir do que já é lido hoje (apps/*/.deploy.yml), sem cache (custo desprezível).
  • Exato vence, senão radical por prefixo-de-segmento: se a referência é o nome exato de um app, resolve só ele; senão casa por prefixo de segmento (ccs- → todos os ccs-*). Comandos de leitura (status, versions) agem em todos os alvos casados; comandos que mutam (deploy, fetch, restart, reconcile, cleanup, unregister) listam os alvos e pedem confirmação (ou --yes em automação/CI); comandos de alvo único (edit, set-ckey, mkey, rotate-secret, adopt) exigem match único e erram listando candidatos se houver ambiguidade.
  • -x/--exact: flag global que força match exato, desabilitando o radical.
  • Batch tolerante a falha: num multi-alvo mutante, a falha de um alvo é registrada e o comando segue pros demais, agregando o resultado (exit≠0 se algum falhou); alvo único propaga o erro cru (comportamento inalterado).
  • Fora de escopo (v1): nome de container não é identidade de resolução — ficaria preso a uma consulta Docker por invocação (assíncrono, custo desproporcional pro hot-path síncrono de hoje). Registrado como follow-up (#48) — a resolver com um cache de containers fora do hot-path (ex: alimentado por um watcher de docker events), nunca uma query Docker síncrona por chamada.

[2.46.1] - 2026-07-08 — Correção de flake nos testes (test-only)

Corrigido

  • Flake de isolamento nos testes de custódia de ckey (#45): ckey_custody/set_ckey/fetch cada um tinha um static MKEY_ENV_LOCK por-função — mutexes distintos que não serializavam entre módulos, então o override concorrente de RUNNER_KEYS_DIR (env var global ao processo) apontava o master key pro dir errado e causava falha espúria não-determinística no cargo test paralelo (e no gate do make publish). Consolidado num lock único compartilhado (crypto::keys_dir_env_lock/with_isolated_keys_dir). Validado com 21 rodadas paralelas sem falha.

Nota: release test-only — o binário é idêntico ao v2.46.0 (as mudanças vivem só em #[cfg(test)]). Sobe apenas pra registrar o fix na CDN/releases.

[2.46.0] - 2026-07-07 — Integração CCS: force/fetch via task + source no snapshot

Adicionado

  • Snapshot reporta origem por project (#40): cada SnapshotProject agora inclui repo, branch e vcs_provider (derivados de github.repo/dist_branch do .deploy.yml; vcs_provider assume github). Acende a badge SOURCE no CCS (/cicd/deploys) linkando pro repositório, sem mudança no front. Aditivo — campos omitidos quando não há bloco github:.
  • ckeys rotate-secret --to-sealed (Fase 1, #43 — fail-closed): primitivo de custódia de ckey selada do cofre — lê o blob selado do stdin, valida context (anti-replay por generation + project/target), re-encode atômico journaled two-phase do secrets.enc e push estrito com prova JSON. A verificação de assinatura + unwrap da ckey ficam atrás de um trait com stub fail-closed (aborta "contrato do cofre pendente") até o contrato cofre↔runner fechar (Fase 2). O comando existe no binário mas nunca instala uma ckey não-verificada — seguro por construção. State ganhou ckey_generation (default 0).

Corrigido

  • Handlers MCP repassam --force e fetch aceita app (#41): handle_deploy/handle_fetch agora montam os comandos completos que a CLI já suportava (deploy <app> --force, fetch [<app>] --deploy --force), destravando os botões force/fetch-per-app do CCS. Removido o -i <instance> inválido do handle_deploy (o Deploy da CLI não tem esse flag — o deploy via task estava sendo montado com um argumento que o clap rejeita). Schemas dos MCP tools runner_deploy/runner_fetch atualizados.

[2.45.0] - 2026-07-05 — Notificações per-app por template

Adicionado

  • Bloco notify: no .deploy.yml — notificações per-app, declarativas, por template. Cada app assina eventos de deploy (deploy_success/deploy_failure/deploy_rollback/deploy_started) e dispara uma mensagem/requisição montada por template.
  • Sintaxe {{::var}} (a mesma dos secrets) interpola contexto do deploy (repository_name, deploy_result_status, deploy_version, deploy_error, instance_name, instance_domain, event, deploy_id, project) + env/secrets da app.
  • Canais nomeados no config.yml (notify.channels:): webhook (requisição HTTP estruturada method/url/headers/body), telegram, smtp (email via lettre/rustls). Extensível via o trait NotifyChannel.
  • Gate when: opcional por subscrição (==/!=); sem when, sempre dispara.
  • Fire-and-forget: o dispatch é best-effort, com timeout (10s por envio, 30s total), silent failover (canal ausente/falho → só loga) e NUNCA bloqueia/falha o deploy. Aditivo ao notify global do servidor.

[2.44.2] - 2026-07-03 — Corretude de secrets no funil (R1/R2/A1/A2)

Corrigido

  • Secret generate inválido no render do core — o wizard web/MCP/CCS emitiam {GENERATE:hex:32} (sem $), formato que o resolvedor NÃO reconhece → o secret de produção virava a string literal. Agora ${GENERATE:hex:64} (paridade CLI).
  • Secret literal vazava em plaintext no .deploy.yml commitável (render do core) → agora vira o placeholder {{::KEY}} (valor pedido no registro).
  • ${GENERATE:alnum:N} do picker CLI não tinha braço no resolvedor → mesma classe do bug acima, viva no CLI. Corrigido + generate_alnum.
  • Contrato único ${GENERATE:...} no core (runner_core::generate): o wizard-mcp deixa de reimplementar um contains("GENERATE") que abençoava formatos quebrados; o dry-run agora reprova um secret que não resolve.

[2.44.1] - 2026-07-03 — Polimento do multi-ambiente

Corrigido

  • Bootstrap por instância — runner add (bootstrap_push) semeava o secrets.enc global no upstream mesmo para apps per-instância; agora semeia secrets.<instance>.enc (mesmo failover da leitura/commit). Fecha o último canto assimétrico do modelo por instância.
  • add --instance surfa falha de provisionamento — se o set-ckey automático no registro falhar, o operador vê um aviso claro (⚠ + comando sugerido), não só uma linha no log.
  • set-ckey trima a ckey — espaços em volta não vazam mais para encrypt/fingerprint (evita "parece que bate mas não bate").
  • .gitignore ignora .worktrees/ (higiene).

[2.44.0] - 2026-07-02 — Secrets isolados por instância (multi-ambiente)

Adicionado

  • Secrets por instância com failover retrocompatível. Cada instância pode ter seu próprio secrets.<instância>.enc, cifrado com ckey própria — staging e produção deixam de compartilhar credenciais. A resolução no deploy segue um failover de dois eixos: Eixo A (arquivo) config.bundle explícito → secrets.<instância>.enc (por convenção de nome) → secrets.enc global; Eixo B (config ausente) degrada pro provider Bundle padrão da instância declarada no deploy. Ausência nunca é erro. App antigo com só secrets.enc se comporta byte-idêntico ao anterior.
  • Bloco config: por instância no .deploy.yml (source: bundle | vault, bundle, vault: {addr, path, mode}). O wizard emite o bloco só quando não-default; o runner add gera manifest sem bloco (secrets por convenção). Lido no deploy pelo reader do client (InstanceDeployConfig).
  • Escrita/commit de secret por instância — o deploy (phase 1.6 + commit) agora grava/commita secrets.<instance>.enc (o mesmo arquivo que a leitura resolve), não mais secrets.enc global. Fecha a assimetria read/write: runner secrets set num servidor persiste no arquivo por-instância no próximo deploy. Regra do nome centralizada em target_enc_filename. Retrocompat: app legado mantém secrets.enc.
  • runner set-ckey --instance — provisiona a ckey de uma instância cifrando .env/.secrets em secrets.<instance>.enc (ckey via --ckey ou stdin sem eco) e registrando-a no vault/state local. runner add --instance --ckey provisiona o .enc no registro. Separação por servidor; source: vault no deploy segue com erro loud (resolução via vault fora de escopo).
  • Abstração ConfigProvider no core: BundleProvider (default, zero-dependência) decifra o .enc da instância; VaultProvider (opcional) faz poll HTTP (KV v2) ou lê arquivo JSON local. Token do vault sempre de RUNNER_VAULT_TOKEN, nunca do manifest.

Segurança / build

  • Vault é feature opt-in de compilação (--features vault no runner-core, ativada pelo client). Sem a feature, ureq não é linkado — o core segue wasm-safe para as SPAs e o default permanece zero-dependência.
  • source: vault dá erro loud no deploy (resolução via vault ainda não habilitada; provisionamento pendente de spec própria) — evita deploy silencioso sem secrets.

Notas

  • Fronteira dev↔prod recai só nos secrets (ckey independente por instância); config não-secreta (environment_overrides) fica em texto plano no manifest.
  • Pendente: provisionamento de ckey independente por instância (set-ckey) .

[2.43.9] - 2026-06-25 — assets chown por uid:gid numérico (não pelo nome)

Corrigido

  • runner assets upload e chown de state/cache no deploy agora resolvem o user do container para uid:gid numérico antes do chown no host. Antes usavam {{.Config.User}} direto (ex.: ccs) — um nome que não existe no host — então chown ccs falhava e o asset (ex.: secret PEM) ficava root:600, ilegível pelo uid do processo do container. Agora: nomes são resolvidos via id (docker exec no container rodando; docker run na imagem no deploy); specs numéricos (999, 999:986) usados direto; root/0 → skip. Helper puro classify_chown_user + testes unitários.

Contexto

Quebrava o provisionamento de assets.secrets em qualquer imagem com USER nominal — inclusive a rehidratação host-fresh do Regra 9 (export/import de projeto CICD), onde o secret precisa ser legível pelo container no primeiro boot.

[2.43.3] - 2026-06-24 — anexar ckey a app já registrado (sem re-add)

Adicionado

  • runner deploy <app> --ckey [<chave>] — anexa um ckey a um app que ainda não tem, sem precisar re-registrar. --ckey sozinho gera um AES-256-GCM novo; --ckey <chave> usa a informada. No-op (com aviso) se o app já tem ckey; nunca sobrescreve. Grava no state.ckey, então destrava env set --secret / secrets set. Não combina com --all.
  • runner env set <app> KEY VALUE --secret — em modo interativo, quando o app não tem ckey, agora pergunta se quer gerar+registrar um na hora (em vez de só errar). y → gera, anexa e salva o secret encriptado. JSON/não-interativo mantêm o erro estruturado (E7002) — CI/scripts inalterados.

Contexto

Antes, secrets num app registrado sem ckey exigiam re-registrar (runner add --ckey, destrutivo) ou copiar de um peer. O runner ckeys add só criava no vault, sem gravar no state do app — por isso não destravava o env set --secret.

[2.43.2] - 2026-06-24 — redesenho do template de notificação de deploy

Modificado

  • Template das mensagens de deploy reformulado. Header lidera com a ação (🚀 Deploy Iniciado / ✅ Deploy Concluído / ❌ Deploy Falhou), seguido de Deploy: <app>, Projeto: <grupo> (opcional), 🌐 Instance: <inst>. Removido o badge de cor de ambiente (🟢/⚪ PROD) — a instância textual substitui.
  • Commits agora aparecem na mensagem de INÍCIO (não mais na de conclusão), pra diagnosticar o que está sendo deployado antes de um possível erro. A captura de commit passou a rodar antes do deploy_started.
  • Linha de branch simplificada: 🌿 Branch: <branch> (+ · PR #N quando detectado). Removido o ⚠️ direct push e o ícone de alerta, que confundiam.
  • Conclusão/Falha enxutas e auto-contidas: repetem nome/projeto/instância/versão, com ⏱️ duração; falha traz ⚠️ Erro:. Não repetem domínio, branch nem commits.

Adicionado

  • project_group: campo opcional no .deploy.yml (cliente/produto guarda-chuva), exibido como Projeto: quando presente. Não confundir com project (nome do app).
  • Campo project_group no payload JSON do evento (webhook/audit) quando setado.

[2.43.1] - 2026-06-24 — fix: notify silenciava deploy_success/failure com `_`/`*` no subject

Corrigido

  • Notificação de deploy_success/deploy_failure sumia quando o subject do commit tinha _, *, ` ou [ (snake_case é comuníssimo). O to_markdown() interpolava subject/autor crus, fora de code span, e o canal Telegram envia com parse_mode=Markdown → a API respondia HTTP 400 "can't parse entities" e o erro era engolido pelo let _ = …send(). Como o deploy_started é emitido antes da captura de commit (sem linha de commit), ele sempre chegava — daí o sintoma "iniciado chega, concluído some". O evento continuava gravado no audit log; só o chat sumia.
    • Fix: subjects, autores e o resumo de erro agora vão em code span (`...`), onde os metacaracteres são literais; crases internas são sanitizadas pra não fechar o span. Não era race/timeout nem backend-specific (o report suspeitava) — qualquer app com _/* no subject era afetado.

[2.43.0] - 2026-06-11 — notificações por classe (3 gates) + health/init-log opcionais

Modificado

  • Notificações agora são por classe. notify.enabled (default true) controla a classe deploy (iniciado/concluído/falha); notify.extras.* (todos default false, opt-in) controlam os extras.
  • ⚠️ Mudança de comportamento: verbose_errors (era default true, top-level) virou notify.extras.verbose_errors (default false, opt-in). O resumo de batch ("Runner deploy batch") virou notify.extras.batch_summary (opt-in — antes saía sempre que havia canal). notify_success foi removido: o "Deploy concluído" agora segue notify.enabled. Configs antigos parseiam sem erro (as chaves antigas viram inertes).

Adicionado

  • notify.extras.health_status (opt-in): status do health check pós-deploy.
  • notify.extras.init_log (opt-in): log do boot do container até o health. ⚠️ Pode conter segredos impressos no boot — habilite deliberadamente.

[2.42.4] - 2026-06-23 — Docs: flag notify_success no MANUAL

Documentation

  • MANUAL: documenta notify.notify_success (sucesso opt-in, default false) — bloco de config de referência, tabela de eventos notificados (Deploy Concluído marcado como opt-in) e subseção dedicada explicando quando ativar e que o evento de sucesso continua sempre no audit log (deploy-events.jsonl). Sem mudança de código — o flag foi entregue no v2.42.3; esta release apenas publica a doc na CDN (GitOps lê docs da tag).

[2.42.3] - 2026-06-23 — Notify: commits reais no deploy + sucesso opt-in

Fixed

  • Notify reportava o auto-commit de secrets em vez do commit real do dev: apps com encrypted secrets (.runner/secrets.enc + ckey) sempre mostravam 📝 Último: <hash> — chore: update encrypted secrets no Deploy Concluído. O runner faz um auto-commit chore: update encrypted secrets no pull/ depois do deploy, e collect_commit_info lia git log -1 (HEAD) → pegava o auto-commit. Agora os commits-bookkeeping do runner (is_runner_secret_commit) são filtrados do range <deploy anterior>..HEAD, e a notificação lista os commits reais reconhecidos no deploy (📝 Commits: — até 5, com trailer … +N mais) em vez de uma única linha. Apps sem encrypted secrets não eram afetadas e seguem iguais.

Added

  • notify.notify_success (default false): a notificação de chat por app no deploy bem-sucedido (✅ Deploy concluído) agora é opt-in. Ela duplica o resumo 🚀 Runner deploy batch e vira ruído em deploys de um dev só; só compensa em deploys multi-app/compose ou com muitas execuções acumuladas. Falhas continuam sempre notificando, e o evento de sucesso continua gravado no audit log (deploy-events.jsonl) independentemente do setting.
    • Campo novo no deploy-events.jsonl/webhook JSON: commits (lista dos commits reais do range, newest-first, capada).

[2.42.0] - 2026-06-12 — Network DNS aliases pra integração CCS↔DA + `runner env unset`

Added

  • Network DNS aliases gerenciados via .deploy.yml (Plan A da integração CCS↔DeployAlly): a chave networks: agora aceita três formatos intercambiáveis — string list legada (- public), lista de objetos (- name: X, aliases: [...]) e mapping (X: { aliases: [...] }). Todos convergem internamente pro mesmo network_aliases. O top-level network_aliases: (v2.24.0+) continua funcionando e mescla com dedup quando coexiste com aliases inline.
  • Switch determinístico de alias no blue-green: após health check passar, o runner faz docker network disconnect explícito no container antigo pra cada network com aliases declarados e confirma via docker network inspect que o alias resolve pro novo container. Elimina a janela em que ambos containers (verde + azul) compartilhavam o alias e a resolução DNS interna era não-determinística. Falha no disconnect/verificação emite warn mas não aborta — o stop/remove do container antigo logo em seguida desconecta implicitamente pelo Docker.
    • Log por switch: network alias 'deployally_sys' switched on 'internal_ccs_da': <old_container> -> <new_container>.
    • Motivador: integração CCS→DA precisa de nome estável (deployally_sys) independente da versão deployada (deployally_sys_production_v0-12-N).
  • Backward-compat estrito: configs com networks: em string-list seguem o caminho de parsing rápido (sem normalização), preservando linha/coluna em erros YAML — só configs com formato inline disparam a pré-passagem.
  • Testes: 7 novos cobrindo legacy string list, inline objects, mapping form, merge com top-level legado, default omitido, mistura string+object e contrato end-to-end yaml→--network-alias.

Added

  • runner env unset <app> <KEY>: remove uma chave do .env (default) ou do .secrets (--secret). Casos comuns: dropar um var plaintext ou um duplicado que também está cifrado no .secrets (onde .secrets vence em runtime, então a app segue funcionando). Com ckey, re-encripta o .runner/secrets.enc a partir dos arquivos pós-remoção e propaga pro remoto via commit_and_push_secrets.
    • Durabilidade: o push pro remoto é necessário — deploy Phase 1.4 sincroniza remote→local e re-mescla chaves só-remotas; uma remoção apenas local reapareceria. Se o bundle commitar local mas não propagar (push falho), emite AVISO explícito (detecta commit à frente do upstream) pra evitar falsa sensação de remoção durável.
    • Mesma rede de segurança do set: hidrata local do bundle ANTES de mutar, então nunca regrava um arquivo "magro".
    • Fecha o gap em que o runner só tinha set/load e não dava pra remover uma chave sem editar /data/apps à mão.

[2.40.0] - 2026-06-11 — traefik-manager refactor (Plans 1-8) + integração pós-v2.39 (PR #17)

Fixes de integração (review do merge com a dev v2.32→v2.39)

  • Sanitização de versão unificada (naming.rs): nomes de leaf services sempre sanitizam versões pontuadas (1.2.3 → 1-2-3) — deploy, weight, promote, abort e scheduler usam o mesmo helper (antes: deploy cru × weight sanitizado = weighted children dangling e queda de tráfego)
  • healthCheck restaurado nos services gerados (path, interval 10s, timeout 5s, hostname=domínio da rota — preserva o fix v2.23.15 de hostnames com underscore); multi-route ganha hostname por rota
  • Render escopado por arquivo: o plano renderizava o índice fundido inteiro em cada arquivo (routers de apps vizinhos vazavam; primeiro deploy de instância nova falhava com "concurrent edit detected")
  • destroy completo: remove TODOS os routers/services da instância (multi-route incluso) e o arquivo dinâmico, ANTES de remover containers (antes: preflight falhava depois dos containers já removidos = destroy parcial)
  • docker_labels multi-route: índice de rota consistente entre router/service/port labels (antes: rota ≥2 referenciava service de índice errado = rota morta)
  • Scheduler de canary usa traefik.manager.state_dir configurado (lock compartilhado com o deploy); label entrypoints vazia omitida; parity tests comparam healthCheck

Refactor completo do gerenciamento de Traefik no runner. Substitui as ~2020 LOC de geradores monolíticos em src/traefik/ por um workspace crate dedicado (crates/traefik-manager/) com:

Arquitetura

  • Operações tipadas: enum Op cobre 16 variantes (DomainAdd/DomainRemove/DomainReplace, RouterCreate/RouterDelete/RouterEditRule, ServiceSetTarget/ServiceCreate/ServiceWeights/ServiceBlueGreenInit/ServicePromote, MiddlewareAttach/MiddlewareDetach, StaticSet, Adopt, FailoverSwitch).
  • Plan + Transaction: cada operação produz um Plan revisável; Transaction.apply faz 10 steps (lock, snapshot, parse, op-apply, validate, render, write, restart-if-needed, verify, commit). Rollback automático em qualquer falha.
  • Traits para boundaries: FileSource, DockerClient, TraefikApi, DnsResolver, Lock, Clock, RestartStrategy — testabilidade alta sem touchar fs/docker real.
  • Validators: ConfigIndex com lookups (router/service/middleware/by_host/domain_index)
    • dangling_refs + duplicates.
  • Doctor: 12 checks padronizados (TM-API, TM-DYNDIR, TM-YAML, TM-DUPL, TM-DANGLING, TM-ENTRYPOINT, TM-PROVIDER, TM-NETWORK, TM-LOADED, TM-TLS, TM-LOGS, TM-DNS — este último via hickory-resolver).

Pipeline migration

  • update_traefik (Local + External + DockerLabels modes) — migrado para Ops.
  • deploy_canary, promote_canary, destroy_instance — todos via traefik-manager.
  • canary_scheduler.update_traefik_weights + abort — idem.
  • Status/snapshot/misc read paths usam o novo crate.

CLI

Read commands (Plan 5)

runner traefik ganha 13 subcomandos read-only:

  • info, scan, schema <resource>
  • routers list, services list, middlewares list, domains list
  • inspect domain|router|service|file
  • doctor, history, diff
  • api (ping/routers/services/certs/overview)

Write commands (Plan 7)

16 subcomandos de escrita, todos dry-run por default, mutação só com --apply:

  • domain-add, domain-remove, domain-replace --scope single|all
  • router-create, router-delete, router-edit-rule
  • service-target, service-weight, service-bluegreen-init, service-promote
  • middleware-attach, middleware-detach
  • static get, static set-log-level, static set-access-log, static set-entry-point, static set-api
  • adopt, rollback, failover-switch
  • validate --apply agora aplica ops (era plan-only)

Saídas estruturadas

  • Envelope JSON padronizado: { success, timestamp, schema_version, client, command, data, error }.
  • 21 JSON Schemas v1 reais para cada resource de escrita (placeholders read continuam para vNext).
  • WriteOutcome: DryRun | Applied | Failed com mode/plan/report/response.

Cleanup

  • src/traefik/ deletado (1115 + 331 + 574 ≈ 2020 LOC removidos).
  • 3 validators de docker-fs (TraefikPathCheck + validate_dynamic_dir_*) migrados para traefik_manager::io::docker_fs.

Testes

  • 215 unit/integration tests em traefik-manager.
  • Suites de paridade: parity_local, parity_canary, parity_multi garantem semântica idêntica aos geradores legacy (validação byte-equivalent pós-normalização YAML).
  • Doctests verdes.
  • Runner: 764 tests (drop natural com a saída do src/traefik/tests.rs).

Compatibilidade

  • Sem breaking changes nos artefatos produzidos (parity tests garantem).
  • Comandos antigos não afetados — apenas o codepath interno trocou.

[2.39.0] - 2026-06-10 — `runner adopt`: recovery de registro a partir de container vivo (#36)

Item G do #35 (P7 do incidente #32). Substitui ~10 comandos manuais por app (playbook Fase 2) por um comando só, no caso residual sem nenhum backup (disco zerado, server novo, migração) com containers ainda vivos.

Adicionado

  • runner adopt <app> --from-container <c> (--json suportado):
    • Captura as envs do container vivo via Docker API e filtra os defaults da imagem (diff container × imagem: só fica o que foi injetado no run)
    • Classificação secret/plain via secret_detect: confiança ≥75% → .secrets, ≤35% → .env, faixa intermediária pergunta (com --yes, vai pra .secrets, conservador)
    • Modo cold (sem registro): exige --repo/--branch/--ckey, registra pelo mesmo caminho do runner add (sem deploy; containers não são tocados)
    • Modo warm (registro existe): reusa repo/token/ckey do state, só recaptura envs e re-encripta o bundle (flags de registro ignoradas com warning)
    • Grava .env (0644)/.secrets (0600) atômico, re-encripta .runner/secrets.enc e pusha o bundle (mesma regra do secrets reset-bundle)
    • State: status: running + current_version do label ccs.systems/version
    • Guards: container parado/inexistente → erro; warm com captura vazia → erro (não sobrescreve arquivos existentes com vazio); label ccs.systems/project divergente → confirmação; --json é não-interativo por contrato (exige --yes se houver prompt pendente)
    • Valores de env nunca aparecem em logs ou na tela sem máscara; --json emite um único objeto (sem valores)

Modificado

  • fetch::add_app e secrets reset-bundle ganharam modo quiet interno (usado pelo adopt pra manter o contrato de JSON único); comportamento dos comandos existentes inalterado. Push de bundle falho em modo quiet emite warning explícito.

[2.38.0] - 2026-06-10 — modelo `assets:` Fase 2: CLI + lifecycle + export (#28)

Completa o modelo de ativos persistentes (F1 no v2.37.0).

Adicionado

  • CLI runner assets (todos com --json):
    • list <app> — categorias, source, mount, tamanho, status de required (✓/✗ vazio)
    • upload <app> --to <source> <files...> — cópia server-local pro asset dir + chown automático (User do container corrente, non-root) + audit; --to precisa ser um source declarado no manifesto
    • snapshot <app> [-f file] / restore <app> [-i file] — tar de data/ (primitiva pra BackupAlly e migração entre servidores); restore confirma overwrite; --json no snapshot exige -f
    • clean <app> --category cache — limpa caches respeitando ttl: como filtro de idade (Nd/Nh; ttl inválido tipo 30m erra em vez de virar wipe total)
  • runner export --include-assets — embute o tar de data/ (assets_tar_b64) no bundle; falha de tar é erro explícito (não null silencioso).

Modificado

  • ⚠️ unregister agora protege assets: com data/ não-vazio e sem --force, bloqueia mostrando o tamanho + hint de snapshot (antes --json prosseguia silenciosamente e apagava tudo). Fluxos automatizados devem usar --force (como já era a prática).

[2.37.0] - 2026-06-10 — modelo `assets:` Fase 1: ativos persistentes com categorias (#28)

Originado no report da migração do Capital28 (#28): cada app usava um padrão diferente de persistência, os mounts automáticos eram não-documentados, e não havia validação de asset crítico (app subia healthy sem os arquivos OAuth — quebrava na 1ª chamada).

Adicionado

  • Campo assets: no manifesto — 4 categorias semânticas (açúcar tipado sobre volumes:, que segue válido):
    • secrets (operador sobe 1x; RO; required: true aborta o deploy se vazio, com o docs: na mensagem)
    • state (app escreve; RW; backup: é metadado pro BackupAlly — o runner não agenda)
    • cache (ephemeral; ttl: é metadado pra limpeza na Fase 2)
    • config (arquivo do repo na versão deployada; RO; atualiza no deploy)
    • Itens vivem em <app>/data/<source> (subtree já preservado pelo ciclo de vida).
  • Validação: schema (paths relativos sem .., mounts absolutos, colisões) no add/validate; required no pre-flight do deploy (pós-refresh do manifesto — valida o que vai rodar de fato).
  • chown automático: dirs novos de state/cache recebem o owner do User da imagem (non-root) — apps non-root não falham mais silenciosamente em escrita.
  • Sem overlap: mounts de assets entram nas destinations explícitas (auto-mounts data/logs/keys não duplicam).
  • Drift: assets: é operator-owned — não dispara manifest drift.
  • Docs: MANUAL/generate-config/content ganham a seção de assets, a documentação dos mounts automáticos data/logs/keys (antes implícitos) e a tabela de ciclo de vida verificada no código — incluindo: unregister apaga data/ (snapshot antes!); reset --hard preserva data/ (o help antigo era enganoso).

Fase 2 (anunciada, v2.38.0)

CLI runner assets list/upload/snapshot/restore/clean, proteção no unregister, export --include-assets.

[2.36.1] - 2026-06-10 — Spec 3: fecha o débito de observabilidade + data-safety (#35)

Corrigido

  • runner status não mascara mais state corrompido: erro de load_state (corrompido sem backup íntegro) vira aviso vermelho explícito (⚠ state: corrompido/ilegível — runner state restore <app>) + campo aditivo state_error no JSON, em vez de contadores zerados como se fossem dados.
  • Service-API GET /apps não dropa app com state ilegível: emite a entrada com status: "state_corrupt" (campos null) — o dashboard vê o app com problema em vez de contagem menor silenciosa.
  • Canary state com write atômico (atomic_write): fecha o último fs::write truncável de state (mesma classe do incidente #32).
  • Reconcile sem churn de metadata: regeneração que só muda o header (generated_at/runner_version) agora é "Already in sync" — sem write, sem commit, sem push (antes poluía o repo com commits de metadata a cada reconcile no-op).
  • PR deploy grava o state @pr-N (qualifier threaded pelo deploy() → mark_as_deployed_with_commit/mark_as_deploy_failed): sucesso, falha (retry counters agora funcionam pra PR) e short-circuit (re-deploy de PR healthy) — o state default de produção não é mais poluído com versão/contadores de PR.

Decisão registrada

  • Alertas de disco-baixo permanecem em severidade Error (wontfix deliberado — disco cheio foi o gatilho do incidente #32).

[2.36.0] - 2026-06-10 — detecção do "PORT fantasma" (env vs manifesto) (#34)

Incidente app.exemplo.com.br: PORT=3001 no .env sobrescrevia o port: 3000 do manifesto — container healthy (healthcheck usa $PORT), Traefik roteando pra 3000 → 503, nenhuma superfície revelava. Agora é detectado em 3 camadas.

Adicionado

  • runner env set avisa na introdução: ao gravar uma key que diverge do manifesto, imprime ⚠ AVISO imediato no stderr (ex.: PORT ≠ port: → explica que o Traefik continuará roteando pro manifesto). Nunca bloqueia.
  • runner doctor: check env_mismatch — varre os apps registrados comparando o env efetivo de runtime (.env + .secrets, secrets vencem) com o manifesto: mismatch de PORT (alertável) e divergência de valor em keys declaradas no environment: (informativo). Placeholders ${VAR}/vazios ignorados.
  • doctor --watch alerta PORT mismatch no Telegram: o check entra no fingerprint do watcher (mesma classe do disco baixo) — exit 2 + alerta com dedup; recovery quando corrigido.
  • Helper environment::env_manifest_mismatches (fonte única dos 3 pontos).

[2.35.3] - 2026-06-10 — Caso 2B (build.context) + state_health no list (#33)

Bugs encontrados pelo E2E do ciclo de drift no tester (que validou o loop do #33 #2 ponta a ponta: drift → reconcile --apply --yes → push upstream → fetch → deploy sem re-drift).

Corrigido

  • #33 Caso 2B — runner add resolvia build.context relativo ao manifesto (desde v2.26.1), enquanto o deploy copia o repo inteiro pra src/<ver>/ e o docker build resolve contra essa raiz. A divergência rejeitava o output do próprio wizard (context: ./web em web/.deploy.yml → procurava web/web, [semantic_build_context_missing]) e forçava hand-fixes (context: .) que então quebravam no build. O validator do add (e o advisory de monorepo) agora resolvem contra a raiz do clone, a mesma semântica do deploy. Manifestos hand-fixados pra . devem voltar ao valor repo-root-relative (runner reconcile --apply regenera certo).
  • runner list: state_health/repo liam o diretório de state errado (heurística "irmão do apps_dir" → /data/state) em instalação padrão (apps em /data/apps, state em /opt/runner/state) — todo app aparecia com ⚠ state:missing falso e repo: —. Agora usa o state_dir() do config.

[2.35.2] - 2026-06-09 — drift/reconcile: persiste, confirma, sem false-positive (#33)

Adicionado / Modificado

  • #2 — reconcile --apply agora faz commit + push do manifesto reconciliado pro repo, reusando o caminho de auth do secrets reset-bundle. Fecha o loop de drift (antes o cron fetch re-puxava o manifesto velho e o drift voltava). Flags --push/--no-push + config reconcile_auto_push.
    • ⚠️ Mudança de comportamento: reconcile_auto_push é default true — o auto-reconcile do cron passa a pushar manifestos pro repo automaticamente (escrita git não-assistida). Desligue com reconcile_auto_push: false no config ou --no-push.
  • #4 — reconcile --apply confirma antes de sobrescrever (mostra o diff + [y/N]); --yes/--force pula. Não-interativo (cron/pipe) sem --yes não escreve (evita reverter customização manual silenciosamente). Modo JSON exige --yes (prompt corromperia o stdout).
  • #1 — elimina drift false-positive em apps re-registrados de versões antigas: collect_manifest_drift agora canonicaliza build.args (${VAR}/vazio → equivalente), igual ao environment (#10C). Antes, manifesto antigo (KEY: "") vs mapper novo (KEY: ${VAR}) driftava sendo runtime-equivalente.

Nota

  • #3 (mostrar QUAL field difere) já estava implementado para manifest drift desde antes (bloco "Fields divergentes"); nada a fazer.

[2.35.1] - 2026-06-09 — publica pubkey de assinatura no CDN (#31)

Adicionado

  • #31 — pubkey.pub (Ed25519/minisign) publicada no CDN em {arch}/pubkey.pub + root (via GitOps scripts:). Fingerprint F4C67EA1D42B3D9A. Consumers (CCS subsystem_manage, e os clients implant/deployally/backupally) podem restaurar a verificação de assinatura de binários do runner. A pubkey é a mesma já embarcada (RUNNER_RELEASE_PUBKEY); um teste garante que pubkey.pub nunca diverge dela. README documenta verificação manual (minisign -Vp) + política de rotação. Sem mudança de comportamento do binário.

[2.35.0] - 2026-06-09 — robustez + observabilidade de state (#32 Spec 1+2)

Resposta ao incidente crítico de 2026-06-08 (#32): o disco encheu (100%, ENOSPC) durante o fetch --deploy do cron e o write não-atômico truncou os 10 state files a 0 bytes — ckeys/tokens/metadados perdidos (recovery só foi possível por fontes redundantes; não havia backup de state). Este release entrega a camada de data-safety (Spec 1) e a de observabilidade honesta (Spec 2): doctor/list que revelam a perda em vez de mascarar, watcher proativo de alerta, e playbook de recovery.

Adicionado

  • Write atômico de state (atomic_write: tempfile no mesmo FS → fsync → rename → fsync do dir). Em ENOSPC/EIO o arquivo original fica intacto. Substitui o fs::write truncante em write_state_file — todos os save_state herdam. Sozinho já teria evitado 100% do incidente.
  • Backup rolante com integridade + cadeia de rollback: antes de cada swap, a versão anterior íntegra é preservada em state/.backups/{app}/{UTC}-{sha12}.yml (sha no nome = auto-verificação), dedupe por sha, retenção state_backups_keep (default 5). load_state: primário corrompido → caminha a cadeia, restaura o 1º íntegro + warn; cadeia toda ruim → erro alto (nunca mais retorna default mascarando perda).
  • Comandos runner state backup / restore [--all] / list-backups (todos com --json). restore é não-destrutivo (salva o primário atual na cadeia antes de sobrescrever) e idempotente; --all só mexe em primários corrompidos e falha com exit-code não-zero se nada foi restaurado.
  • Pre-flight de disco no fetch (state_min_free_mb, default 500): aborta o ciclo com disk_space_low antes de tentar escrever num disco crítico.
  • Config: state_backups_keep e state_min_free_mb (defaults seguros; configs existentes seguem funcionando).

Modificado

  • Lock combinado por app (AppLock) substitui o DeployLock: um único lock em {state_dir}/{app}.lock cobre deploy e mutações de state (cron fetch, edit, pause/unpause, ckeys copy, deploy), via o helper update_state(closure). Reentrante process-global por path (seguro sob o runtime tokio multi-thread — o deploy segura o lock através de .awaits e re-entra no mark_as_deployed sem stall). Serializa a corrida cron-vs-manual; cross-process via flock.
  • #29 — docker-build honra o WORKDIR do Dockerfile (não injeta mais -w /app); modo image (bind-mount) inalterado.

Observabilidade (Spec 2)

  • runner doctor honesto: novo check state_files detecta state vazio/corrompido (o sintoma que o doctor antes não via) e informa se há backup íntegro; o check mkey valida a master key (32 bytes) no path real (/opt/runner/.keys/master.key, via crypto::master_key_path()) — antes reportava "master key missing" falso checando o lugar errado; + check de disco.
  • runner list não-mascarante: campo aditivo state_health (ok|empty|corrupt|missing) + marcador ⚠ state: quando o state não bate com o container rodando. Antes, list mostrava tudo "OK" pelo container e escondia a perda.
  • runner doctor --watch (cron one-shot): roda os checks e alerta no Telegram (HealthAlert) na transição ok→problema, com dedup por fingerprint (.doctor-watch-state.json, re-alerta a cada watch_realert_hours, default 6h), recovery no problema→ok, exit≠0 em problema. Reusa o NotifyManager do deploy.
  • Timestamps humanos com rótulo UTC explícito (a confusão de TZ dificultou o diagnóstico do incidente).
  • Playbook de recovery (Restaurar em novo servidor): detecção via doctor/list/watch + recovery via runner state restore (ou Fase 1/2/3 quando não há backup).
  • ui::table mede largura visível (ANSI-aware) — alinha células coloridas (marcador novo + coluna STATUS).

[2.34.8] - 2026-06-09 — quick wins do triage de issues (#30, #19)

Corrigido

  • #30 — runner -V/--version agora é só o semver (runner 2.34.8), sem o sufixo (hash) que fazia parsers last-token (ex: CCS subsystem_manage) lerem o commit em vez da versão e quebrarem o update. Alinha com os outros binários CCS (ccs/implant/deployally). A proveniência de build (tag:/commit:) continua em runner version e em runner version --json.
  • #19 — pre-flight do deploy valida pull/ (onde ficam Dockerfile/package.json/src) em vez da raiz gerenciada do app. Antes, todo deploy type: docker-build (bind-mount) reportava erros estruturais espúrios (build_dockerfile_missing, version-file) que entravam só na contagem do resumo e confundiam o operador. O deploy sempre funcionou; era ruído de validação.

[2.34.7] - 2026-06-09

Modificado

  • O bloco Next steps: do output adota o mesmo guia de trilho │ dos blocos de detalhe.

[2.34.6] - 2026-06-09 — output mais legivel

Modificado

  • Erros saem com a tag ERROR (vermelho), alinhada as linhas INFO/ WARN. Antes o erro era impresso como Error: ... sem cor, facil de nao perceber no meio do output.
  • Blocos de detalhe (status, health, tokens test, canary status, debug last-error) ganham um guia de trilho │ a esquerda + labels esmaecidos, com uma linha em branco antes, pra localizar o bloco sob o header [OK]/[FAIL].

Corrigido

  • runner -V exibe a tag git apenas quando ela bate com a versao do build (antes podia mostrar a tag da release anterior).

[2.34.1] - 2026-06-06 — yank dos comandos templates e apikey

Os comandos runner templates e runner apikey introduzidos em 2.34.0 foram removidos por estarem no escopo errado: o catálogo de templates e o ciclo de vida de API keys são responsabilidade do deployally, não do runner. Os dois mundos são contextos independentes — o CCS é o único agregador. Esses comandos vão portar pro deployally v3 em release separado do deployally client.

O envelope JSON canônico v2.0, o streaming JSONL pra deploy/fetch e os comandos portados (list, status, deploy, fetch, reconcile, ...) ficam intactos.

Removido

  • runner templates {list, locate, check, identify} — vão portar pro deployally v3.
  • runner apikey {create, list, show, rotate, revoke} — vão portar pro deployally v3.
  • Audit log /opt/runner/logs/apikey-events.jsonl — não é mais escrito pelo runner (passa pra responsabilidade do deployally).

Mantido da v2.34.0

Envelope canonico v2.0 (`-o json`)

Toda resposta one-shot vira um unico objeto JSON com campos garantidos:

{
  "success": true,
  "timestamp": "2026-06-06T14:23:45Z",
  "schema_version": "2.0",
  "client": { "name": "runner", "version": "2.34.1" },
  "command": "list",
  "data": { ... }
}

Em erro, success: false, data omitido, bloco error: { code, message, details } com codigo estavel (container_not_found, validation_failed, auth_required, ...). Detalhes em Output formats.

Streaming JSONL (deploy, fetch, validate, reconcile)

Comandos longos emitem uma linha JSON por evento em -o json, tagueada por event_type:

  • deploy_phase (pull/create/start/healthcheck/cleanup/rollback)
  • fetch_progress (resolve/download/extract)
  • validate_step (info/warning/error por step)
  • reconcile_step (drift detectado + acao)

Cada evento e parseavel independente (jq -c, line-by-line Python). Ordem de emissao preservada.

Deprecado

  • --json por subcomando (runner fetch --json, runner stage list --json) fica deprecated em favor do flag global -o json / --json. Continua aceito com warning em stderr ate v3.0.0.
# Antes
runner fetch --deploy --json

# Depois
runner -o json fetch --deploy

Migracao

Atualizar crons:

# /etc/cron.d/runner
*/5 * * * * /opt/runner/runner -o json fetch --deploy >> /var/log/runner-fetch.json 2>&1

Scripts que parseavam o output antigo continuam funcionando (envelope v2.0 inclui os mesmos campos top-level), mas ganharam metadados novos (schema_version, client, timestamp).

[2.33.1] - 2026-06-04

Corrigido

  • runner --version reportava um commit de build desatualizado no hash em algumas releases. Agora reflete o commit correto. Sem mudanca funcional.

[2.33.0] - 2026-06-03 — classificacao automatica de env/secrets

O runner wizard e o runner add passam a classificar environment: e secrets: sozinhos ao gerar o .deploy.yml, sem perguntar variavel por variavel.

Adicionado

Classificacao automatica de env/secrets

Ao gerar o .deploy.yml, o Runner decide sozinho quais variaveis sao environment: (plain) e quais sao secrets:, e como resolver cada uma. Deterministico, sem IA. Detalhes em Classificacao automatica de env/secrets.

  • Prefixo publico (NEXT_PUBLIC_*, VITE_*, REACT_APP_*...) vai para environment:, mesmo com KEY no nome. Nomes de secret vao para secrets:.
  • Auto-geravel (JWT_SECRET, APP_KEY...) vira ${GENERATE:hex:64}. Externo (STRIPE_SECRET_KEY, SMTP_PASSWORD...) vira {{::KEY}} + required:. Banco e ambiguos: pergunta no modo interativo, ou prompt + required: no nao-interativo.
  • Secret auto-geravel gera valor novo para producao, sem herdar o valor de desenvolvimento.
  • Tela de revisao consolidada no modo interativo, e comentarios # DETECTED: documentando cada decisao no manifesto.

Bloco `required:` e fail-fast

Secrets externos sem valor entram em required:. O deploy aborta cedo, antes de tocar o Docker, se alguma variavel obrigatoria faltar ou estiver vazia.

Corrigido

  • runner wizard em repo com Dockerfile agora gera type: docker-build + build:. Antes gerava image: com a imagem base e nao buildava a app.
  • O .deploy.yml gerado pelo wizard agora inclui o bloco version:. Antes o deploy falhava com version.file is empty.
  • runner add nao gera mais secrets como placeholder vazio (KEY: ""). Passa a usar a classificacao automatica.

[2.32.0] - 2026-06-03 — UX report de um cliente (8 fixes)

Onboarding de monorepo pnpm com 2 apps (Fastify API + Next.js standalone) produziu um UX report detalhado de 11 atritos. Esta release endereca 8 deles via fix de codigo (3 ja estavam OK e operadores apenas nao sabiam).

Adicionado

Multi-service deploy (P0)

runner add --from-compose <compose-path> bulk-registra todas as apps do compose num call. Detecta services via wizard discovery, requer manifests em <service>/.deploy.yml, gera um ckey shared automatico threadado pra cada runner add. Skip silencioso de infrastructure (redis, postgres, etc.).

runner wizard --from-compose docker-compose.yml --repo <repo> --branch main
git commit -am 'add manifests' && git push
runner add --repo <repo> --branch main --from-compose docker-compose.yml
# → registra N apps com 1 ckey shared, sem --ckey literal repetido

Manifest `required:` + pre-flight bail (P2)

Campo novo required: [VAR1, VAR2] no topo do .deploy.yml. runner deploy bailha fatal antes de tocar Docker se algum key falta ou esta vazio em .env/.secrets. Fim do healthcheck loop de 173s quando operador esquece de setar uma var obrigatoria.

required:
  - NEXT_PUBLIC_SUPABASE_ANON_KEY
  - DB_PASSWORD

runner env validate <app> sem --required cai no required: do manifest automaticamente. runner deploy --allow-missing-required bypassa pra emergencia.

Bulk env loader (P4 + P5)

runner env load <app> [--file <path>] carrega .env-format de uma vez. Sem --file, walks cwd → <app>/pull/ por candidates .env*/.secrets* (.env.example/.template/.sample excluidos). Classify cada key via detector (override com --all-secret/--all-plain).

Auto-sync de peers com ckey shared: walks state dir, fingerprint do ckey, propaga writes pra peers com fingerprint igual. Confirm prompt (--yes pra CI) + --no-propagate pra opt-out.

runner env load web-api --file ./prod.env --yes
# → carrega 10 keys + propaga pra web-front (mesmo ckey)

Auto-propagation de NEXT_PUBLIC_* (P9)

Apps frontend nao precisam mais declarar NEXT_PUBLIC_* duas vezes (build.args: E environment:). Keys cujo nome bate com prefixo publico conhecido — NEXT_PUBLIC_, VITE_, REACT_APP_, NUXT_PUBLIC_, EXPO_PUBLIC_, PUBLIC_ — automaticamente migram de build.args pro environment efetivo. Tokens privados (NPM_TOKEN, GH_TOKEN, DATABASE_URL) NAO propagados — ficam confinados ao build. environment: explicito sempre ganha.

Modificado

`runner inspect-args` redacta secrets (P6)

Antes printava DB_PASSWORD={{::DB_PASSWORD}} literal. Agora:

Default (sem --insecure) Com --insecure
plain → valor verbatim plain → valor verbatim
secret → <set> secret → valor real
missing + default no YAML → default mesmo
missing + sem default → <empty> mesmo

Reusa o global --insecure (mesmo flag que ckeys reveal).

`bootstrap_push` respeita `--manifest-path` (P1)

Operador registra apps/api/.deploy.yml com --manifest-path e o upstream agora recebe a copia em apps/api/.deploy.yml (nao em .deploy.yml raiz duplicado como antes). Fim do runner-bot empurrando manifest duplicates.

Advisories nao-fatais no `runner add`

DNS sanity check (P10)

Walks every instance domain, resolve via system resolver, detecta IP publico do servidor via ipify/ifconfig.me (timeout 2s). WARN nao-fatal quando: domain nao resolve (NXDOMAIN) ou resolve pra IP diferente. Match e test domains (.invalid/.test/.local/wildcards) ficam silent. --skip-dns-check pra CI / quando operador sabe que DNS ainda esta sendo configurado.

Monorepo build.context (P7)

Detection convention-based: se repo tem marker no root (pnpm-workspace.yaml, turbo.json, lerna.json, nx.json, rush.json) E build.context canonicalizado != root → WARN nao-fatal apontando pro fix tipico (build.context: ../.. quando manifest esta em apps/<svc>/.deploy.yml).

Itens do report que ja estavam OK (sem fix necessario)

  • P3 lock conflict cron vs manual deploy: erro claro em pipeline/lock.rs:104-156, cron silencia gracefully.
  • P8 DNS alias estavel web → api: network_aliases: existe desde v2.24.0. Operadores nao sabiam.

Suite

853 tests passing (+~63 desde v2.31.0).

Detalhes tecnicos

Changelog completo de boundaries (modules novos: dns_check, monorepo_check, environment::load, commands::env_load, fetch::from_compose) no CHANGELOG.md do client.


[2.31.0] - 2026-05-29 — Wizard UX refactor

Reescrita do runner wizard pra corte de fluxo e UX clara. Greenfield Dockerfile-only agora vira deploy em 2 perguntas (target <60s).

Adicionado

  • Pre-flight gate antes de qualquer pergunta. Bail com hint actionable quando repo não pode ser deployado (sem Dockerfile + sem compose, sem .git, manifest existe sem --force-overwrite).
  • Q0 source discovery: prefere --path, fallback --repo clone, fallback cwd com .git, último recurso prompt interativo.
  • Token discovery em 4 tiers: --token > $GITHUB_TOKEN > vault (runner tokens) > prompt interativo.
  • ? em qualquer prompt mostra explicação curta inline (mata o "preciso conhecer o Runner pra responder").
  • Handoff estruturado no fim: arquivos gerados, ckey + backup hint, dois caminhos pra mexer em secret (CLI ou encoder web), próximos passos.
  • --resume flag pra retomar wizard interrompido (lê .runner/wizard-answers.yml v2).

Modificado

  • Set de perguntas reduzido a 5 (Q1: project, Q2: instance+domain, Q3 condicional: multi-service picker, Q4 condicional: placeholders, Q5 condicional: ckey strategy).
  • Managed assets (redis/mysql/postgres) no compose multi-service ficam mostrados mas NÃO pré-selecionados.
  • runner add força force_overwrite=true no wizard interno (clone fresh sempre substitui).

Compatibilidade

  • --accept-defaults continua funcionando (non-interactive).
  • --from-compose continua funcionando.
  • runner add interno usa o novo wizard sem regressão.
  • Web wizard schema preservado.
  • wizard-answers.yml v1 carrega normalmente; salva como v2 no próximo write.

[2.30.13] - 2026-05-29 — Fix: `{{::AppDomain?}}` agora expande em runtime

Bug correlato à v2.30.12: o placeholder {{::Var?default}} do wizard não expandia em runtime quando aparecia em instances.<X>.domain. v2.30.12 silenciou o vazamento na notificação; v2.30.13 corrige a raiz.

Corrigido

  • InstanceConfig::effective_routes_resolved (novo método em src/config/mod.rs) — resolve {{::Var?default}} em cada domain contra .env + .secrets da app. Routes que resolvem pra string vazia são DROPADAS em vez de gerar Host(\{{::AppDomain?}}`)` literal que nunca matcha.
  • Traefik routing (pipeline/container.rs TraefikMode::Local e External) — agora chama effective_routes_resolved em vez de effective_routes. Antes, app com domain: "{{::AppDomain?}}" sem AppDomain no .env deployava OK, container subia healthy, mas Traefik não matchava e a app ficava 404 em produção — silent miscompile.
  • Notificação Telegram (pipeline/deploy_single.rs) — notify_domain resolve antes de chegar no event payload. Fallback: se resolução erra ou ainda contém {{::, omite a linha em vez de vazar o template.

Erro mais útil

Quando o placeholder não resolve e não tem default, o runner agora aborta com mensagem actionable em vez de deployar broken:

Instance 'X' has no domain or routes configured (after resolving
{{::Var?...}} templates against .env/.secrets). Either set the variable
(`runner env set <app> --key <K> --value <V>`) or remove the template
from `.deploy.yml`.

Testes

5 testes de regressão em src/config/tests.rs cobrindo: drop quando placeholder + sem env + sem default; resolve via .env value; resolve via template default; domínio literal preservado; path_prefix mantido após resolução. Total: 742 passing.


[2.30.12] - 2026-05-28 — Notificações redesenhadas (env badge + branch + PR detection + TTL)

Operadores rodando multiplos projetos num único canal Telegram não conseguiam distinguir ambientes/branches/PRs no fluxo de mensagens. Esta release redesenha o template pra colocar o sinal visual logo na primeira linha.

Adicionado

  • Badge de ambiente na primeira linha: 🟢 PROD, 🟡 STAGING, 🔵 DEV, 🟣 PR-N, ⚪ <outro>. Classificação automática a partir do nome da instance (production/prod → PROD; staging-*/homolog → STAGING; dev*/development → DEV; pr-N ou pr_number no state → PR-N).
  • Linha 🌿 branch mostrando de qual branch saiu o deploy (lido do state).
  • Detecção de origem PR via regex no commit subject — distingue:
    • PR #N — merge commit padrão do GitHub
    • PR #N — squash merge (trailer (#N) no subject)
    • PR #N (preview) — PR deploy isolado (runner add --pr N)
    • ⚠️ direct push — empurrou direto na branch (provavelmente pulou review)
  • Linha ⏰ Auto-destroy em Xh só para PR deploys com TTL ativo. Calculado em runtime a partir de state.created_at + state.ttl_hours.
  • Novo evento PrExpired disparado pelo runner cleanup --expired quando destrói um PR vencido. Avisa o dev que o ambiente que ele estava testando sumiu (vs ele descobrir via 404).

Modificado

  • Versão compacta: 🏷️ v1.2.3 ← v1.2.2 (era duas linhas separadas).
  • Casing dos títulos: sentence-case (Deploy concluído em vez de Deploy Concluído).
  • Domínio com placeholder do wizard: se o template contém {{...}} (placeholder não expandido em runtime), a linha 🔗 Domínio é OMITIDA em vez de aparecer literal no Telegram.

Bug correlato (separado, próxima release)

  • Placeholders {{::AppDomain?}} do wizard ainda não expandem em runtime quando lidos do .deploy.yml. v2.30.12 silencia a linha pra evitar UX confusa; o fix da expansão fica pra v2.30.13.

[2.30.11] - 2026-05-27 — Fix: deploy quebra com `version.file=""`

Fecha bug crítico reportado pelo um cliente (backend-laravel): deploy falha com Failed to read version file: ".../pull/" — Is a directory (os error 21) quando o .deploy.yml tem version: { source: file, file: "" }. Bloqueia QUALQUER deploy da app.

Corrigido

  • pipeline/prepare.rs::extract_version ganha o guard contra version.file vazio / . / ./ que fetch/git.rs::extract_version_with_diagnostic já tinha desde v2.23.19. Antes, app_path.join("pull").join("") resolvia pra pull/ (diretório), exists() retornava true, e read_to_string quebrava com o erro opaco "Is a directory (os error 21)". Agora bail!a antes do read com mensagem actionable: version.file is empty or '.' — set a concrete file in .deploy.yml or change to version.source: git-commit.
  • Defense-in-depth: troca exists() → is_file() em todos os candidate paths (pull/, src//current/, root, legacy current/) pra nunca passar um diretório pra read_to_string.
  • Wizard (wizard/compose/mapper.rs) para de gerar file: "" em manifests novos. Agora escreve file: "package.json" (o default do serde) mesmo quando source: git-commit. Previne o footgun se o operator editar source depois.

Como atualizar

runner self-update
runner -V   # confirma 2.30.11
runner deploy <app> --force   # tenta de novo

Bugs fechados

  • Report do um cliente: deploy de backend-laravel (Laravel) falhava em version detection independente do version.source configurado.

[2.26.10] - 2026-05-22 — Bug 19 layer 2: wizard preserva `${VAR}` literal

Fecha o caso de borda final do Bug 19, surfaceado durante validacao end-to-end no tester.

Modificado

  • expand_compose_vars no wizard mapper — bare ${VAR} agora preserva o literal quando shell env nao tem o valor, em vez de substituir por string vazia (o que docker compose faz no parse-time). Razao: runner tem ciclo de vida por-app (runner env set / runner secrets set setam valores APOS a geracao do manifest). Preservando o literal, a runtime expansion (deploy-time, v2.26.9) tem chance de substituir contra .env/.secrets. Pre-fix, reconcile destruia esses literais (virava args: K: '') e o silent miscompile do Bug 19 voltava.

Divergencia da spec docker-compose

docker compose substitui empty pra vars unset (parse-time strict). Runner agora preserva o literal nesse caso. Justificado pelo ciclo de vida por-app que docker compose nao tem. Bake-in continua funcionando: shell env setado antes de runner wizard --from-compose ainda e substituido no manifest.


[2.26.9] - 2026-05-22 — Bug 19: `build.args` propagation + `${VAR}` expansion

Fecha Bug 19 reportado por um cliente 24h apos o cutover do video completar: bundles Vite/Next/CRA buildados sem os VITE_* build-args, app crasha no browser (Invalid supabaseUrl).

Adicionado

  • Wizard expansion em build.args (mapper.rs::map_image_or_build) — aplica expand_compose_vars() em cada valor de build.args, paridade com a v2.26.3 (Bug 11) que ja cobria environment:. Novos manifests gerados pelo runner wizard --from-compose saem com ${VAR} substituido pelos valores do shell env.
  • Runtime expansion em build.args (container.rs::start_container) — apos a resolucao de {{::Var}} existente, aplica tambem expand_compose_vars_from_app() (helper novo em environment::) para substituir ${VAR} contra .secrets > .env > shell env. Manifests committados pre-v2.26.9 (com literais ${VITE_*}) sao expandidos transparentemente no deploy.

Precedencia de resolucao em build-time

  1. {{::Var}} → estado do runner (.env/.secrets/CLI overrides) — pre-existente.
  2. ${VAR} → .secrets > .env > shell env — novo em v2.26.9.
  3. Literal — passthrough.

Por que dois layers

Wizard expansion = paridade com environment: (Bug 11 pattern). Runtime expansion = lifecycle por-app: operador pode setar valor via runner env set APOS gerar manifest, e o deploy pega.


[2.26.8] - 2026-05-21 — Orphan bundle auto-recover + git fetch token

Fecha os 2 ultimos workarounds manuais que apareceram no cutover end-to-end do video. Combinado com a v2.26.7, o ciclo runner add + secrets set + reconcile + deploy agora roda 100% scriptado — sem rm, sem git remote set-url, sem sed.

Corrigido

  • Orphan secrets.enc auto-rename — try_restore_from_repo (chamado pelo runner add) agora renomeia o bundle indecifravel pra secrets.enc.orphan-<timestamp> quando a ckey nao bate. Pre-fix, o bundle ficava no lugar e as proximas chamadas runner secrets set/generate/paste falhavam com aead::Error (Bug 2 v2.26.1 so deixou a mensagem descritiva, sem auto-recovery). Plaintext .env/.secrets ficam intocados. Operator pode recuperar renomeando de volta se achar a ckey original.
  • git fetch propaga token via GitAuthHelper — fetch_to_pull (Phase 0 do deploy) e fetch_single_app (CLI runner fetch, caminho legado com app_path/.git) agora usam git_fetch_secure(auth, ...) que injeta GIT_ASKPASS. Pre-fix, o pull/.git/config tinha o placeholder https://[email protected]/... sem password real → GIT_TERMINAL_PROMPT=0 fazia fetch de repo privado falhar silenciosamente. Resolucao de token: state.github_token → decrypt → fallback GITHUB_TOKEN env.

Adicionado

  • git_fetch_secure(auth, repo_path, branch) em pipeline/prepare.rs — variante autenticada do git_fetch. Use em todos os call sites de fetch contra remote privado.

[2.26.7] - 2026-05-21 — Manifest-vs-mapper drift check (Bug 12 final fix)

Fecha a classe inteira de "Bug 12 reentry" (operator deploya container em network errada porque manifest esta stale relativo ao wizard-answers.yml). 7 tentativas de cutover do video expuseram este caso recorrente.

Causa raiz

O drift check existente (check_compose_drift_or_halt) so comparava o hash do compose service contra o stored hash do manifest. Isso pega quando docker-compose.yml muda estruturalmente, mas NAO pega quando:

  • wizard-answers.yml muda (project_prefix adicionado, system retypado, domain editado) e o compose service fica byte-identico.
  • Manifest foi committado por uma versao antiga do runner (pre-v2.26.3, antes do project-prefix) e operator nunca rodou reconcile.

Resultado real: cutover do video teve manifest com networks: [video] (pre-prefix) committado no upstream + wizard-answers.yml com project_prefix: video. Mapper produziria [video_video] mas o compose-hash batia → container caia em video (errado, sem alias entre services).

Adicionado

  • check_manifest_matches_mapper() roda apos o compose-hash check existente. Re-roda map_service com o wizard-answers.yml atual, compara cada campo mapper-derivado do InstanceDeployConfig com o que esta no disco, halta com diff por campo + comando exato de fix.

Exemplo de halt

Error: manifest drift detected for service 'agent' — re-running the wizard mapper
with the current `wizard-answers.yml` would produce a DIFFERENT `.deploy.yml`
than what's on disk.

Fields divergentes (current → expected):
  port:
    current:  8787
    expected: 80
  networks:
    current:  ["video"]
    expected: ["video_video"]
  network_aliases:
    current:  {}
    expected: {"video_video": ["agent"]}
  environment:
    (... lista completa de chaves ...)

Fix: runner reconcile video-agent --apply

Campos checados (27)

project, system, app_type, image, build, port, networks, network_aliases, volumes, ports, environment, expose, dns, tmpfs, devices, gpus, platform, entrypoint, command, hostname, init, stop_signal, stop_grace_period, pull_policy, resources, security, logging.

Campos explicitamente NAO checados (operator-customizable)

secrets, config_templates, hosts, env_sync, source_dir.

Tests

570/570 verde, com 2 testes novos reproduzindo o cenario video:

  • halts_when_manifest_has_pre_prefix_network_but_answers_has_project_prefix — asserta halt + diff menciona networks + comando runner reconcile.
  • passes_when_manifest_matches_mapper_output — asserta zero false-positive quando manifest = mapper output.

[2.26.4] - 2026-05-21 — Multi-deploy lifecycle (Bugs 12 re-open + 14, 16, 17)

Fecha bugs do ciclo "reconcile → deploy → reconcile" que apareceram durante a 4a tentativa de cutover do video (de um cliente). Reportado em /opt/runner/BUGS_REPORT_video_v2.26.3_attempt_2026-05-21.md.

Corrigido

  • Bug 12 (re-open) — runner reconcile --apply agora sobrevive ao git reset --hard que o proximo runner deploy faz em pull/. Pos-reset, se app_path/.deploy.yml tem structure_hash_v2 que bate com o fingerprint do compose atual, ele e copiado de volta sobre pull/<manifest> antes do refresh. Hash-gated: updates legitimos no upstream (operador commitou novo manifest) continuam ganhando.

    Sintoma pre-fix: numa cadeia de deploys multi-service (livekit → agent → frontend), o primeiro service entrava na network correta (<prefix>_<net>) mas os subsequentes pegavam a network velha (<net> sem prefix) porque o reconcile deles tinha sido desfeito pelo fetch.

  • Bug 14 — runner deploy nao limpa mais pull/ no fim. Pre-fix, "Cleaned up pull/ directory" rodava sempre, quebrando o proximo runner reconcile (que precisa de pull/ pra ler o compose). Loop: deploy → pull gone → reconcile fails → fetch bootstraps slowly → repeat. Pull/ agora persiste entre deploys; fetch reusa via git fetch + git reset (incremental, cheap). Economia de disco (1 shallow clone) nao compensava o custo operacional.

  • Bug 17 — Quando um first-deploy falha, o container quebrado e removido ANTES do runner deploy retornar erro. Pre-fix, ficava UP segurando portas e bloqueava rollback pro docker compose up (Bind for 0.0.0.0:PORT failed: port is already allocated). Removal e best-effort (warn-only se falhar).

Adicionado

  • runner deploy --force-unlock (Bug 16) — remove .deploy.lock manualmente quando a stale-detection (PID gone OR > LOCK_MAX_AGE) nao dispara. A mensagem de erro do lock ja apontava pra essa flag desde versoes anteriores; agora ela existe de fato.

Cascata (Bug 15)

Bug 15 ("livekit-agents Invalid URL") deve sumir com Bug 12 re-open fixado — provavelmente era DNS fail mascarado pela lib do agent. Sera revalidado na proxima tentativa.

Bugs fechados

  • /opt/runner/BUGS_REPORT_video_v2.26.3_attempt_2026-05-21.md — Bugs 12 (re-open), 14, 16, 17. Bug 15 marcado pra revalidacao.

[2.26.3] - 2026-05-21 — Multi-service compose unblock (Bugs 9–13)

Fecha o cluster de bugs que travava cutover de monorepos com docker-compose multi-service. Reportado em /opt/runner/BUGS_REPORT_video_v2.26.2_attempt_2026-05-21.md (tentativa 3 de cutover do projeto um cliente video).

Adicionado

  • command: no schema do .deploy.yml (Bug 10) — aceita Vec<String> ou shell-string. Wizard --from-compose traduz compose.services.X.command automaticamente. Container pipeline passa pro docker run apos entrypoint:
    command:
      - --config
      - /etc/livekit.yaml
      - --bind
      - 0.0.0.0
  • Expansao compose-spec de variaveis (Bug 11) — wizard expande ${VAR}, ${VAR:-default}, ${VAR-default}, ${VAR:?error} lendo do shell env, exatamente como docker compose faria no parse. Pre-fix, a string literal ${AGENT_PORT:-8787} chegava no container e quebrava parsers que esperavam tipos primitivos.
  • Networks compartilhadas por project_prefix (Bug 12) — wizard project-scopa networks user-defined (video → <prefix>_meet, igual docker compose), com service-name registrado como DNS alias em cada network compartilhada. Apps do mesmo wizard run resolvem-se por nome curto. Networks reservadas (mysql, redis, postgres, mongo, ...) e built-ins (public, bridge, host) passam unchanged.

Modificado

  • Precedencia env: .env / .secrets (estado operador, local) agora sobrescrevem environment: (config-as-code, manifest) (Bug 13). Pre-fix, runner env set era no-op para qualquer chave declarada no manifest porque -e KEY=VAL sempre venceu --env-file. Manifest agora vale como default; operador override em prod ganha.
  • runner fetch resolve repo de state OR manifest (Bug 9, re-open) — apps type: image gerados pelo wizard frequentemente omitem github: no manifest (a imagem nao precisa de clone no build). --repo passado ao runner add escreve em state.repo, mas fetch ignorava isso e retornava no_git em silencio. Agora fetch resolve repo de state.repo OR .deploy.yml::github.repo (state ganha).

Bugs fechados

  • /opt/runner/BUGS_REPORT_video_v2.26.2_attempt_2026-05-21.md — Bugs 9 (re-open), 10, 11, 12, 13

[2.26.2] - 2026-05-21 — source_dir + volume + fetch fixes (Bugs 7/8/9)

Fecha bugs descobertos na 2a tentativa de cutover do video. Reportado em /opt/runner/BUGS_REPORT_video_v2.26.1_attempt_2026-05-21.md.

Modificado (breaking-ish, mas matches compose semantics)

  • source_dir: null agora usa pull/ inteiro (Bug 7) — pre-fix, image-mode apps com source_dir: null disparavam auto-detect que adivinhava errado (escolheu docs/ num repo com markdown na pasta, deixando src/<v>/ sem o codigo). Default agora = repo root, matching docker-compose. Heuristica antiga continua disponivel via source_dir: auto.

Corrigido

  • Volume bind path resolve a source_path (Bug 8) — ./livekit.prod.yaml:/etc/livekit.yaml agora aponta pra src/<instance>/<version>/livekit.prod.yaml (onde o source copy mora) quando o arquivo existe ali, com fallback pra app_path para runtime state dirs (./data, ./logs). Pre-fix, todos os caminhos relativos resolviam a app_path que esta vazio pos-copy — container recebia mount vazio.
  • Fetch fallback pra GITHUB_TOKEN env var (Bug 9, parcial) — quando state token e ausente ou indecifravel (apps registrados com --insecure, ou apos rotacao de mkey), fetch agora usa GITHUB_TOKEN do env como fallback. Hint acionavel ao falhar: runner edit <app> --token <pat>.

[2.26.1] - 2026-05-21 — Reconcile bug fixes (Bugs 1–5)

Fecha 5 bugs reportados em /opt/runner/BUGS_REPORT_reconcile_migration_2026-05-21.md durante 1a tentativa de cutover do video (de um cliente).

Corrigido

  • Bug 1: runner add --manifest-path agent/.deploy.yml agora valida build.dockerfile relativo ao subdir do manifest, nao ao clone root.
  • Bug 2: runner secrets set em bundle orfao (ckey rotacionada) agora imprime erro descritivo com 2 caminhos de recovery em vez de aead::Error cru.
  • Bug 3: runner fetch faz clone inicial de pull/ quando ausente (apps em estado no_git apos --insecure ou falha de initial_pull_clone).
  • Bug 4 (critical): runner reconcile --apply agora escreve em ambos app_path/.deploy.yml e pull/<manifest>, e refresh_manifest_from_pull faz skip quando os bytes batem. Pre-fix, o reconcile era silenciosamente desfeito pelo proximo deploy.
  • Bug 5 (critical): runner reconcile agora detecta a lista de compose files usada na geracao via wizard-answers.yml::compose_files. Apps gerados com wizard --from-compose -f a -f b re-mergem automaticamente sem operador passar -f de novo.

Adicionado

  • WizardAnswers.compose_files: Vec<String> — persistido pelo wizard, lido por reconcile + drift check.

[2.26.0] - 2026-05-20 — Multi-file compose + fail-soft wizard + `--ignore-eccentric`

Fecha os bugs do report /opt/runner/BUGS_REPORT_compose_2026-05-20.md que sobraram apos v2.25.1:

  • Bug 1 (multi-file compose) — runner wizard --from-compose agora aceita -f file.yml -f override.yml, com semantica de merge identica ao docker compose (override last wins, suporta !override, !reset).
  • Bug 2 (HALT total) — Halt eh agora per-service por default (fail-soft). Service risky com privileged: true nao bloqueia mais o manifest do service clean. Flag --strict restaura o comportamento halt-all-or-nothing da v2.25.
  • --ignore-eccentric — escape hatch para "eu sei o que estou fazendo, gera mesmo assim". Aceita lista comma-separated de field names ou all.

Adicionado

  • -f / --compose-file <PATH> repeatable na CLI. runner wizard --from-compose -f base.yml -f prod.yml mescla per compose-spec antes da discovery.
  • Auto-detect de docker-compose.override.{yml,yaml} ao lado da base: se existir e nenhum -f for passado, o override é auto-mesclado com notice em stderr ("Detected override: X — auto-merging (pass -f explicitly to skip)").
  • Engine de merge (wizard/compose/merge.rs): deep merge de maps, concat de sequencias por default, replace com !override, drop com !reset, scalars later-wins.
  • --ignore-eccentric <fields> — fields como privileged,sysctls,group_add ou all.
  • --strict — restaura halt-all-or-nothing.
  • ComposeRunResult ganha eccentric_halts: Vec<(String, Vec<EccentricFinding>)> populado mesmo quando manifests sao gerados (relata os services pulados).

Modificado

  • Halt fail-soft por default: comportamento previo (v2.25) era halt-all-or-nothing — qualquer service com eccentric finding bloqueava o batch inteiro. Agora services com halts sao listados em eccentric_halts e os services limpos seguem para a geracao normal. Migration path: quem dependia do comportamento antigo (ex: CI que verificava 0 manifests on halt) adiciona --strict.
  • ComposeRunOptions.compose_file: Option<PathBuf> esta deprecated (mantido para compat interna). Use compose_files: Vec<PathBuf> ou deixe vazio pra auto-detect.

Compatibilidade

  • v1 fingerprint baseline test inalterado.
  • 6 fixtures de regressao v2.24 inalterados.
  • Single-file compose (sem -f, sem override sibling) → output identico a v2.25.x.
  • Apps geradas em v2.25.x com structure_hash_v2: continuam funcionando, drift check inalterado.
  • 562 tests passam (+21 sobre v2.25.1).

Validacao

Bug report repro (operador de um cliente, repo video):

  • agent com deploy.resources.limits.{cpus, memory} + livekit-server + frontend (definido apenas no override) → 3 manifests gerados apos v2.26.0 (era 0 em v2.24.2).

[2.25.1] - 2026-05-20 — Wizard nao halta mais em cap_add/cap_drop/devices/deploy.resources

Fix critico: o eccentric check do runner wizard --from-compose ainda estava haltando em cap_add, cap_drop, devices e deploy.resources — campos que agora sao suportados nativamente pelo .deploy.yml em v2.25.0 (via security.cap_add, security.cap_drop, devices, resources + gpus). O check legado bloqueava o uso real da feature.

Corrigido

  • Removidos da lista de halt: cap_add, cap_drop, devices, deploy.resources.
  • Tests atualizados pra refletir o novo comportamento.
  • Continuam haltando (corretamente nao suportados): privileged, sysctls, network_mode: host, pid/ipc/uts/userns_mode, group_add, extra_hosts, profiles, extends, deploy.replicas, deploy.placement.

[2.25.0] - 2026-05-20 — Compose translator catches up to v2.24.0

runner wizard --from-compose agora traduz os 30+ campos novos da v2.24.0 que estavam sendo silenciosamente descartados.

Adicionado

  • Parser: leitura de mem_limit, mem_reservation, memswap_limit, cpus, cpu_shares, cpuset, pids_limit, shm_size, ulimits (formas single e {soft, hard}), read_only, security_opt, logging.driver/logging.options, hostname, dns, expose, stop_signal, stop_grace_period, pull_policy, platform. Bloco swarm deploy.resources.{limits,reservations} agora estruturado em vez de opaco.
  • Mapper: tradução completa pros blocos novos do .deploy.yml. Precedência: swarm deploy.resources.limits.{memory,cpus} vence mem_limit/cpus curtos quando ambos presentes.
  • no-new-privileges:true em security_opt é roteado pro campo bool security.no_new_privileges, não duplicado em security_opts.
  • GPU: deploy.resources.reservations.devices.driver: nvidia mapeia pra gpus: all (ou gpus: device=N quando count numérico).

Fingerprint v2

  • Novo structure_hash_v2 no header x-runner-source, substituindo structure_hash em manifestos gerados pelo wizard a partir do v2.25.0.
  • Apps antigas com structure_hash (v1) continuam usando o algoritmo v1 — zero risco de drift halt falso-positivo após upgrade.
  • v2 captura presença de cada bloco novo como booleano estrutural. Valores (ex: memory: 256m vs memory: 512m) NÃO entram no hash, seguindo o princípio v1 de "estrutura entra, tuning sai".
  • check_compose_drift_or_halt faz dispatch automatico: lê structure_hash_v2 se presente, senão cai pro structure_hash legado.

Migration

Apps geradas com v1 hash são upgradeadas naturalmente quando o operador roda runner reconcile <app> --apply. Output mostra fingerprint: upgrading v1 → v2 (new v2.24 fields now tracked). Sem urgência — apps v1 funcionam indefinidamente com o algoritmo legado.

Compatibilidade

  • Suite de regressão lockando v1 fingerprint contra fixture representativa garante zero regressão no algoritmo legado.
  • Apps .deploy.yml autoradas à mão (sem x-runner-source): inalteradas.
  • 541 tests passam (+17 sobre v2.24.2): parser, mapper, fingerprint v2, dispatcher.

[2.24.2] - 2026-05-20 — Help do CLI limpo

Patch UX: runner -h agora mostra uma linha curta por comando em vez de despejar exemplos e parágrafos. Os exemplos continuam disponíveis via runner <cmd> --help.

Modificado

  • Cada variante de Commands em cli/mod.rs ganhou um docstring de uma linha como primeira linha, seguido de blank line, depois detalhes/exemplos. Clap usa a primeira linha em -h e o bloco inteiro em --help.
  • --insecure global flag tambem ganhou linha curta no topo.
  • Comandos sem nenhum docstring (add, deploy, rollback, template, validate, upgrade, logs, stage, generate-config, instances, versions, weights, weight, promote, destroy, cleanup, migrate, canary, mcp, ports) ganharam descricao.

Antes vs Depois

Antes:

init             Examples: runner init runner init --set github.default_user=devborlot --set paths.apps_dir=/data/apps ...

Depois:

init             Initialize runner configuration (config.yml + master key)

Exemplos preservados em runner init --help.


[2.24.1] - 2026-05-20 — runner inspect-args

Patch: novo comando runner inspect-args <app> imprime o vetor de args que seria passado pro docker run no próximo deploy, sem executar nada. Read-only.

Adicionado

  • runner inspect-args <app> — preview dos args do docker run. Útil pra validar resources/security/logging/... da v2.24.0 antes de pushar pro dist branch.
  • Flag --instance <name> pra forçar instance específica.
  • Flag --json pra output como JSON array.

Uso

runner inspect-args meu-app
runner inspect-args meu-app --instance staging
runner inspect-args meu-app --json

Internamente reusa a mesma função (preview_run_args) que a suite de regressão usa pra lockar goldens — operador vê exatamente o que o runner emitiria.


[2.24.0] - 2026-05-20 — Container resources, security, logging & lifecycle

Release minor com pacote grande de controles que faltavam no .deploy.yml. 100% backward compat — defaults preservam byte-a-byte o comportamento da v2.23.x. Apps em produção sem alteração no .deploy.yml produzem docker run idêntico.

Adicionado

  • resources: — memory, memory_swap, memory_reservation, cpus, cpu_shares, cpuset, pids_limit, shm_size, ulimits (map). Limita RAM, CPU, PIDs.
  • security: — user, read_only, no_new_privileges, cap_add, cap_drop, security_opts. Hardening de container.
  • logging: — driver (json-file/journald/syslog/none) + options (max-size, max-file). Para rotação de logs em produção.
  • Lifecycle — init (default false), entrypoint, stop_signal, stop_grace_period, pull_policy (ausente = daemon default).
  • Network avançado — hostname, dns, expose, network_aliases (map network→aliases).
  • Storage / Hardware — tmpfs, devices, gpus, platform.

Compatibilidade

  • Schema parsing: todos os campos novos com #[serde(default)] ou Option<T>. .deploy.yml antigo carrega sem erro.
  • Runtime: ausente = sem flag emitido. App registrada hoje deploya idêntica a v2.23.x.
  • Suite de regressão lockando args do docker run de 5 fixtures de produção (app-front, app-sys, app-worker, ccs-scanner, minimal). Toda task de Wave A/B teve que manter goldens inalterados.
  • wizard/compose/fingerprint.rs intacto — apps geradas via runner wizard --from-compose mantém structure_hash estável (sem drift halt).

Side effect determinístico

Labels e env vars no docker run agora são emitidos em ordem alfabética (eram em ordem aleatória de HashMap). Docker não liga pra ordem, mas o output ficou determinístico — útil pra debug e diff.

Validação em campo

Smoketest: deploy de app-front com binary v2.24-dev. docker inspect HostConfig diff vs v2.23.20: vazio (exceto pela ordem de 2 bind volumes, causada por estado do disco entre deploys, não pelo binary).


[2.23.20] - 2026-05-20 — Auto-prune de dangling images + build cache

Bug em campo: servidores com muitos deploys em modo build: (Dockerfile) acumulavam imagens <none> (camadas intermediarias) e build cache do buildkit indefinidamente. Um docker system prune manual recuperou 23GB de um servidor. O cleanup_old_versions que ja rodava no phase_7 removia o {project}:{version} tagueado mas nao tocava em dangling/cache.

Adicionado

  • docker.auto_prune_dangling: bool no config.yml (default true). Apos a remocao das versoes antigas em apps build:, roda docker image prune -f. So remove untagged <none> sem container associado — seguro.
  • docker.auto_prune_build_cache: string no config.yml (default "72h"). Apos build-mode deploys, roda docker builder prune -a -f --filter until=72h. Aceita "72h", "168h", "7d", "all", ou vazio (desliga).
  • Logs Pruned dangling images — Total reclaimed space: XGB e Pruned build cache (until=72h) — Total: ... quando ha algo a remover.

Comportamento

Apps em modo image: (registry pull) nao sao afetadas — nao geram cache nem dangling local. So apps com build: no .deploy.yml disparam o prune.

Equivalencia manual

# Equivalente do que roda automatico em build-mode deploy:
docker image prune -f
docker builder prune -a -f --filter until=72h

Quem prefere controlar manualmente: auto_prune_dangling: false + auto_prune_build_cache: "" desligam e voce roda runner cleanup --images --build-cache 72h quando quiser.


[2.23.19] - 2026-05-19 — Bugfix triple no fetch reporter

Tres bugs identificados em campo no fetch --json/cron:

Corrigido

  • current_version: null em apps deployed — build_no_git_result em fetch/resolve.rs hardcodava current_version e container_version como None. Apps healthy reportavam null no log do cron pra ambos os campos, mesmo com runner list mostrando a versao correta. Agora popula do state.current_version e via docker inspect (get_running_container_version).
  • version config: cannot read '/data/apps/<app>/': Is a directory — extract_version_with_diagnostic em fetch/git.rs usava path.exists(), que aceita diretorios. Quando version.file no .deploy.yml era vazio ou ., o app_path.join("") resolvia pro proprio diretorio e o fs::read_to_string quebrava. Agora rejeita early com erro claro e usa path.is_file() em todos os candidate paths (defense-in-depth).
  • Apps em error loop infinito — quando um repo era removido do GitHub (caso print), o runner tentava git ls-remote a cada 5min e falhava sem nenhuma acao corretiva. Adicionado contador consecutive_fetch_errors no state que incrementa em cada erro de fetch e zera em sucesso. Quando cruza o threshold runner_config.max_consecutive_fetch_errors (default 10), o app vira status: paused e e skipado pelo cron.

Adicionado

  • runner unpause <app> — clear do estado pausado. Reseta consecutive_fetch_errors e last_error, volta status pra active. Use depois de corrigir o problema upstream (repo movido, network voltou).
  • max_consecutive_fetch_errors no config.yml — threshold global pra auto-pause. Default 10. Set 0 desabilita.
  • error agora populado em no_git results — quando o ls-remote falha, a mensagem de erro aparece no error: do FetchResult e em state.last_error (antes ficava so no log via warn!).

[2.23.18] - 2026-05-19 — Env sync: remoto soberano com hash check

O secrets.enc do repositorio agora e a fonte de verdade para .env/.secrets. Se o hash do secrets.enc remoto (em pull/.runner/secrets.enc) diferir do local, o runner decripta e sobrescreve .env/.secrets automaticamente durante o deploy.

Isso resolve o cenario onde secrets sao atualizados em outro servidor (ou via runner secrets set remoto) e o servidor local ficava com .env desatualizado indefinidamente.

Adicionado

  • env_sync no .deploy.yml (per-app): controla se .env/.secrets sao regenerados a partir do secrets.enc remoto.
    • remote (default): remoto e soberano — hash diferente = regenera .env/.secrets
    • skip: local e soberano — nunca sobrescreve .env/.secrets por mudanca remota
  • env_sync_default no config.yml (global): define o default para todas as apps no servidor. Util para servidores de staging onde .env e ajustado manualmente.
  • Phase 1.4 (sync_secrets_from_repo): novo passo no pipeline entre template rendering e env provisioning. Compara SHA256 do secrets.enc remoto vs local.

Comportamento

env_sync secrets.enc mudou? Resultado
remote Sim Decripta e sobrescreve .env/.secrets
remote Nao Nenhuma acao
skip Sim ou Nao Nenhuma acao (log informativo)

Precedencia

  1. Per-app env_sync: skip no .deploy.yml sempre vence
  2. Se per-app e remote (default/omitido), usa env_sync_default do config.yml
  3. Se ambos omitidos, comportamento e remote (retrocompativel com v2.23.17 para apps sem secrets.enc no repo)

Nota

.deploy.yml sempre sincroniza do remoto independente do env_sync — ele controla o contrato do deploy (portas, networks, healthcheck) e deve estar sempre atualizado.


[2.23.17] - 2026-05-19 — Fix: `phase_8_push_secrets` usa GIT_ASKPASS pro auth

Bug em campo (deploy app/sys+worker, prod): o último passo do pipeline (phase_8_push_secrets, sincronia opcional do .runner/secrets.enc de volta no repo quando o plaintext hash muda) chamava git push sem GIT_ASKPASS. Resultado:

[7/7] [ROUTING] Configurando Traefik...
Password for 'https://[email protected]':
 WARN git push failed: remote: Invalid username or token.
fatal: Authentication failed for 'https://github.com/usuario/app-sys.git/'

Git abria prompt interativo, GitHub rejeitava ("Password authentication is not supported for Git operations"), o phase falhava com warn (não-fatal) e o secrets.hash nunca era escrito. Resultado: todo deploy futuro tentava push de novo e falhava igual.

Fix

commit_and_push_secrets:

  • Recebe state_dir como parâmetro (phase_8_push_secrets passa via RunnerConfig).
  • Carrega state file do app, decrypta github_token via mkey.
  • Constrói GitAuthHelper com esse token e chama auth.git_command(&["push"]) em vez de Command::new("git").args(["push"]).
  • Sem token disponível: pula com warn sugerindo runner secrets push <app> manual.

Reusa a mesma infra de auth de bootstrap_push e runner secrets push — askpass via env var, token nunca em ps/git-config.

Impacto

  • Deploys pre v2.23.17 já completados continuam funcionais (containers rodando, Traefik configurado). Só o sync do bundle ficou pendente.
  • Após runner self-update, o próximo runner deploy --force consegue completar phase_8 sem precisar de intervenção manual.

[2.23.16] - 2026-05-18 — Wizard preserva ordem do `.deploy.yml` nos prompts

Patch UX: o wizard interativo (runner add em modo interativo) agora apresenta os prompts de environment: e secrets: na mesma ordem que aparecem no .deploy.yml. Antes saía em ordem aleatória do HashMap.

Bug

HashMap<String, String> no Rust randomiza ordem de iteração (anti hash-flooding). Quando o wizard fazia for (key, value) in environment pra emitir os prompts, as chaves saíam embaralhadas. Operador escrevia:

environment:
  BASE_DOMAIN: "{{::Base Domain?app.exemplo.com.br}}"
  MYSQL_HOST: "{{::MySQL Host?mysql}}"
  MYSQL_PORT: "{{::MySQL Port?3306}}"
  MYSQL_USER: "{{::MySQL User?app}}"
  MYSQL_DATABASE: "{{::MySQL Database?app}}"
  REDIS_HOST: "{{::Redis Host?redis}}"
  REDIS_PORT: "{{::Redis Port?6379}}"
  REDIS_DB: "{{::Redis DB?4}}"
  ADMIN_USER: "{{::Admin User?admin}}"

E o wizard pedia em ordem aleatória (MySQL Host, MySQL User, Admin User, Redis Port, Base Domain, ...). Difícil agrupar mentalmente "agora preencho todos os MYSQL_*, depois os REDIS_*" — cada campo aparecia em local imprevisível.

Fix

environment e secrets em InstanceDeployConfig agora são IndexMap<String, String> (crate indexmap com feature serde). IndexMap preserva ordem de inserção e o serde_yaml respeita a ordem do YAML no deserialize. Resultado: o for...in do wizard itera na sequência exata que o operador escreveu no .deploy.yml.

Outras estruturas (overrides, lookups internos) continuam HashMap — só o source-of-order virou IndexMap.


[2.23.15] - 2026-05-18 — Healthcheck: TCP fallback `/dev/tcp` + Traefik probe respeita `mode` e injeta `hostname`

Patch endereçando 2 bugs críticos reportados em produção (deploy de um backend Spring Boot), ambos família healthcheck.

Fix — TCP healthcheck com fallback `bash /dev/tcp` para Java/JRE Ubuntu

Bug: imagens Java/JRE baseadas em Ubuntu (ex.: eclipse-temurin:17-jre) têm bash mas não têm nc. O healthcheck TCP do runner gerava só sh -c 'nc -z 127.0.0.1 PORT', retornando exit 127 (command not found), container ficava unhealthy eternamente. Família relacionada ao fix de v2.23.12 (Alpine sem bash), agora cobrindo o outro lado: Ubuntu sem nc.

Fix: chain de fallback no comando emitido:

sh -c '(nc -z 127.0.0.1 PORT 2>/dev/null) || (timeout 2 bash -c "exec 3<>/dev/tcp/127.0.0.1/PORT" 2>/dev/null)'

Cobertura:

Base image nc bash Funciona
Alpine (busybox) ✓ ✗ sim, via nc
Debian/Ubuntu padrão ✗ ✓ sim, via /dev/tcp
eclipse-temurin:17-jre ✗ ✓ sim, via /dev/tcp
Python slim ✗ ✓ sim, via /dev/tcp
Distroless/scratch ✗ ✗ precisa mode: cmd ou HEALTHCHECK no Dockerfile

timeout 2 previne hang se /dev/tcp ficar esperando syn-ack indefinidamente.

Fix — Traefik dynamic respeita `mode: tcp` e injeta `hostname` correto

Bug parte a: mesmo com .deploy.yml declarando healthcheck.tcp: 8080, o runner gerava Traefik dynamic com healthCheck.path: /health HTTP. Backend TCP-only respondia 404 ao probe → Traefik marcava DOWN → 503 ao cliente.

Bug parte b: o probe HTTP do Traefik (quando legítimo) usava o nome do container como Host header (ex.: api_production_9b5c0b9:8080). Spring Boot/Tomcat são RFC 7230 strict — recusam Host com underscore retornando 400 → service marcado DOWN → 503 ao cliente. Cliente real passava porque vinha com Host: backend.exemplo.com.br (via passHostHeader).

Fix parte a: o pipeline agora usa resolve_mode() em vez de só checar path.is_empty(). Traefik só recebe healthCheck no modo HTTP; tcp/cmd/exit ficam sem probe Traefik (o HEALTHCHECK do container ainda valida internamente).

Fix parte b: adiciona campo hostname ao healthCheck do Traefik dynamic, e o runner injeta automaticamente o domain da instância:

  • Single-route: emite healthCheck.hostname: <domain>.
  • Multi-route: cada serviço usa o route.domain próprio (em monorepo com N rotas, cada uma tem Host diferente).
  • Canary: ambos stable e canary versions usam o mesmo domain.

YAML gerado (exemplo single-route):

http:
  services:
    api-production-v1-0-3:
      loadBalancer:
        servers:
          - url: http://api_production_9b5c0b9:8080
        healthCheck:
          path: /api/health
          interval: 10s
          timeout: 5s
          hostname: backend.exemplo.com.br   # ← v2.23.15

[2.23.14] - 2026-05-18 — `secrets push` preserva upstream + compose translator polish

Patch com 3 bug fixes — 1 crítico no secrets push reportado em campo, 2 de polish no wizard --from-compose detectados em field test (adspot).

Fix crítico — `runner secrets push` não sobrescreve mais `.deploy.yml` upstream

Bug: secrets push clonava o repo upstream e cegamente copiava o .deploy.yml local por cima do upstream antes de commitar. Operador que editou o manifest direto pelo GitHub web (ou de outra máquina) tinha suas edições silenciosamente revertidas pela cópia local stale.

Fix: o push agora detecta o estado do upstream:

Upstream tem .deploy.yml? Comportamento v2.23.13 Comportamento v2.23.14
Não (bootstrap) Copia local → seeda repo Copia local → seeda repo (igual)
Sim (operador editou) Sobrescreve com local (bug!) Preserva upstream (fix)

Quando upstream já tem .deploy.yml, secrets push foca estritamente em .runner/secrets.enc. A mensagem do commit reflete o que foi staged:

  • Bootstrap: chore: bootstrap .deploy.yml + encrypted secrets
  • Push só de secrets: chore: update encrypted secrets

Edits feitas pelo operador (GitHub web, branch local, outra máquina) sobrevivem.

Fix — `port` extraído de label Traefik quando `ports:` vazio

Containers que só roteiam via Traefik (sem publicar porta no host) tinham services.X.ports vazio no compose → mapper caía no default 80. O bug pegava qualquer Flask/Bun/API que escuta em porta não-80.

Agora o mapper procura traefik.http.services.<id>.loadbalancer.server.port=N em labels: como fallback. Resultado típico:

Service port v2.23.13 port v2.23.14
sys (Flask) 80 5000
api (Rust) 80 8080
app (Bun) 80 3000

Ports explícitos em services.X.ports: continuam ganhando — o label só preenche quando ports é ausente.

Fix — `project_prefix` resolve paths relativos

runner wizard --from-compose --path . passava ./docker-compose.yml que tem parent() = "." e file_name() == None. O fallback caía em monorepo hardcoded sempre. Agora canonicaliza o path antes:

v2.23.13: project_prefix: monorepo (sempre)
v2.23.14: project_prefix: <nome real do dir cwd>

Ainda pendentes

  • Extração de domain de labels Traefik Host(X) — precisa parser de rule mais cuidadoso (casos compostos Host(a) || Host(b)).
  • Heurísticas de guess_system pra "app" → front e imagens oven/bun → front.

Ambos são polish — operador interativo preenche via prompts.


[2.23.13] - 2026-05-18 — Compose translator + monorepo multi-sistema

Pacote grande de features focado em monorepos com múltiplos sistemas. Permite registrar N apps do mesmo repo apontando pra .deploy.ymls diferentes em subdirs, com geração assistida via runner wizard --from-compose que traduz docker-compose.yml em N manifestos.

Escopo fundamentado (gravado em pedra)

Runner é deployer single-host Docker. .deploy.yml é a linguagem nativa. Aceita docker-compose.yml como input alternativo via wizard porque o modelo mental coincide. Não aceita Kubernetes, Nomad, ECS — use Helm/Argo/Flux pra K8s.

Feature 1 — `build.target` no schema

build:
  context: .
  target: production    # ← novo, multi-stage Dockerfile
  args:
    NODE_VERSION: "22"

Mapeia 1:1 com docker build --target X.

Feature 2 — `runner add --manifest-path `

runner add --repo user/monorepo --branch main --manifest-path frontend/.deploy.yml
runner add --repo user/monorepo --branch main --manifest-path backend/.deploy.yml

Implica --force-duplicate. Mensagem do erro duplicate_repo agora menciona --manifest-path como opção válida.

Bonus fix: após git pull, o runner agora re-copia o manifest do pull/ pra app_path/.deploy.yml. Pré-fix essa cópia era estática no runner add e mudanças upstream em .deploy.yml eram silenciosamente ignoradas em todos os apps.

Feature 3 — `runner wizard --from-compose`

Tradutor que lê docker-compose.yml e gera N .deploy.yml em subdirs:

cd /path/to/monorepo
runner wizard --from-compose
# Discovery: lista services, classifica (app vs infra)
# Prompts: confirma quais wizardizar + pede domain/system por serviço
# Output: N .deploy.yml em subdirs + .runner/wizard-answers.yml

Cobertura prática: ~85% dos compose files reais.

Suportado: image/build (com target/args), command, environment, env_file, ports, volumes (bind), networks, depends_on (entre wizardizados), labels (Traefik), healthcheck.

Halt incondicional (security/correctness): cap_add, devices, privileged, sysctls, network_mode: host, build.secrets, build.platforms, deploy.replicas>1, etc.

Drop com warning (operational tuning): restart, init, tmpfs, build.cache_from/to.

Feature 4 — Drift check pré-deploy

.deploy.yml gerado carrega header x-runner-source com structure_hash. Antes de cada deploy, runner re-calcula hash do service no compose atual e HALT em mismatch — container atual fica rodando intocado.

$ runner deploy monorepo-backend
ERROR: compose drift detected for service 'backend':
  stored: sha256:a3f2...
  current: sha256:b9c1...

docker-compose.yml mudou estruturalmente desde a geração do manifest.
Container atual continua rodando intocado.

Próximos passos:
  1. Inspecionar mudança no compose
  2. Regenerar manifest: `runner reconcile <app>`
  3. Re-deployar

Feature 5 — `runner reconcile [--apply]`

Re-aligna .deploy.yml com docker-compose.yml atual:

runner reconcile monorepo-backend           # dry-run + report
runner reconcile monorepo-backend --apply   # escreve manifest
runner reconcile monorepo-backend --json    # output estruturado

Fluxo: lê manifest atual → carrega compose do pull/ → re-roda mapper com wizard-answers.yml cached → renderiza novo YAML → diff vs atual → aplica (com --apply) ou só reporta.

Limitações conhecidas

  • Canary não suportado em manifests compose-derived
  • docker-compose.override.yml fora do MVP
  • Sem 3-way merge se editar .deploy.yml à mão — reconcile sobrescreve. Edite o compose.
  • Named volumes dropam com warning — runner usa bind mounts

477 testes passando.


[2.23.12] - 2026-05-18 — Multi-route + Fix HEALTHCHECK Alpine-compat

Multi-route por instance

Container expondo múltiplos serviços (ex: backend na 8000 + front na 5173) agora pode rotear via Traefik para domínios distintos sem precisar splitar em dois containers. Novo campo routes: no .deploy.yml:

instances:
  production:
    routes:
      - domain: api.cliente.com
        port: 8000
      - domain: app.cliente.com
        port: 5173
        path_prefix: /admin   # opcional, por-route

Backward compat: domain: top-level continua valendo (vira a primeira route implícita).

Validação: rotas que compartilham domain precisam de path_prefix único.

Healthcheck composto: container HEALTHCHECK probe-a todas as portas, qualquer falha = unhealthy.

Cobertura: file provider, docker_labels e External — todos os três modos.

Limitação: canary não suportado em multi-route.

Fix — HEALTHCHECK quebrava em imagens Alpine

Containers Alpine ficavam eternamente unhealthy com bash: not found (ExitCode 127). Runner usava bash + /dev/tcp que não existem em Alpine. Trocado por wget → curl em sh POSIX:

# HTTP:
sh -c '(wget -q -O /dev/null http://127.0.0.1:PORT/PATH || curl -fsS http://127.0.0.1:PORT/PATH > /dev/null) 2>/dev/null'

# TCP:
sh -c 'nc -z 127.0.0.1 PORT'

Funciona em Alpine, Debian, Ubuntu, Rocky, distroless-with-shell. Para imagens distroless sem shell e FROM scratch: declare HEALTHCHECK no Dockerfile próprio.

Recomendação — `localhost` em healthchecks custom

Em Alpine, getent hosts localhost retorna ::1 antes de 127.0.0.1 e wget busybox prefere IPv6. Pra healthchecks que você escreve no Dockerfile ou em healthcheck.cmd: do .deploy.yml: sempre use 127.0.0.1 literal, nunca localhost. O runner já usa 127.0.0.1 em tudo que gera automaticamente.

405 testes passando.


[2.23.11] - 2026-05-18 — Fix REPO VER no `fetch` + `traefik.mode: docker_labels`

Fix — REPO VER reflete versão remota no `runner fetch`

A coluna REPO VER da tabela impressa por runner fetch [--deploy] mostrava a versão deployada em vez da versão que está no remoto (i.e., a versão que o próximo deploy vai publicar). Em apps no layout v1.2+ (sem .git no app root), o fetch só fazia git ls-remote e nunca baixava o version.file do remoto.

Correção: novo helper baixa version.file de raw.githubusercontent.com/{repo}/{commit}/{file} (com Authorization: Bearer quando há token), timeout 5s. Tabela agora prefere new_version/new_commit com fallback pra current_version/old_commit.

Feature — `traefik.mode: docker_labels`

Em /opt/runner/config.yml:

traefik:
  mode: docker_labels      # antes: local | external | none
  network: public

Resolve o caso de Traefik em network: host (que não tem DNS Docker). Em vez de escrever dynamic file, emite labels Traefik no container; o provider docker do Traefik descobre o IP via socket Docker. Bonus: routing reage a eventos start/stop do container automaticamente.

Requisitos no Traefik:

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false

E /var/run/docker.sock:/var/run/docker.sock:ro montado no container do Traefik.

Limitação: canary não suportado em docker_labels (weighted service não cabe em labels per-container).

390 testes passando.


[2.23.10] - 2026-05-16 — Quality: handlers do main quebrados em sub-funções

Continuação do refactor da v2.23.9. Cada handler extraído (cmd_debug, cmd_add, cmd_tokens, cmd_ckeys, cmd_edit, cmd_cleanup) foi quebrado em sub-funções nomeadas, levando CCN de cada um abaixo de 30:

  • cmd_cleanup (CCN 104) → 10 funções cleanup_*_phase + helpers
  • cmd_edit (58) → edit_apply_*, validate_branch_change, git_checkout_and_reset
  • cmd_ckeys (45) → 1 função por SecretsCommands variant
  • cmd_tokens (38) → cmd_tokens_list/test/reveal
  • cmd_add (37) → resolve_add_token/ckey, register_pr_app, find_parent_app_for_repo
  • cmd_debug (31) → resolve_debug_target_app/project_instance, cmd_debug_test

Restam 2 CRITICALs (main 517 e cmd_secrets 161) que precisam refactor estrutural maior — deferidos.

385 testes passando.


[2.23.9] - 2026-05-16 — Quality: refactor 12 funções CRITICAL + build perf

Refactor

/proj/security/quality flagou 11 funções com CCN (Cyclomatic Complexity Number) acima de 30 (CRITICAL pela regra do Uncle Bob). Esse release quebra cada uma em fases nomeadas:

  • Pipeline: deploy_phases (CCN 67→<30), deploy (37→<30), start_container (54→<30)
  • Fetch/Registro: add_app (CCN 91→<30), fetch_single_app (44→24)
  • Environment: provision_env_files (37→<30)
  • Secrets: bootstrap_push (39→<30)
  • Notify: setup_telegram (33→<30)
  • Validator: fmt Display impls quebrados em 7 funções write_*
  • main.rs: 4510 linhas → 2374 linhas. 8 handlers extraídos (cmd_secrets, cmd_cleanup, cmd_edit, cmd_debug, cmd_status, cmd_ckeys, cmd_tokens, cmd_add).

Sem mudança de comportamento — 385 testes passando.

Build performance

  • lto = "thin" + codegen-units = 16 no Cargo.toml — cold build multiarch ~46% mais rápido. Binário cresce ~2MB (trade-off aceitável).
  • sccache integrado no build_multiarch.sh (apenas no build nativo; containers cross ignoram).

[2.23.8] - 2026-05-16 — Security: bump reqwest 0.12 + remove tokio-process

Modificado

  • Bump reqwest 0.11 → 0.12. Stack TLS antiga (rustls 0.21 → rustls-webpki 0.101.7) tinha GHSA-82j2-j2ch-gfr8 (DoS via malformed CRL, CVSS 7.5). Agora rustls 0.23 → rustls-webpki 0.103.13 (patched). API pública compat — zero mudança nos call sites.

  • Removido tokio-process 0.2. Crate abandonada que arrastava crossbeam-utils 0.6.6/0.7.2, vulneráveis a CVE-2022-23639 (CVSS 7.5). Não tinha uso direto no código — substituída há tempos por tokio::process built-in.

Resultado SCA

Antes: 4 HIGH (/proj/security scanner score 60, REPROVADO). Depois: 0 HIGH, score 100, APROVADO.

385 testes — todos passando.


[2.23.7] - 2026-05-16 — State files com permissão 0640

Modificado

  • State files (/opt/runner/state/*.yml) agora têm perm 0640 em vez de 0644. Esconde metadata sensível (repo URL, timestamps, encrypted token+ckey) de usuários não-root no host. A criptografia já protegia os valores, mas não há motivo pra deixar metadata aberta.

    Migração lazy: load_state corrige perm de files legacy (0644) pra 0640 na primeira leitura. Não loosen perms já mais restritivas — operador que manualmente setou 0600 mantém isso.

Postura de acesso resultante

Path Perm Quem lê
/opt/runner/.keys/master.key 0600 root:root só root
/opt/runner/state/*.yml 0640 root:devs root + grupo devs
/data/apps/<app>/.env 0644 qualquer (env público)
/data/apps/<app>/.secrets 0600 só root
/data/apps/<app>/.runner/secrets.enc herda umask qualquer (encriptado)

Forçar migração de todas as apps:

runner status --json >/dev/null
ls -la /opt/runner/state/

[2.23.6] - 2026-05-16 — Audit log de acesso a secrets

Adicionado

  • Audit trail JSONL para operações em runner secrets. Cada show/set/generate/delete/paste-encoded escreve uma linha em /opt/runner/logs/secrets-access.jsonl (perm 0640 root:devs):

    {
      "timestamp": "2026-05-16T17:58:58Z",
      "action": "show",
      "app": "meu-app",
      "keys": ["DB_PASSWORD", "JWT_SECRET"],
      "user": "root",
      "uid": 0,
      "hostname": "ns3177045",
      "pid": 1067444
    }

    Garantia: campo keys registra apenas nomes, nunca valores. Auditável end-to-end com cat/jq sem vazar secrets. Separado de insecure-events.jsonl (que registra bypass de security guards) — este cobre acesso legítimo mas sensível.

Análise rápida

jq -c 'select(.action == "show")' /opt/runner/logs/secrets-access.jsonl
jq -r '"\(.timestamp) \(.user) \(.action) \(.app)"' /opt/runner/logs/secrets-access.jsonl

[2.23.5] - 2026-05-16 — Fix `secrets delete` da última key

Corrigido

  • runner secrets delete da última key falhava com No .env or .secrets content to encrypt. Causa: write_app_secrets tentava encriptar bundle vazio, e runner_core (corretamente) refuse. Agora detecta caso vazio e remove .runner/secrets.enc em vez de encriptar zero bytes. Próximo secrets set repopula normalmente. Multi-key delete (caso comum) já funcionava na v2.23.4.

    Detectado em teste isolado pós-release da v2.23.4.


[2.23.4] - 2026-05-16 — `runner secrets delete`

Adicionado

  • runner secrets delete <app> <key> — fecha gap CRUD do subcommand-grupo secrets. Mesmo padrão do ckeys delete (v2.23.0). Procura a key em .env e .secrets, remove de onde encontrar, reescreve os 3 stores atomicamente. --force/-y/--yes skipa confirmation. Key ausente é no-op (exit 0).

    Resolve o resíduo TEST_CONVERGENCE_KEY=hello-2026 reportado em campo após validação da v2.23.3.


[2.23.3] - 2026-05-16 — 7 bugs do field report (deploy de um cliente)

Após uma sessão de deploy real que levou 3h por causa de bugs do runner, 7 itens críticos do report do operador foram fechados.

Corrigido — críticos

  • Traefik path validation — antes, deploy retornava ✓ DEPLOY CONCLUÍDO mas o domínio ficava 404 com cert default quando paths.traefik_dynamic apontava pra dir que o container Traefik não montava. Agora o runner inspeciona os mounts do traefik container antes do deploy e bail com hint específico:

    traefik_path_unreachable: container 'traefik' does not see /etc/traefik/dynamic.
      Detected mount: /var/lib/docker/volumes/traefik_data/_data (host) -> /etc/traefik (container).
      Fix: runner settings set paths.traefik_dynamic /var/lib/docker/volumes/traefik_data/_data/dynamic
  • Stores convergentes em secrets paste-encoded — antes, encodar secrets via paste-encoded atualizava só .runner/secrets.enc, mas o deploy lia .env/.secrets plaintext (stale) e sobrescrevia o bundle. Resultado: container subia com tudo vazio. Agora as 3 fontes são escritas atomicamente.

  • build.context honrado — antes era silenciosamente ignorado, runner sempre procurava Dockerfile no root e enviava o repo inteiro como contexto. Agora effective_context = source/<build.context> e o Dockerfile é resolvido relativo a ele (docker semantics standard).

  • Connection strings auto-detectadas como secret — DATABASE_URL, DB_URI, POSTGRES_URL, MONGO_URI, REDIS_URL, AMQP_URL, SMTP_URL e variantes agora vão automaticamente pro .secrets cifrado em vez de vazarem pro .env plaintext. (BASE_URL/API_URL continuam tratados como públicos.)

Corrigido — altos

  • version.file aceita subpath confirmado via testes de regressão. Mensagem de erro agora mostra os 4 paths absolutos tentados pra diagnóstico imediato.

  • runner add faz clone inicial pra pull/ — antes app ficava em status: no_git permanente até primeiro deploy. Agora shallow clone (--depth=1) ao final de add_app, best-effort.

Adicionado

  • runner tokens reveal <app> --insecure — mirror do runner ckeys reveal. Audit-logged. Use case: operações git ad-hoc quando scriptando ao redor do runner.
runner tokens reveal meu-app --insecure
runner tokens reveal meu-app --insecure --json | jq .plaintext

[2.23.2] - 2026-05-15 — Fix `.gitignore` traps em `secrets push`

Corrigido

  • secrets push falhava em repos com .runner/ no .gitignore (template comum). Causa: o helper adicionava !.runner/secrets.enc como negação, mas em git uma negação não funciona se o diretório parent está ignorado. Agora o runner reescreve .runner/ → .runner/* (glob form) antes de adicionar a negação.

  • Auto-fix de proteção plaintext em vez de refusar push — se o repo não tem .env/.secrets no .gitignore, runner injeta os 4 patterns (.env, .env.*, .secrets, .secrets.*) no mesmo commit do bootstrap, em vez de bailar e pedir pro operador fixar manualmente.

  • Fallback git add -f .runner/secrets.enc se padrões exóticos de .gitignore ainda travarem o staging.


[2.23.1] - 2026-05-15 — `runner secrets push`

Adicionado

  • runner secrets push <app> — push manual de .deploy.yml + .runner/secrets.enc pro repo do app. Usa o token já encriptado no state (zero credentials por flag/env). Resolve o caso "registrei o app, runner gerou os files localmente, mas o repo ainda não tem". Antes da v2.23.1 isso só acontecia após o primeiro deploy bem-sucedido.

    Encrypted-only por design: whitelist de staging (.deploy.yml + .runner/secrets.enc apenas), .gitignore check, refuse se .runner/secrets.enc não puder ser produzido.

  • Auto-bootstrap no runner add — quando --ckey e token estão disponíveis e --deploy não foi passado, runner tenta o bootstrap_push ao final de add. Failure não falha o add — operador pode retry via runner secrets push.


[2.23.0] - 2026-05-15 — UX cleanup + ckeys CRUD

Bump menor por novas subcommands. Bugs do feedback do operador, reportados em sessão anterior.

Corrigido

  • runner add não cria mais ckey órfã. Antes, runner ckeys add <project> seguido de runner add criava DUAS ckeys, a primeira ficava órfã sem como remover. Agora add_app consulta vault::ckeys::find_for_project() e:

    • Sem --ckey + vault tem uma → herda silenciosamente
    • Com --ckey + vault tem uma → warna + instrui delete
  • runner ckeys delete <id> novo subcommand. Completa o CRUD do vault.

  • runner secrets show header agora indica source: --- secrets (from .runner/secrets.enc) --- em vez de --- .secrets ---.

  • runner manifest --resolved renderiza {{::Var}} no .deploy.yml usando .env/.secrets/secrets.enc. Templates sem valor ficam como <UNRESOLVED:KEY>.

  • runner secrets set/generate/share substitui o paste-encoded --mode {manual,generate,shared} confuso. Antigo continua funcionando com deprecation warning.

  • runner generate-config agora emite secrets: block separado em vez de placeholder {{::KEY}} em environment:.

  • --yes / -y aliases pra --force em unregister, destroy, ckeys delete.

  • Mensagem misleading "pull/ on first fetch" → "on first deploy" (clone só acontece em runner deploy).


[2.22.10] - 2026-05-15 — Fix deploy-loop em falha pós Phase 1.6

Corrigido

  • Deploy-loop quando deploy falha após secrets.enc ser commitado. Bug de v2.22.6: pipeline encriptava .env/.secrets em Phase 1.6 e já fazia git push do .runner/secrets.enc — antes do pre-flight, build e health check. Quando o deploy falhava depois, o commit ficava no remote. O cron fetch --deploy detectava "novo commit", tentava deploy de novo, falhava, commitava… loop infinito.

    Fix: persist_to_repo quebrado em dois:

    • write_local_secrets_enc — escreve apenas app_path/.runner/secrets.enc localmente (Phase 1.6). Não toca git, então falha posterior não polui o repo.
    • commit_and_push_secrets — copia o blob pro pull/, git add/commit/push. Roda em Phase 8 (após Phase 7 cleanup), só quando deploy validou.

    Plus: hash SHA-256 do plaintext (.env+.secrets) é comparado contra o último push bem-sucedido (app_path/.runner/secrets.hash). Pula commit se nada mudou — antes o nonce AES-GCM aleatório fazia o ciphertext sempre diferir, gerando commit a cada deploy mesmo sem mudança real.

Sintoma em produção

app-worker ficou em loop com 12+ commits chore: update encrypted secrets em ~30min, todos seguidos de deploys falhados pelo mesmo pre-flight check (secrets do provedor de pagamento em branco). Após v2.22.10, deploy falha silenciosamente pro repo — operador resolve o problema sem ruído de commits.

Compatibilidade

Recuperação zero-touch (runner add --ckey num servidor novo) continua funcionando idêntico — secrets.enc é gravado localmente em todas as tentativas, só não vai pro remoto enquanto o deploy não validar.


[2.22.9] - 2026-05-15 — Remote operation: restart + container logs

Adicionado

  • runner restart <app> [-i instance] [--timeout N] [--json] — recycle do container que está servindo a app, sem ir pelo pipeline de deploy. Útil pra pegar mudanças de env (após runner env set) ou destravar um processo preso. Resolve o container via state file → current_version → {project_und}_{instance}_{version_und}.
  • runner container logs <app> [-i instance] [-n tail] [--json] — logs do container em execução, sem precisar de SSH + docker logs. Permite operação remota via CCS ou orquestrador. Strip automático dos 8-byte headers do docker log stream. -n all lê tudo.

Motivação

Análise de contrato CCS ↔ runner detectou que o operador remoto não conseguia fazer debug/recycle sem SSH direto ao servidor. Estes 2 comandos fecham o gap pra 90% dos casos de operação remota.


[2.22.8] - 2026-05-15 — `runner deploy-all` top-level

Adicionado

  • runner deploy-all [--force] [--verbose] [--json] — alias top-level pra runner deploy --all. CCS e outros orquestradores chamam runner deploy-all --json esperando comando explícito; antes o runner só tinha deploy --all e o CCS quebrava com unrecognized subcommand. Output JSON inclui success, deployed, failed, timestamp.

Adicionado (infraestrutura)

  • tests/ccs_contract_test.sh — script de validação do contrato entre o CCS client e o runner CLI. Testa ~30 comandos que o CCS invoca, validando: existência de subcommands, JSON parse no stdout, exit codes, convenção de flags. Uso: ./tests/ccs_contract_test.sh (local) ou ./tests/ccs_contract_test.sh ssh production (remoto).

[2.22.7] - 2026-05-12 — `ckeys reveal` + export fixes

Adicionado

  • runner ckeys reveal <app-or-id> --insecure — comando novo para revelar a ckey plaintext de um app ou ckey id. Gated por --insecure. Aceita nome do app (lê do state file), storage id (ck_<8hex>) ou formatted id (ck_xxxx…xxxx). Use case: recovery quando precisa re-registrar um app num servidor novo mas a ckey ficou só encriptada no servidor velho.

Corrigido

  • runner ckeys export agora aceita nome do app. Antes só achava standalone ckeys em /opt/runner/.keys/ckeys/*.json. Apps com ckey legacy no state file retornavam "not found". Fallback adicionado.
  • runner export --include-secrets-sealed retornava null. Olhava em .secrets.enc (path legacy). Canônico desde v2.10+ é .runner/secrets.enc. Agora tenta os dois.

[2.22.6] - 2026-05-12 — Pre-flight + persist_to_repo fixes

Corrigido

  • Pre-flight scanner: .env/.secrets em /data/apps/{app}/ não disparam mais warning. Desde v2.15 esse path é a área de runtime do runner — não é git working tree. Os arquivos são artefatos legítimos gerados pelo runner (passados via --env-file ao docker), não candidatos a commit. Check agora skipa quando app_path/.git/ não existe.
  • persist_to_repo: o git add .runner/secrets.enc rodava em /data/apps/{app}/ (fatal: not a git repository). Agora redireciona pra pull/ (git working tree real). Auto-propagação de secrets.enc pro repo remoto via git push funciona — sem mais step manual de copiar via scp.

[2.22.5] - 2026-05-12 — `--build-cache` default

Corrigido

  • runner cleanup --images --build-cache sem valor agora usa default 72h (alinhado com a doc). Antes falhava com "a value is required" apesar da doc dizer "Without value: prunes cache older than 72h". Aceita: omitido (72h), 168h, all.

[2.22.4] - 2026-05-12 — Template resolution: secrets.enc restore + docker --env

Corrigido

  • B1: secrets.enc restore não provisionava templates do .deploy.yml. Quando runner add decifrava secrets.enc do repo, restaurava o .env antigo (que podia ter só PROJECT+INSTANCE) e skipava o env wizard. Agora roda em merge mode: adiciona keys do .deploy.yml environment: que estejam faltando no .env restaurado, resolvendo templates com defaults.
  • B2: docker run injetava template literal via --env. O pipeline injetava todas as entries de config.environment como --env KEY=VALUE no docker run, incluindo valores com {{::...}} não resolvidos. Como --env tem precedência sobre --env-file, o template literal sobrescrevia o valor correto do .env. Agora entries com {{:: são skippadas (mesmo padrão de GENERATE).
  • C: multi-template começando com {{:: (v2.22.3): "{{::A}},{{::B}}" era parseado como single Prompt full-match. Agora parse_env_value_spec verifica que o inner não contém outro {{:: antes de tratar como Prompt — strings multi-template viram Composite corretamente.

[2.22.2] - 2026-05-12 — Template resolution no deploy pipeline

Corrigido

  • runner fetch --deploy (cron) deixava {{::Var?default}} literal no .env porque provision_env_files só resolvia GENERATE, nunca {{::}}. Agora step 5 do provisioning resolve templates com defaults (non-interactive). Exemplo: BACKEND_DOMAIN: "{{::BackendDomain?backend.staging.exemplo.com.br}}" gera valor correto no .env.
  • --env "HostBind=X" para template em ports: era descartado. Era consumido apenas para resolver environment:/secrets:. Se o label não existia como key em nenhum dos dois, era silenciosamente perdido. Agora overrides não consumidos são escritos no .env para que ports: e build.args: possam resolvê-los via resolve_value_from_app_files.

[2.22.1] - 2026-05-12 — Template resolution no `runner add`

Corrigido

  • runner add --env "TenantSlug=X" com .deploy.yml tendo TENANT_SLUG: "{{::TenantSlug?demo}}" agora resolve corretamente. Antes o lookup era feito só pelo nome da variável (TENANT_SLUG), ignorando o label do template (TenantSlug).
  • Templates compostos em environment:/secrets: como "https://{{::Domain?example.com}}/api" agora são expandidos. Antes só templates full-match (valor inteiro = {{::...}}) eram processados — compostos ficavam literais no .env.
  • Templates em ports: (ex: ports: ["{{::HostBind?10.108.0.5:80}}:80"]) agora resolvem a partir do .env/.secrets antes de passar ao docker.

[2.22.0] - 2026-05-12 — Native `path_prefix` routing

Resumo

Apps que precisam compartilhar host com outro servico (frontend + sidecar MCP, painel admin sob /admin, API publica sob /api) ganharam suporte nativo via instances.<inst>.path_prefix. Antes da v2.22.0, esse cenario exigia escrever um arquivo Traefik dinamico a mao em /opt/traefik/dynamic/ — o runner so emitia regras Host() puras.

Adicionado

`instances..path_prefix` — co-hospedagem em sub-rota

Quando definido, a regra Traefik vira:

Host(`{domain}`) && PathPrefix(`{path_prefix}`)

com priority: 100 (vence o router catch-all do host vizinho).

instances:
  production:
    domain: wizard.runnerci.com
    path_prefix: /mcp
    source:
      type: dist
    keep_versions: 3

Caso de uso real (em producao): o frontend wizard serve wizard.runnerci.com/; o MCP server wizard-mcp serve wizard.runnerci.com/mcp/*. Ambos containers separados, mesmo dominio.

Comportamento:

  • Vazio/omitido/whitespace-only → regra fica apenas Host() (legacy, sem priority)
  • Todos os tres caminhos de geracao Traefik honram o prefix: single-version, canary, snippet externo

Validacao

  • 348 unit tests pass (era 343 v2.21.0; +5 do v2.22):
    • path_prefix_renders_when_set, path_prefix_omitted_when_empty (runner-core schema)
    • test_path_prefix_appends_to_rule_with_priority, test_path_prefix_empty_falls_back_to_host_only (runner-client traefik)
    • path_prefix_round_trips (wizard YAML → runner parser contract)

Backwards compatibility

  • .deploy.yml sem path_prefix: → comportamento identico v2.21.x
  • Apps em producao com workaround manual (arquivo Traefik hand-edited) podem migrar adicionando path_prefix: e removendo o arquivo apos o proximo deploy

[2.21.0] - 2026-05-07 — Multi-app follow-ups

Resumo

5 features que completam MVPs/slots deixados em v2.20.0 + 1 UX safety net + 1 doc fix. Construído sobre o feedback de um deploy multi-service complexo em produção.

Mudança comportamento notável: runner add em repo já registrado agora falha com duplicate_repo exit 2. Workaround compatível: --force-duplicate. Documentado abaixo.

Adicionado

A — Interpolation `{{::Var}}` em `build.args:`

v2.20.0 entregou MVP literal (passa valor cru pro --build-arg). v2.21.0 fecha: agora interpola o mesmo {{::Var}} que environment: usa, resolvendo de .env e .secrets em build time.

build:
  args:
    VITE_SUPABASE_URL: "{{::SUPABASE_URL}}"
    VITE_LIVEKIT_WS_URL: "{{::LIVEKIT_WS_URL?wss://default.com}}"

Order: .secrets primeiro (mais específico), .env fallback. Suporta {{::Key?default}}. Sem TTY no build → não pede prompt; se var não resolve, erro instrutivo apontando pra runner env set.

B — Healthcheck `depends_on:` cross-app

Resolve o caso multi-app comum: meu-agent não deveria subir antes do meu-livekit estar healthy. Antes era manual (deploys sequenciais com vista nos logs).

instances:
  production:
    depends_on:
      - app: meu-livekit
        condition: healthy           # healthy | started
        timeout: 300s
      - app: meu-redis
        instance: production         # opcional, default = mesma instance
        condition: started

Comportamento: start_container polla docker inspect por cada dep até condition met OR timeout. Bypass: --insecure ignora. Erro instrutivo: dependency_timeout: <app>/<instance> not ready after Ns.

C — `traefik.external.{snippet, write_to}` — slot ocupado

v2.20.0 entregou só o bypass do write local. v2.21.0 enriquece o slot:

# /opt/runner/config.yml
traefik:
  mode: external
  external:
    write_snippet_to: "/opt/traefik-snippets/{app}.yml"
    print_snippet: true

Output em modo external:

  • print_snippet: true (default) → snippet em stderr depois do deploy, copy-pasteable
  • write_snippet_to: <path> → também salva em arquivo (Ansible sync)
  • {app} placeholder no path expande pro project slug

D — `runner add` detecta repo duplicado

UX safety net pro caso real: operador registrou 2 vezes sob nomes diferentes (meu-app-livekit + meu-livekit). Runner aceitou silenciosamente, gerando state divergente.

runner add --repo usuario/meu-app --branch main --ckey "..." ...

Error: duplicate_repo: Repo usuario/meu-app já está registrado
       como `meu-app-livekit` (branch: deploy/livekit, status: registered).

       Opções:
         --force-duplicate            permite criar SEGUNDA app pro mesmo repo
         runner unregister meu-app-livekit --force    remove a anterior

Bypass: runner add --force-duplicate ... (raro; testes A/B, multi-instance experimental).

E — `generate-config` template completo

Operador olhou runner generate-config e concluiu erradamente que ports:/secrets:/type: image não existiam. Template embedded estava incompleto. Agora mostra TODOS os campos válidos do schema, com comentários esclarecendo opcionalidade + version markers (v2.20.0+, v2.21.0+).

Validação

  • 340 unit tests pass (era 328 v2.20.0; +12 do v2.21)
  • Matriz CI/CD: 82 pass / 0 fail / 71 n/a em ~184s (9 estados × 16 ops)
  • 3 ops novas no matrix:
    • build_args_interp (state B): valida {{::Var}} resolution end-to-end
    • depends_on (state B): valida wait timeout em dep não-existente
    • duplicate_repo (state A): valida runner add --repo X re-aceito falha com duplicate_repo
  • traefik_external (já em v2.20) ganhou snippet asserts: confirma stderr emit em mode=external

Backwards compatibility

  • .deploy.yml sem depends_on: → Vec vazio, comportamento idêntico v2.20.x
  • .deploy.yml com {{::Var}} em build.args: agora interpola (era literal em v2.20 — comportamento mais correto, sem quebra esperada)
  • config.yml sem traefik.external: → sub-block opcional, só usado em mode=external
  • runner add em repo já registrado muda comportamento: era silently OK, agora duplicate_repo exit 2. Workaround: --force-duplicate

Backlog v2.22+

  • Sol 2 post_deploy_check schema (smoke pós-deploy configurável)
  • Service discovery dinâmico (env var templating tipo ${app.endpoint})
  • W6 smoke probe (rodar imagem brevemente + detect listen port real)
  • UDP-aware allocator (random strategy só pensa TCP hoje)
  • runner list --orphans (states sem deploy_count > 0)

Backlog v3.0 (acumulando breaking)

  • Compose-style multi-service em 1 manifesto
  • status --json cleanup — remover legacy top-level fields

[2.20.0] - 2026-05-06 — Multi-app polish + CCS schema

Resumo

5 features opt-in que fecham fricções identificadas em deploys multi-service reais (LiveKit + Python agent + Vite frontend) + schema split do runtime status --json pra CCS dashboard. Zero breaking changes — apps v2.19.x rodam sem mudança.

Adicionado

A — `build.args:` no `.deploy.yml` (Vite/Next/Angular zero-touch)

build:
  context: frontend
  dockerfile: Dockerfile
  args:                              # ← NOVO
    VITE_SUPABASE_URL: "https://..."
    VITE_LIVEKIT_WS_URL: "wss://..."
    BUILD_ENV: production

Cada entry vira --build-arg KEY=VAL no docker build. Mata o workaround "build local + commit dist/" pra apps com env vars compile-time. MVP v2.20.0: valores literais. Interpolation {{::Var}} (igual environment:) é follow-up — resolve_value helper público ainda não existe.

B — Auto-create docker network

Quando .deploy.yml declara networks: [video] e a network não existe, runner cria automaticamente antes do docker run. Idempotente. Built-ins (bridge, host, none) são skipped. Consolidado num único path em containers::ensure_network.

C — `traefik.mode: external | none` (bypass)

# /opt/runner/config.yml
traefik:
  mode: external      # none | local (default) | external
Mode Comportamento
local (default) Escreve em <traefik_dynamic_dir> (v2.19.x)
external Skip do write local — operador trata roteamento externo
none Mesmo bypass — apps internas sem HTTP público

v2.20.0 = só bypass. Slot reservado pra ações futuras (snippet output, write_to). Default local preserva backcompat 100%.

D — `status --json` schema split (aditivo)

Payload do runner status <app> --json ganha 3 blocos lógicos:

{
  "current_version": "abc1234",
  "manifest": { "project": "...", "repo": "...", "branch": "...", "ckey_fingerprint": "ck_...", ... },
  "config":   { "type": "...", "port": ..., "ports": [...], "networks": [...], "hosts": {...}, ... },
  "runtime":  { "current_version": "...", "status": "...", "network": { ... v2.19.0 } },
  "_deprecated_top_level": true,    // marker pra CCS migrar
  // legacy top-level fields preserved (project, system, app_type, instances, state)
}

CCS dashboard pode migrar gradualmente: data.manifest !== undefined → caminho novo. Cleanup de legados em v3.0 (postergado).

E — `hosts:` map (DNS aliases via `--add-host`)

# .deploy.yml
hosts:
  host.docker.internal: host-gateway
  internal-api: 10.0.0.5
  livekit-pub: 10.108.0.5

Cada entry vira --add-host KEY:VAL no docker run. Resolve host.docker.internal em Linux Docker (que não tem por default). Map (não array) — last-wins em duplicatas, override fácil em re-deploy.

Refatorado

  • containers::ensure_network ganhou skip pra docker built-ins (bridge, host, none). Single source of truth — pipeline não duplica mais a lógica.

Validação

  • 328 unit tests pass (era 309 v2.19.0; +19 do v2.20)
  • Matriz CI/CD: 79 pass / 0 fail / 47 n/a em ~170s (9 estados × 13 ops)
  • 4 ops novas no matrix:
    • build_args: valida injection em build-mode states (B/D/I)
    • network_autocreate: valida criação automática de network nova
    • traefik_external: valida que mode=external não escreve local file
    • hosts_check: valida --add-host no docker inspect
  • status_schema.py agora hard check (era permissivo) — falha se manifest/config/runtime faltarem

Backwards compatibility

  • .deploy.yml sem build.args: → BTreeMap vazio, zero --build-arg flags
  • .deploy.yml sem hosts: → BTreeMap vazio, zero --add-host flags
  • config.yml sem traefik.mode: → default local → comportamento idêntico v2.19.x
  • .deploy.yml com networks: [X] e X não existe → v2.19.x quebrava no docker run; v2.20.x cria
  • Consumer do status --json legado → continua lendo top-level (_deprecated_top_level: true marker indica caminho novo)

Nenhuma quebra. Apps registradas em v2.19.x seguem rodando.

Backlog identificado pra v2.21.0

  • Interpolation {{::Var}} em build.args: — precisa expor resolve_single_value como pub em src/environment/mod.rs
  • Healthcheck depends_on: cross-app — multi-app ordering
  • Sol 2 post_deploy_check schema (smoke pós-deploy configurável)
  • Service discovery dinâmico entre apps (env var templating)
  • Sub-bloco traefik.external.{snippet, write_to} — ações concretas em external mode
  • W6 smoke probe (rodar imagem brevemente pra detectar listen port real)

Migration path

Pra apps existentes opt-in:

# Adicionar build.args
echo "  args: { VITE_X: ... }" >> .deploy.yml/build/

# Adicionar hosts
echo -e "hosts:\n  host.docker.internal: host-gateway" >> .deploy.yml

# Mudar pra external mode
echo "traefik:\n  mode: external" >> /opt/runner/config.yml

Sem precisar runner unregister ou --force.


[2.19.0] - 2026-05-06 — Port allocation autônomo (Lote B)

Resumo

Single feature focused: eliminar a única intervenção manual restante no CI/CD do runner — escrever ports: ["VPC_IP:porta:porta"] no .deploy.yml. Runner agora aloca porta livre automaticamente, persiste sticky no state file (re-deploys reusam a mesma porta), e expõe via runner status --json pra CCS consumir.

Bonus: pre-flight de secrets (Sol 1 do feedback de prod) — deploy aborta cedo se secret declarado em .deploy.yml::secrets: tem valor vazio (a menos de --insecure). Encerra o ciclo "deploy aparentemente bem-sucedido mas funcionalmente quebrado" causado por healthchecks que retornam 200 mesmo com secrets faltando (ex: Laravel /up).

Adicionado

Port allocation (config + state)

network: block opcional em /opt/runner/config.yml:

network:
  vpc_ip: 10.108.0.5         # opcional; sem isso bind cai pra 127.0.0.1
  publish_strategy: random   # random | sequential | explicit | none
  port_range: [8000, 8999]
  port_blocklist: []

Estratégias:

  • random (recomendado): sorteia porta livre na primeira deploy, persiste sticky pra re-deploys reusarem
  • sequential: pega a primeira porta livre no range
  • explicit: lê ports: do .deploy.yml (comportamento v2.18.x)
  • none: container roda sem -p no host

Sticky lifecycle:

  1. Primeira deploy: aloca porta → persiste em state/<app>.yml::network.published_port
  2. Re-deploys (commit novo, fetch --deploy): reusa a mesma porta — Traefik externo permanece válido
  3. runner unregister X: libera porta de volta ao pool
  4. runner reset --hard X: libera state → próximo deploy realoca
  5. runner ports realloc X: força nova alocação (operador atualiza Traefik)

Resolução de bind address:

  • network.vpc_ip configurado → bind nesse IP
  • Sem vpc_ip → bind em 127.0.0.1 (safe default — Traefik no mesmo host alcança via loopback)

Comandos novos

runner ports list [--json]    # lista portas alocadas em todas apps
runner ports release <app>    # libera state.network sem parar container
runner ports realloc <app>    # limpa published_port — próxima deploy realoca
runner deploy <app> --port N  # override permanente (escreve no state, força reuso)
runner deploy <app> --no-port # desabilita port mapping (strategy=none) só desta deploy

`runner init` interativo

Quando rodado em TTY, runner init agora pergunta:

  1. "Esse server fica em uma VPC interna? [s/N]"
  2. Se sim: oferece auto-detect via ip -4 -o addr show, confirma o IP
  3. "Strategy [random/sequential/explicit] (default: random)"
  4. Persiste o bloco network: no config.yml

Em modo não-TTY (CI, piped) skipa silenciosamente — backcompat 100%.

`runner status --json` runtime.network

Bloco novo no payload pra CCS consumir:

"runtime": {
  "network": {
    "vpc_ip": "10.108.0.5",
    "published_port": 8473,
    "endpoint": "http://10.108.0.5:8473",
    "allocation": {
      "strategy": "random",
      "allocated_at": "2026-05-06T12:30:00Z",
      "sticky": true,
      "source": "state"
    }
  }
}

Apps legacy sem state.network → runtime.network: null (key presente, valor null).

Pre-flight de secrets (Sol 1 do feedback prod)

Antes de iniciar o container, valida que cada secret declarado em .deploy.yml::secrets: tem valor não-vazio em .runner/secrets.enc (após decifrar). Se vazio:

Error: deploy_blocked: secrets declared in .deploy.yml have empty values: ["APP_KEY"]
       Bypass: rerun with --insecure (NOT recommended).
       Or fill the values:
         runner env set <app> --key APP_KEY --secret --value <value>

Bypass via --insecure (operator opt-out). Resolve o caso onde Laravel /up retornava 200 mesmo com APP_KEY vazio, fazendo o runner declarar deploy bem-sucedido enquanto / retornava 500.

Corrigido

  • Sticky port resolution não filtrava containers do próprio app: sticky::resolve consultava ports_in_use (ss + docker ps) e rejeitava porta ocupada por OUTRO processo — mas não distinguia containers da MESMA app (que estão sendo substituídos). Resultado: re-deploy do mesmo app sob random/sequential strategy hitava port_in_use_by_other. Fix: pipeline filtra ports de containers {project_und}_{instance}_* antes de chamar sticky. Resolve "ou é nosso → reusa" prometido na docstring desde a v2.19.0 inicial.

Backwards compatibility

  • Apps registradas em v2.18.x continuam rodando sem mudança
  • Default de publish_strategy é Explicit quando network block ausente — comportamento idêntico ao v2.18.x (lê ports: do .deploy.yml)
  • State files antigos sem bloco network migram silenciosamente — porta re-alocada no próximo deploy quando strategy != Explicit
  • instances.X.domain continua aceito (sem mudança)

Validação

  • 309 unit tests pass (era 304 + 5 do secrets_preflight + extras de network module)
  • Matriz CI/CD: 90 cells, 73 pass, 0 fail, 17 n/a em 114s
  • 2 ops novas no matrix:
    • network_check: valida porta sticky em range 9000-9100
    • secrets_preflight: valida deploy aborta com secret vazio
  • 9 estados validados ponta-a-ponta sob random strategy: A B C D E F G H I

Backlog identificado (não bloqueia)

  • runner status --json schema ainda flat fora do novo runtime.network — refactor completo (manifest/config/runtime blocks separados) fica pra v2.20.0
  • W6 smoke probe (rodar imagem brevemente pra detectar listen port real do container)
  • Sol 2 do feedback prod (post-deploy smoke test configurável no .deploy.yml) — v2.20.0

[2.18.2] - 2026-05-06 — Wizard "auxiliar inteligente"

Resumo

5 melhorias no wizard/generator que tornam o runner add realmente zero-touch para a maioria dos perfis e auxiliam o operador quando a análise estática não tem como saber a resposta. Construído sobre uma matriz de testes CI/CD (72 cells, 9 estados × 7 ops) que cobre todos os cenários de uso do runner.

Adicionado (W1) — Warning explícito quando porta é incerta

Antes: se o Dockerfile não tinha EXPOSE, o generator silenciava com port: 8080. Para compose, pegava o mapping host:container sem avisar que o container internamente pode escutar em outra porta.

Agora:

  • Dockerfile sem EXPOSE: warning Port: Dockerfile sem EXPOSE; assumindo 8080. Se o container escuta em outra porta, ajuste port: no .deploy.yml
  • Compose com ports:: warning Port: usando porta X do mapping no compose. ATENÇÃO: este é mapping host:container — se o container escuta internamente em outra porta (nginx :80, php-fpm :9000), ajuste port:

Adicionado (W2) — Healthcheck type-aware no generator

Antes: TCP cego em todos os deploys gerados.

Agora detect_default_healthcheck() escolhe baseado no nome da imagem:

Imagem Healthcheck gerado
nginx*, caddy*, httpd*, apache*, traefik* mode: http path: /
php-fpm*, qualquer *fpm* mode: tcp
node*, python*, ruby*, openjdk*, rust* mode: tcp
Outros mode: tcp (fallback seguro)

Adicionado (W5) — Aviso quando Dockerfile + compose coexistem

Antes: silenciosamente usava o Dockerfile, ignorava compose service.

Agora: warning Source: repo tem Dockerfile + docker-compose.yml. Build do Dockerfile foi usado como source-of-truth (compose só pra env vars). Para usar a imagem do compose em vez de buildar, edite type:image + image:

Adicionado (W7) — Detecção de domínio placeholder no wizard

Após o prompt de domínio interativo, warning loud se o valor casa com:

  • *.example.com / .org / .net
  • .test, .local, .localhost, .invalid
  • Markers: placeholder, todo, fixme, xxx, changeme, yourdomain

Não bloqueia (operador pode realmente querer um staging em *.test), só nudge.

Corrigido (Bonus) — `--accept-defaults` rejeita perfil bare

Antes: runner wizard --accept-defaults em repo sem Dockerfile/compose/.env gerava silenciosamente um .deploy.yml inútil.

Agora: bail com no_deploy_profile (mesmo comportamento do runner add).

Corrigido — TCP/HTTP probe IMAGE mode usa `bash -c` no fallback

Bug: probe usava sh -c '... /dev/tcp/... ...' no fallback. Como /dev/tcp é builtin do bash (não do dash/sh), containers Debian-based sem nc falhavam silenciosamente o healthcheck. Quebrava nginx:1, php-fpm:debian, etc.

Fix: o fallback agora invoca bash -c "exec 3<>/dev/tcp/127.0.0.1/PORT" — funciona em qualquer imagem que tenha bash (a maioria das de produção).

Validação

  • 284 unit tests pass (era 283; +1 do placeholder_domain)
  • Matriz CI/CD: 72 cells, 63 pass, 0 fail, 9 n/a em 110s
  • 9 estados validados: A (.deploy.yml), B (Dockerfile), C (compose), D (Laravel), E (bare → falha esperada), F (image-only), G (sem EXPOSE), H (IP-restricted), I (secrets em env)

Backlog identificado pela matriz (não bloqueia)

  1. runner status --json ainda flat — refactor para separar manifest / config / runtime blocks (input pra integração CCS)
  2. W6 (smoke probe pra detectar porta real do container) — alta complexidade
  3. W3 (version source detection: file vs git-commit) — baixa prioridade
  4. W4 (git ls-remote --heads pra listar branches no wizard) — baixa prioridade
  5. W8 (presets por stack: Laravel/React/Flask) — média prioridade

[2.18.1] - 2026-05-05 — Hotfix (6 bugs do loop e simulation)

Corrigido (P0)

  • Pipeline lê repo do state file (Bug E real em prod): migration v2.16.0 era parcial — pipeline ainda exigia github.repo no .deploy.yml e quebrava deploys de apps migrados. Agora resolve do state primeiro, fallback pro manifest legacy. Erro novo no_repo_configured é instrutivo.
  • Wizard --answers.instance_branch propaga até o YAML: era parseado mas hardcoded como branch: main. Agora InstanceSection.branch chega ao render.
  • Wizard recusa sobrescrever .deploy.yml existente (perfil A): bail com manifest_already_exists. Antes gerava defaults silenciosos.
  • Validator aceita type: OR app_type:: antes pedia só app_type: mas o parser canonical e o wizard usam type:.

Corrigido (P1)

  • Mensagem de erro no_deploy_yml: no_deploy_profile: redundante: strip do prefixo interno do generator.
  • runner status --json top-level current_version: campo era NULL no top, populado em 3 lugares diferentes do JSON. Agora atalho do state.current_version.
  • crypto.rs/vault/ckeys.rs aceitam RUNNER_KEYS_DIR env var: paths hardcoded /opt/runner/.keys exigiam root. Agora override opcional pra rootless test/CI; default inalterado pra prod.

Testes

36 unit tests no wizard module + 283 na suite total. Loop validation script: 6/6 pass do zero, incluindo cenário adversarial sem github: no manifest.


[2.18.0] - 2026-05-05 — Lote A (feedback de producao)

Atendendo feedback do deploy real do minha-api (Laravel 11). 6 itens.

Corrigido

  • --branch propaga pra instances.X.source.branch (#4): antes, runner add --branch dev clonava de dev mas a instance ficava com main do .deploy.yml — runner fetch posterior puxava da branch errada
  • runner add --deploy realmente deploya (#5): antes era silenciosamente ignorado com mensagem Auto-deploy requested but disabled. Agora invoca pipeline::deploy() na sequencia

Wizard polish

  • Curadoria de .env.example (#6): detecta APP_ENV=local, APP_DEBUG=true, LOG_LEVEL=debug, NODE_ENV=development → oferece "perfil de producao" (production, false, info); pruning automatico de placeholders comuns (AWS_*, MAIL_*, MEMCACHED_*, PUSHER_*)
  • Deteccao Laravel via composer.json (#7): identifica laravel/framework, analisa routes/api.php + resources/views/ para distinguir system: api vs sys; com Dockerfile -> type: docker-build
  • runner wizard --answers <file> (#8): substitui prompts interativos por respostas em JSON. Habilita IA/script/CI gerar manifesto valido sem TTY. Suporta override de campos do manifesto, env_overrides, skip_env (glob), secret_keys/non_secret_keys (forca classificacao)
  • ckey display por fingerprint (#10): default agora mostra apenas ck_xxxx…xxxx (ckey fica encriptada em state/<app>.yml); flag --ckey-show opt-in para exibir plaintext

Adiado para Lote B/C/D

Itens P0 do feedback (architectural changes):

  • VPC port allocation (network.{vpc_ip, publish_strategy, port_range}) — entregue na v2.19.0
  • traefik.mode: external + remocao de URL fantasia no output (planejado, não implementado)
  • Backup automatico no unregister (planejado, não implementado)

Detalhes

  • 7 unit tests novos (281 total na suite, 0 falhas)
  • Sem breaking change

[2.17.5] - 2026-05-05

  • gitops fix attempt #5: migra extra_artifacts -> scripts: v3.0 section. Tool web em runner.ccs.systems/tools/secrets.html segue aguardando atualizacao do gitops worker em producao

[2.17.4] - 2026-05-05

  • gitops fix attempt #4: arquivo tools/secrets/index.html na raiz do repo (igual init.sh)

[2.17.3] - 2026-05-05

Corrigido (gitops)

[2.17.2] - 2026-05-05

Corrigido

  • gitops: type: html em extra_artifacts era silenciosamente ignorado. Trocado para type: script

[2.17.1] - 2026-05-05 — F3 Tool web

[2.17.0] - 2026-05-05 — F1 Wizard CLI

  • `runner wizard` — wizard interativo para gerar .deploy.yml (e opcionalmente .runner/secrets.enc)
  • Auto-trigger no runner add em modo TTY quando perfil E (cru)
  • Detecta linguagem (next/react/flask/django/celery/rust/php) → infere system/type
  • Multi-service compose: pergunta qual e o app principal, marca redis/postgres/etc como assets
  • Subcomando runner template inspeciona schema embutido (debug)

[2.16.0] - 2026-05-05 — Lote 5 (#6, #4)

  • Generator de .deploy.yml — detecta perfil A-E e gera manifesto proporcional
  • AppState.repo migrado para state file (deprecates github: no .deploy.yml)
  • Migration silenciosa para apps existentes

[2.15.0] - 2026-05-05 — Lote 4 (#7)

  • runner add nao clona mais o repo na raiz do app dir (causava duplicacao)
  • Apenas .deploy.yml + .runner/state-marker na raiz; codigo vive em pull/ e src/{instance}/{ver}/
  • runner cleanup --reset-root <app> opt-in para apps existentes

[2.14.0] - 2026-05-05 — Lote 3 (#8)

  • runner add/deploy BLOQUEIA secrets em environment: sem ckey configurada
  • Flag global `--insecure` suprime TODAS as travas (banner vermelho + audit log JSONL)

[2.13.0] - 2026-05-05 — Lote 2 (#3)

  • `runner tokens` — list/test agrupado por fingerprint (sem revelar plaintext)
  • runner add --token-from <other-app> reusa token encriptado entre apps

[2.12.0] - 2026-05-05 — Lote 1 (#1, #2, #5)

  • --ckey sem valor gera ckey random; com valor aceita literal
  • --repo aceita URLs (https/ssh/git@) e normaliza para user/repo
  • --branch agora obrigatorio sempre (sem default dist)

[2.11.6] - 2026-05-04

  • Logs vao para stderr (stdout limpo para piping/JSON)
  • Handler SIGPIPE evita panic ao piping para head/jq

[2.11.5] - 2026-05-04

  • self-update lida com chattr +i (immutable bit) automaticamente

[2.11.0-2.11.4] - 2026-04-29 a 2026-05-03

  • v2.11.4: docker run env injection via --env-file
  • v2.11.3: provisionamento de .env/.secrets no bootstrap
  • v2.11.2: bug fix app_has_ckey (secrets nao injetados em redeploys)
  • v2.11.0: vault completo (mkey + ckeys + config-backups + secrets), Ed25519 minisign

[2.10.0] - 2026-04-25

  • runner debug test — validacao end-to-end (config, vault, deploys, validators)
  • Coverage 100% de --json em todos os comandos

[2.9.0] - 2026-04-22

  • CCS Data Contract — alinhamento de output runner snapshot --json para integracao com dashboard CCS

[2.8.0] - 2026-04-20

  • Master key backup: runner mkey export/import para disaster recovery

[2.7.0] - 2026-04-19

  • init.sh — instalador unificado (arch-aware, --rc flag para release candidates)

[2.6.0] - 2026-04-18

  • mode: exit no healthcheck — apps CLI/scanners/jobs one-shot (sem container permanente)

[2.5.0] - 2026-04-18

  • runner init interativo — wizard de setup inicial com --set e --mkey

[2.4.2] - 2026-04-17

  • Deteccao automatica de secrets — Aho-Corasick multi-pattern matching com 3 niveis de confianca (HIGH 95%, MEDIUM 75%, LOW 50%)
  • runner env set inteligente — auto-detecta secrets pelo nome, encripta com ckey ou avisa se plaintext
  • --force no env set — forca como secret sem questionamento (requer ckey)
  • .secrets so com encriptacao — sem ckey o pipeline ignora .secrets, --secret sem ckey retorna erro
  • regenerate fallback — procura key no .env se .secrets nao existe
  • Filtro de falsos positivos — keyboard, primary_key, max_tokens, cache_key, public_key, etc.

[2.4.1] - 2026-04-17

  • runner cleanup --images — remove imagens Docker nao utilizadas de projetos gerenciados pelo runner
  • runner cleanup --build-cache [FILTER] — prune do Docker buildkit cache com filtro de idade (default: 72h, aceita 24h, 168h, all)
  • Limpeza automatica de imagens no pipeline — cleanup_old_versions agora remove a imagem Docker de versoes que excedem keep_versions
  • docker image prune -f — executado automaticamente apos cleanup de imagens (dangling layers)

[2.3.0] - 2026-04-16

  • mTLS com tokio-rustls — API HTTP requer client certificates quando habilitado
  • Hot-reload de CA cert — mtime-based, CCS rotaciona cert via SCP sem restart
  • TLS proxy architecture — tokio-rustls acceptor + warp backend ephemeral

[2.2.0] - 2026-04-14

  • runner service — lifecycle CLI para systemd (init, start, stop, restart, status, config, uninstall)
  • runner signal — sinais externos para CI/CD: send (ok/warn/fail + action), clear, list
  • runner serve --service — service mode com API HTTP + scheduler generico
  • API HTTP autenticada — 5 endpoints REST com X-API-Key (/health, /api/v1/status, signal, deploy, apps)
  • API Key com fingerprint — formato rk_{SHA256(hostname+MAC+machine-id)[..8]}_{random}
  • Scheduler generico — trait-based: CanaryJob, TtlCleanupJob, SignalCheckJob
  • service.yml — configuracao separada do service mode (API, scheduler, webhook, mTLS)

[2.1.0] - 2026-04-07

  • Healthcheck honra HEALTHCHECK do Dockerfile — nao sobrescreve com --health-cmd
  • Healthcheck modos tcp: e cmd: — prioridade cmd > tcp > path
  • depends_on: entre apps — topo sort em runner deploy --all, cycle detection
  • Validacao semantica em runner add — build context, Dockerfile, port, healthcheck
  • image_mode: dockerfile aceito como alias de self-contained
  • start_period configuravel no .deploy.yml (default 30s)
  • wait_for_healthy tolerante — nao aborta em Unhealthy transitorio
  • Self-update multi-arch — detecta x86_64/aarch64 e baixa binario correto do CDN
  • Lock file PID-alive — remove locks orfaos automaticamente
  • Mensagens de erro melhoradas — YAML com linha/coluna, token expirado, rollback em 1a deploy

[1.5.0] - 2026-03-17

Adicionado

  • --instance no runner add: Define qual instancia do .deploy.yml esta maquina deve deployar
  • --instance no runner edit: Altera a instancia a qualquer momento, com validacao contra o .deploy.yml
  • Campo instance no state: Persistido no state file, usado pelo auto-deploy
  • Guia Multi-Ambiente: Documentacao completa sobre deploy em multiplas maquinas

Alterado

  • Auto-deploy resolve instancia por prioridade: state.instance > default_instance_name() > "production"
  • runner edit --show: Agora mostra a instancia configurada

[1.4.1] - 2026-03-13

Adicionado

  • max_deploy_retries: Limita re-tentativas de deploy para o mesmo commit que falhou (padrao: 3). Evita loops infinitos em builds quebrados
  • Rastreamento de falhas no estado: Campos last_failed_commit e deploy_retry_count no arquivo de estado da app
  • Rollback automatico em falha de start: Quando o container antigo e parado por conflito de porta e o novo falha ao iniciar, o anterior e reiniciado automaticamente

Melhorado

  • Mensagens de erro nas notificacoes: Extrai a linha de erro real de outputs verbosos (Docker BuildKit, npm, Rust) em vez de truncar no inicio. Limite de 800 caracteres
  • Notificacao de falha mostra rollback: Indica qual versao ficou ativa apos o rollback

[1.4.0] - 2026-03-12

Adicionado

  • runner notify setup: Setup interativo de notificacoes Telegram e Discord
  • runner notify test: Envia notificacao de teste para todos os canais configurados
  • runner notify status: Mostra status dos canais
  • StructuredLogChannel: Audit trail em deploy-events.jsonl
  • NotifyManager v2: Substitui funcoes legacy por multi-channel com suporte a Telegram, Discord e structured logging

Alterado

  • Pipeline usa NotifyManager v2: Substitui chamadas legacy por NotifyManager.send()
  • Config consolidado: GlobalConfig removido, tudo via RunnerConfig

Removido

  • Codigo legacy de notificacoes (send_telegram, send_discord, notify_deploy_*)

[1.2.3] - 2026-03-01

Modificado

Modos de Deploy Simplificados

O Runner agora opera em dois modos exclusivos, determinados pela presenca de image: ou build: no .deploy.yml:

  • Modo IMAGE (image: xxx): usa imagem pronta do registry, monta source via volume
  • Modo BUILD (build: {dockerfile: Dockerfile}): docker build com Dockerfile do repositorio

Isso substitui a logica anterior de image_mode (bind-mount/self-contained) e app_type: docker-build.

A geracao automatica de Dockerfile por stack (generate_dockerfile) foi deprecada. Projetos devem fornecer seu proprio Dockerfile.

Retrocompatibilidade

Configs antigas continuam funcionando:

  • image_mode: self-contained -> tratado como modo BUILD com Dockerfile default
  • app_type: docker-build -> tratado como modo BUILD
  • image_mode: bind-mount + image: -> modo IMAGE (sem mudanca)

Adicionado

  • Deploy lock: Previne deploys concorrentes no mesmo app via .deploy.lock
  • Audit log no pipeline: Deploy registra sucesso e falha no deploy-audit.json com versao, duracao e erro
  • Output estruturado: Modulo output/ com envelope JSON para integracao programatica
  • Campo source.directory: Permite especificar subdiretorio do repo para deploy (ex: directory: . usa raiz inteira)
  • Deteccao de VOLUME em imagens: Override automatico com bind mount explicito para evitar overlay de anonymous volumes
  • Metodo is_build_mode() para deteccao do modo de deploy
  • Metodo is_image_mode() para deteccao do modo de deploy
  • Metodo validate_deploy_mode() para validar configuracao
  • Metodo effective_build_config() para obter config de build efetiva
  • Auto-mount de data/, logs/, keys/ em ambos os modos

Corrigido

  • Audit log backward compat: Campo instance em DeployAuditEntry com serde(default) para logs antigos
  • source.directory ignorado: Campo nao existia em InstanceSourceConfig, serde ignorava silenciosamente
  • Read-only filesystem: Mount de source como :ro impedia Docker de criar mountpoints para VOLUMEs
  • Health check timeout: Usava healthcheck.timeout (per-check) como tempo total de espera. Agora calcula corretamente: start_period + (interval + timeout) * retries + buffer
  • prepare() em build mode: Usa diretorio completo do pull/ ao inves de auto-detectar subdiretorios

Deprecado

  • generate_dockerfile() — projetos devem fornecer Dockerfile
  • write_dockerfile() — nao utilizado pelo pipeline
  • Campo image_mode — usar image: ou build: diretamente
  • app_type: docker-build — usar secao build:

[1.2.2] - 2026-02-19

Adicionado

Comando `runner edit`

Novo comando para editar configuracao de aplicacoes registradas:

# Ver configuracao atual
runner edit runner-docs/front --show

# Mudar branch trackeada
runner edit runner-docs/front --branch dev

O comando inclui validacoes automaticas:

  • Verifica se repositorio git existe
  • Verifica se branch existe no remote
  • Verifica se branch contem .deploy.yml

Melhorias no Cleanup

  • Output colorido com destaque para nomes de containers
  • Flag --verbose para logs detalhados
  • Deteccao de conflitos quando multiplas configs matcham mesmo container

Self-Update com URL

Suporte para atualizar para versao especifica via URL:

runner self-update --url https://runner.ccs.systems/rc/v1.2.2-rc.8/runner

CLI Padronizado

Todos os comandos agora aceitam app como argumento posicional:

# Antes (verbose)
runner fetch --app runner-docs/front

# Agora (direto)
runner fetch runner-docs/front
runner health runner-docs/front
runner logs tail runner-docs/front
runner env status runner-docs/front

Preparacao de Conteudo Melhorada

A funcao prepare() agora suporta repositorios com conteudo na raiz:

  • Detecta automaticamente diretorios: dist/, guides/, content/, docs/, public/, src/
  • Fallback para raiz do projeto quando nenhum diretorio especifico existe
  • Identifica corretamente src/ como estrutura de versoes vs codigo fonte

Corrigido

  • Duplicatas na deteccao de containers orfaos
  • Comparacao de versao usando SHA256 quando versoes sao iguais
  • Conteudo na raiz (ex: guides/) nao era copiado para diretorio de versao
  • Comandos fetch, health, logs, env exigiam flag --app desnecessariamente

[1.2.0] - 2026-02-16

Adicionado

Novos Source Types

Tres novos tipos de fonte para instancias:

Tipo Descricao Uso
tag Deploy de tags Git Releases (v*)
local Deploy de diretorio local Hotfixes, builds externos
image Deploy de imagem Docker pronta Imagens pre-built
instances:
  release:
    source:
      type: tag
      tag_pattern: "v*"

  hotfix:
    source:
      type: local
      path: /builds/hotfix/

  canary:
    source:
      type: image
      image: ghcr.io/org/app:canary

Canary Scheduler (Auto-Promocao)

Sistema de promocao automatica de canary deployments:

instances:
  production:
    canary:
      enabled: true
      auto_promote:
        enabled: true
        initial_weight: 10
        increment: 10
        interval: 5m
        final_weight: 100
        health_check_before: true
      abort_on:
        - health_check_failed
        - error_rate_above:5%
        - latency_p99_above:500ms

Novos comandos CLI:

  • runner canary status <project> -i <instance> - Ver status do scheduler
  • runner canary pause <project> -i <instance> - Pausar promocao
  • runner canary resume <project> -i <instance> - Retomar promocao
  • runner canary abort <project> -i <instance> - Abortar e rollback

Notificacoes v2

Arquitetura extensivel com trait-based channels:

HTTP Webhook - Notificacoes para qualquer endpoint HTTP:

# config.yml
integrations:
  flare:
    type: http_webhook
    url: https://flareapp.io/api/deploys
    method: POST
    auth:
      type: api_key
      key: "${FLARE_API_TOKEN}"
      location: query
      param_name: api_token

Tipos de autenticacao suportados:

  • none - Sem autenticacao
  • api_key - Token em query param ou header
  • bearer - Header Authorization: Bearer
  • oauth2 - Client credentials flow (auto-renew)
  • basic - Username:password
  • hmac - Assinatura HMAC-SHA256

Structured Log - Audit log em JSON Lines:

logs:
  deploy_events:
    type: structured_log
    path: /opt/runner/logs/deploy-events.jsonl
    format: json_lines

Comando `runner serve`

Servidor de triggers para webhooks e cron:

runner serve                    # Auto-detecta modo
runner serve --mode webhook     # Apenas webhook server
runner serve --mode cron        # Apenas cron scheduler
runner serve --mode both        # Ambos
runner serve --port 9000        # Porta customizada

O webhook server:

  • Recebe eventos GitHub/GitLab/Bitbucket
  • Verifica assinatura HMAC-SHA256
  • Aciona deploys automaticos
  • Endpoint de health em /health

CLI Filters por Labels CCS

Filtragem de containers por labels:

runner list --project=meu-app
runner list --instance=production
runner list --project=frontend-* --json

Labels CCS usadas:

  • ccs.systems/project
  • ccs.systems/instance
  • ccs.systems/version

Alterado

Deploy Output Melhorado

Novo formato de output com badges de progresso:

[1/6] [PREPARE]   Preparando deploy...
[2/6] [ARTIFACT]  Copiando artefatos...
[3/6] [ENV]       Gerando ambiente...
[4/6] [SECURITY]  Verificando segurança...
[5/6] [CONTAINER] Subindo container...
[6/6] [PROMOTE]   Promovendo versao...

═══════════════════════════════════════
  ✓ DEPLOY CONCLUIDO
  App:     projeto/production
  Versao:  v1.2.0
  URL:     https://app.projeto.com.br
═══════════════════════════════════════
  • deploy --verbose (-V): Modo verbose com logs tecnicos detalhados
  • deploy --force (-f): Forca redeploy mesmo se versao ja implantada
  • Box de resumo: Exibe app, versao e URL ao final do deploy

Fetch Output em Portugues

Status traduzidos para melhor legibilidade:

Icone Status Significado
✓ atualizado App atualizada
↑ desatualizado Update disponivel
↓ baixado Artefatos baixados
✗ erro Erro no processo
  • fetch output ordenado: Resultados ordenados alfabeticamente por projeto/sistema
  • fetch --force (-F): Nova flag para forcar git pull antes de carregar config

Corrigido

  • Recursao infinita na copia: Skip de diretorios de instancia (production, staging, pr-*) e commit hashes
  • app_type no Dockerfile: Usa config.app_type ao inves de hardcoded "sys"
  • Duracao de healthcheck: Normaliza valores numericos para formato Docker (30 → 30s)
  • Tipo "docs": Novo tipo para imagens de documentacao (MkDocs, PimDocs)

[1.1.13] - 2026-02-16

Alterado

  • fetch output colorido: Cores ANSI para facilitar leitura
    • Verde (✓) para sucesso e "up to date"
    • Amarelo (↑) para updates/pulled
    • Vermelho (✗) para erros
    • Ciano para sugestoes de solucao
    • Dim para informacoes secundarias (versoes)

[1.1.12] - 2026-02-16

Alterado

  • fetch output limpo por padrao: Output mais amigavel para usuarios
    • Logs INFO escondidos por padrao (usa nivel WARN)
    • Formato limpo com alinhamento e indicadores visuais
    • Sugestoes de fix para erros comuns
    • Nova flag --verbose para output detalhado
    • Uso: runner fetch (limpo) ou runner fetch --verbose (detalhado)

[1.1.11] - 2026-02-16

Adicionado

  • fetch --force (-F): Nova flag para forcar git pull antes de carregar config
    • Resolve problema de .deploy.yml desatualizado no servidor
    • Util para migracao de formato legado para v1.1.10
    • Uso: runner fetch --force ou runner fetch -F

[1.1.10] - 2026-02-16

Corrigido

  • Bug 11 - Auto-mount volumes: Respeita volumes explicitos do .deploy.yml
    • Se usuario configura keys:/app/keys:ro, usa essa config
    • Default para keys/ mudou de :rw para :ro (seguranca)
    • logs/ e data/ continuam como :rw
    • Evita erro de mount duplicado

[1.1.9] - 2026-02-16

Corrigido

  • app_type parsing: Alias type/app_type para compatibilidade YAML
  • env_files: Container carrega .env e .secrets automaticamente
  • volumes bind mount: Volumes relativos convertidos para paths absolutos

[1.1.8] - 2026-02-16

Corrigido

  • extract_version(): Suporte a campos nested em TOML (ex: package.version em Cargo.toml)

    • Parser agora detecta secoes [section] e busca chaves dentro delas
    • Compativel com JSON e YAML via dot notation
  • copy_dir_recursive(): Previne recursao infinita durante copia de diretorios

    • Detecta quando o destino esta dentro da origem
    • Pula diretorios de versao (v*) e symlinks
  • docker-build: Suporte completo no pipeline de deploy

    • Nova struct DeployBuildConfig para configuracao de build
    • Metodo is_docker_build() para deteccao de tipo
    • Healthcheck sem curl (usa bash /dev/tcp)
    • Build de imagem a partir do Dockerfile do projeto

[1.1.7] - 2026-02-15

Alterado

  • Storage: Default apps_path alterado de /apps para /data/apps

    • Segue estrutura de storage do ecossistema CCS
    • Compativel com migracao para Kubernetes
    • Configs existentes com apps_path: /apps continuam funcionando
  • Self-Update: Novas flags

    • --rc: Atalho para --channel rc
    • -C, --check: Apenas verifica se ha atualizacao (nao baixa)

[1.1.6] - 2026-02-14

Adicionado

  • Canais GitOps - Suporte a canais stable e rc no .gitops.yml
  • Releases estaveis na raiz: /runner
  • Release candidates em: /rc/runner
  • Versionamento automatico por tag pattern (vX.Y.Z vs vX.Y.Z-rc.N)

Alterado

  • Self-update agora suporta flag --rc para baixar release candidates
  • Estrutura de storage separada por canal

[1.1.5] - 2026-02-14

Corrigido

  • Versao interna do binario corrigida para corresponder a tag

[1.1.2] - 2026-02-13

Corrigido

  • folder_name no .gitops.yml corrigido para processar corretamente

[1.1.1] - 2026-02-13

Alterado

  • URL de download alterada para runner.ccs.systems
  • .gitops.yml atualizado para nova estrutura de storage

[1.1.0] - 2026-02-12

Adicionado

MCP Server (Model Context Protocol)

Implementacao completa do servidor MCP para integracao com Claude Code.

Comandos CLI:

runner mcp serve      # Inicia servidor MCP
runner mcp install    # Instala config MCP
runner mcp uninstall  # Remove config MCP
runner mcp status     # Mostra info do servidor

Tools disponiveis (25+):

Categoria Tools
Deploy runner_deploy, runner_rollback
Apps runner_list_apps, runner_app_status, runner_add_app
Instances runner_instances, runner_versions, runner_destroy_instance
Canary runner_weights, runner_set_weight, runner_promote
Staging runner_stage_deploy, runner_stage_list, runner_stage_destroy, runner_stage_cleanup
Environment runner_env_status, runner_env_set, runner_env_validate
Config runner_validate_config, runner_generate_config
Maintenance runner_fetch, runner_cleanup
Logs runner_logs_history, runner_logs_tail

Resources (9):

  • runner://instructions/* - Documentacao estatica
  • runner://status/* - Status dinamico (apps, staging, config)

Prompts (5):

  • setup_new_project - Setup de novo projeto
  • diagnose_deploy - Diagnostico de falhas
  • canary_workflow - Workflow de canary
  • migrate_config - Migracao para v1.0.0
  • staging_pr - Deploy de PR

Self-Update

Modulo de auto-atualizacao do binario.

runner self-update                   # Atualizar para versao estavel
runner self-update --force           # Forcar reinstalacao
runner self-update --channel rc      # Usar release candidate
runner self-update --path /opt/bin   # Caminho customizado

Features:

  • Download automatico da versao mais recente
  • Suporte a canais (stable/rc)
  • Backup automatico do binario atual
  • Verificacao de integridade

[1.0.0] - 2026-02-05

Alterado (Breaking Changes)

Esta versao remove completamente o codigo legado e padroniza a arquitetura baseada em instancias.

Novo Formato `.deploy.yml`

O novo formato usa instances{} ao inves de containers[]:

# Novo formato (v1.0.0)
project: meu-projeto
port: 8000
networks: [public, mysql]
environment:
  KEY: value
instances:
  production:
    domain: app.site.com
    source:
      type: dist
    keep_versions: 3

Comparacao de mudancas:

Legado v1.0.0
containers[] instances{}
type: front-static (enum) app_type: "front-static" (string)
versioning.keep_versions instances.X.keep_versions
environment.templates environment: {} (HashMap)
deploy.strategy Traefik dynamic config

Mantido

  • InstanceDeployConfig - Estrutura principal
  • GlobalConfig - Config global
  • RunnerConfig - Config em /opt/runner/config.yml
  • Traefik module - Geracao de YAML dinamico
  • Containers module - Docker run/build

[0.6.0] - 2026-02-04

Adicionado

Novos Modulos

  • runner/mod.rs - Configuracao global do Runner
  • traefik/mod.rs - Geracao de YAML dinamico para Traefik
  • containers/mod.rs - Operacoes Docker via docker run/build

Novos Comandos CLI

  • runner init - Inicializa ambiente Runner
  • runner instances <project> - Lista instancias
  • runner versions <project> -i <name> - Lista versoes
  • runner weights <project> -i <name> - Mostra pesos
  • runner weight <project> <inst> <ver> <weight> - Ajusta peso
  • runner promote <project> -i <name> <version> - Promove versao
  • runner destroy <project> -i <name> - Remove instancia
  • runner cleanup - Limpa versoes antigas
  • runner migrate --app <path> - Migra estrutura legada

[0.5.0] - 2026-01-28

Adicionado

  • Comando reset - Reset de deploy
  • Flag --hard - Reset incluindo .env e .secrets
  • Flag --skip-tests - Pula testes apos redeploy
  • Hook post_healthy - Comandos apos health check
  • Variaveis em hooks - ${VERSION}, ${APP_PATH}, ${CONTAINER}
  • Comando stage reset - Reset de staging

[0.4.0] - 2026-01-26

Adicionado

  • Production Compose Generation - Geracao automatica de docker-compose.yml
  • Staging de PRs - Ambientes efemeros para Pull Requests
  • Comando stage deploy - Deploy de PR
  • Comando stage list - Lista stagings
  • Comando stage destroy - Remove staging
  • Comando stage cleanup - Cleanup automatico
  • Template variables - ${PR_NUMBER}, ${REPO}, ${PROJECT}, etc.

[0.3.0] - 2026-01-26

Adicionado

  • Comando add - Registra novas apps
  • Comando fetch - Busca atualizacoes
  • Tipo docker-build - Deploy com Dockerfile customizado
  • Auto Deploy - Configuracao per-app
  • State Management - Persistencia de estado
  • Audit Log - Log global de fetch

Alterado

  • Binario renomeado de cicd-runner para runner
  • Path padrao alterado para /opt/runner/

Removido

  • Webhook server - Substituido por add/fetch

[0.2.0] - 2025-01-25

Adicionado

  • MANUAL.md - Documentacao para clients
  • Webhook server - Endpoint para GitHub push events
  • HMAC-SHA256 - Validacao de assinaturas

[0.1.0] - 2025-01-25

Adicionado

  • CI/CD Runner em Rust - Implementacao inicial
  • Blue-Green Deployment - Teste antes de promover
  • Versioned Deploys - Diretorios versionados
  • Environment Templates - Sistema de templates
  • Custom Deploy Types - wordpress-plugin, mautic, custom
  • Multi-Source Downloads - Download de multiplos repos
  • Telegram/Discord Notifications - Notificacoes
  • Playwright Integration - Testes E2E
  • Automatic Rollback - Rollback em falha
  • Health Checks - Verificacao de saude
By Borlot.com.br on 16/02/2026