
Colocar a release na CI, duas vezes
A GeekBye v1.8.4 moveu as builds de release para a CI no macOS e no Windows. O que o changelog não diz é que já tentamos isso uma vez antes, apagamos um mês depois por causa do custo dos runners, e só fizemos pegar na segunda vez — porque, a essa altura, o script de release já funcionava na mão.
O changelog da v1.8.4 tem uma linha que soa a pura arrumação: «As builds de release agora vêm da CI, tanto no macOS quanto no Windows.» Lê-se como o tipo de coisa que acontece uma vez, limpa, e na qual nunca mais se pensa. Não foi. A equipe já tinha colocado as releases na CI uma vez — e arrancado fora. Este lançamento é a segunda tentativa, e a razão de ter pegado é a verdadeira lição de engenharia, então vamos contar na ordem.
A primeira tentativa, e por que ela morreu
Lá em outubro de 2025 havia um release.yml. Ele era disparado no push de um tag — você finca um tag v*.*.* e um runner de macOS subia, compilava e publicava. Fazia a própria preparação de assinatura na mão: um passo que decodificava o certificado de assinatura, criava e destravava um keychain temporário, importava o cert para que o codesign conseguisse achá-lo, e um passo de limpeza correspondente no final. Funcionava. Também era só para macOS — as releases de Windows não estavam na CI de jeito nenhum.
No dia 4 de novembro, ele foi apagado. A mensagem do commit é incomumente franca sobre o porquê: «remove GitHub Actions release workflow to conserve minutes / macOS runners cost 10x multiplier (100 billed minutes per 10-minute release). Releases will be done locally.» Essa é toda a história da primeira tentativa em uma frase. O GitHub fatura o tempo de runner de macOS a dez vezes a taxa do Linux, e uma release disparada por tag significa que cada tag — incluindo os descartáveis, os re-tags, os de «opa, esqueci de subir a versão» — gasta caladamente cem minutos faturados. Automação que você não controla é automação que gasta dinheiro enquanto você dorme. Então as releases voltaram para o Mac de um desenvolvedor pelos três meses e meio seguintes.
Onde os problemas difíceis de fato foram resolvidos
Aqui está a parte que faz a segunda tentativa funcionar, e ela aconteceu inteiramente fora da CI. Durante aqueles três meses e meio de releases locais, o scripts/release.js — o simples script Node que um desenvolvedor roda na mão — absorveu cada problema de empacotamento que o app tinha, uma correção de cada vez:
- Restore white-label. Este é um codebase white-label que compila tanto GeekBye quanto Pavleur a partir de uma única fonte, o que significa que uma release troca campos de
package.jsone assets de ícones pelos do produto alvo. Duas correções ensinaram o script a colocar tudo de volta depois, para que compilar o Pavleur não deixasse a sua árvore git suja com a identidade do Pavleur. - Windows dual-arch. Uma correção para compilar os dois instaladores de Windows, x64 e arm64, em vez de um.
- O contorno para Windows sem assinatura. As versões mais novas do electron-builder ignoravam o flag de config que deveria desativar a assinatura no Windows, então o script aprendeu a forçar uma build sem assinatura definindo
CSC_IDENTITY_AUTO_DISCOVERY=falseno ambiente — uma dobra que você só encontra ao esbarrar nela. - Autodetecção de notarização. O script aprendeu a olhar o seu ambiente e decidir: tem credenciais de assinatura? Assina. Tem também credenciais de notarização da Apple? Notariza. Nenhuma? Build sem assinatura. Nenhum flag para lembrar; a presença dos secrets é a configuração.
Nada disso é glamouroso. Tudo isso é o tipo de coisa que, se te surpreende dentro de um runner de CI, te custa uma hora de push-esperar-falhar-ler-logs por tentativa — a faturamento de 10x. Resolvido em um Mac na sua frente, cada um custa um minuto.
A segunda tentativa: sessenta e seis linhas
Quando a CI voltou na v1.8.4, o workflow tinha 66 linhas, e a sua qualidade definidora é o pouco que ele faz. O commit descreve isso sem rodeios: «Manual workflow_dispatch trigger that builds both platforms in parallel, reusing existing release.js script. macOS builds are signed and notarized, Windows builds are unsigned.» Cada decisão de design nele é uma cicatriz da primeira tentativa:
workflow_dispatch, não disparado por tag. Você inicia uma release clicando em «Run workflow». Um humano controla cada minuto pago de macOS. O problema de custo que matou a primeira versão se resolve simplesmente não automatizando o gatilho — o único ponto onde a automação era ativamente prejudicial.- Uma entrada
product. O dispatch recebe um dropdown —geekbyeoupavleur— para que o mesmo workflow publique qualquer uma das marcas. A costura white-label vai até o próprio botão de release. - Dois jobs paralelos.
build-macnomacos-latest,build-winnowindows-latest, rodando ao mesmo tempo. Windows na CI é genuinamente novo aqui; o workflow de outubro nunca o compilou. - Nenhuma lógica de assinatura no YAML. Este é o ponto todo. Não há malabarismo com keychain, nem passo de import de cert, nem limpeza. O job do Mac roda
node scripts/release.js <product> --publishe o job do Windows roda o mesmo com--no-sign. Tudo o que o primeiro workflow fazia na mão em YAML agora vive no script que já funciona. O workflow é um orquestrador, não uma implementação.
As duas plataformas querem coisas opostas, e os jobs refletem isso com honestidade. macOS compila sob um hardened runtime, assinado e notarizado pelo caminho embutido do electron-builder (@electron/notarize), com as credenciais fornecidas pelos secrets do repositório e autodetectadas pelo script. Windows compila instaladores NSIS sem assinatura para x64 e arm64. Um workflow, dois jobs, duas noções completamente diferentes de «pronto».
A prova está no que não aconteceu
É assim que você sabe que a ordem estava certa: depois dos dois ajustes do mesmo dia que nomearam as execuções e corrigiram o nome de um secret de token, o release.yml não foi tocado de novo por quatro meses. Nenhum hotfix de falha de assinatura. Nenhum aperto por rejeição de notarização. Nenhum pânico de «o binário nativo está faltando no runner». Para um pipeline de assinatura-e-notarização de código — o gênero de CI mais famoso pela instabilidade — quatro meses de silêncio é algo quase inédito.
Estava quieto porque o barulho já tinha acontecido em um lugar mais barato. As brigas que normalmente se desenrolam numa aba de logs de CI, a faturamento de 10x, um force-push de cada vez, tinham se desenrolado nos Macs dos desenvolvedores durante a era local. A CI não precisou ser onde o empacotamento foi depurado, porque o empacotamento já estava depurado. Essa é a tese em uma linha: só tire a build do seu Mac depois que o seu Mac tiver parado de te surpreender.
O único acoplamento que ainda pode morder
Não é totalmente sem um ponto fraco, e vale nomeá-lo porque é sutil. Os binários Swift dos quais o app de macOS depende — OCR, captura de tela, os transcritores — estão commitados no git. Mas a release não publica as cópias commitadas; a build as recompila no runner. E uma barreira check-swift-version.js faz a release inteira falhar na hora se o compilador não for a versão fixada. Nesta release essa fixação era Swift 6.2.x, e nada no workflow a instala — o job simplesmente confia que o Swift padrão do macos-latest bate. No dia em que o GitHub subir a imagem do seu runner para além da fixação, a release para, não porque algo esteja errado com o app, mas porque o pipeline está caladamente acoplado a uma imagem de máquina que ele não controla. É o único detalhe de implementação que este workflow enxuto não empurrou para baixo, para algo que seja seu.
A outra metade da release: payloads menores
A release trouxe uma feature não relacionada que merece menção, porque é um exemplo limpo de correção de latência por subtração. A ação Assist Me estava mandando ao backend mais contexto do que precisava em cada request. Um commit enxugou duas coisas: o histórico de transcrição enviado junto caiu das últimas 30 entradas para as últimas 15, e os arquivos de contexto do perfil do usuário — injetados no system prompt em toda chamada — foram limitados na marra a 4,000 caracteres com um marcador de truncamento. Menos para serializar, menos para subir, menos para o modelo ler antes de começar a responder, que é a métrica que o commit otimiza: o time-to-first-token. Em nome da honestidade: o commit afirma o ganho de velocidade mas não o mede — não há número de antes/depois no histórico, então trate isso como um corte bem fundamentado, não como uma vitória com benchmark.
Três coisas que este lançamento nos ensinou
- A CI é um invólucro fino sobre um script que já funciona. O workflow de 66 linhas não tem lógica de assinatura própria; ele chama um script que três meses de releases locais já tinham depurado. Ponha o mecanismo em algo que você consiga rodar na mão, e deixe a CI só decidir quando rodá-lo.
- Automatize o trabalho, não necessariamente o gatilho. A primeira tentativa morreu porque releases disparadas por tag gastavam sozinhas minutos de runner faturados a 10x. Um
workflow_dispatchmanual mantém a automação e remove a parte que custava dinheiro — às vezes o humano no loop é a feature. - Depure onde iterar é barato. Cada briga de empacotamento resolvida no Mac de um desenvolvedor é uma briga que nunca acontece numa aba de logs de CI a dez vezes o preço. Saia do Mac por último, não primeiro.
Para o capítulo anterior da história v1, publicar trinta idiomas sem rede de segurança (v1.8.3); e para todo o arco, a anatomia de entregar software até a perfeição.