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.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ânciarunner 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.31-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 sdr.emploia.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 → fsyncrenamefsync 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 do SDR (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 sdr-api --file ./sdr-prod.env --yes
# → carrega 10 keys + propaga pra sdr-web (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 customer Agency-Arms (sweepstake-backend): 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 customer Agency-Arms: deploy de sweepstake-backend (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 pelo Agency-Arms 24h apos o cutover do meet 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 meet. 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-renametry_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 GitAuthHelperfetch_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://x-access-token@github.com/... 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 meet 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 meet teve manifest com networks: [meet] (pre-prefix) committado no upstream + wizard-answers.yml com project_prefix: meet. Mapper produziria [meet_meet] mas o compose-hash batia → container caia em meet (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:  ["meet"]
    expected: ["meet_meet"]
  network_aliases:
    current:  {}
    expected: {"meet_meet": ["agent"]}
  environment:
    (... lista completa de chaves ...)

Fix: runner reconcile meet-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 meet:

  • 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 meet (Agency-Arms). Reportado em /opt/runner/BUGS_REPORT_meet_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 14runner 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_meet_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_meet_v2.26.2_attempt_2026-05-21.md (tentativa 3 de cutover do projeto Agency-Arms meet).

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 (meet<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_meet_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 meet. Reportado em /opt/runner/BUGS_REPORT_meet_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 meet (Agency-Arms).

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 (operator Agency-Arms, repo meet):

  • 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.
  • Lifecycleinit (default false), entrypoint, stop_signal, stop_grace_period, pull_policy (ausente = daemon default).
  • Network avançadohostname, dns, expose, network_aliases (map network→aliases).
  • Storage / Hardwaretmpfs, 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 (warmd-front, warmd-sys, warmd-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 warmd-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 deployedbuild_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 directoryextract_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 warmd/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://x-access-token@github.com':
 WARN git push failed: remote: Invalid username or token.
fatal: Authentication failed for 'https://github.com/devborlot/warmd-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?warmd.mundexico.com}}"
  MYSQL_HOST: "{{::MySQL Host?mysql}}"
  MYSQL_PORT: "{{::MySQL Port?3306}}"
  MYSQL_USER: "{{::MySQL User?warmd}}"
  MYSQL_DATABASE: "{{::MySQL Database?warmd}}"
  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 reserva/bolixebackend), 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.: reserva_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: bolixebackend.emploia.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:
    reserva-production-v1-0-3:
      loadBalancer:
        servers:
          - url: http://reserva_production_9b5c0b9:8080
        healthCheck:
          path: /api/health
          interval: 10s
          timeout: 5s
          hostname: bolixebackend.emploia.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/bunfront.

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 emploia)

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

hogi-worker ficou em loop com 12+ commits chore: update encrypted secrets em ~30min, todos seguidos de deploys falhados pelo mesmo pre-flight check (secrets C6BANK 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.sweepapex.com}}" 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: [meet] 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 middleware-241 (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.ymlrunner 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 positivoskeyboard, 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 pipelinecleanup_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