Fazendo backfill da realidade num rastreador de sindicação
Há alguns dias eu lancei a interface que mostra quais dos meus posts estão no Medium. Aí abri em produção e ela dizia que nenhum estava. 169 posts, zero entradas no Medium. O modelo de dados estava certo; o banco de produção simplesmente nunca tinha sido informado do que o ambiente de dev já sabia.
A parte fácil: rodar de novo o backfill codificado
O trabalho de reconciliação anterior vivia em rake tasks, não no histórico de shell de alguém, e isso valeu a pena imediatamente. medium:seed_published e medium:seed_scheduled já estavam embarcadas na imagem de produção, e backfill_medium! só faz o estado avançar, nunca retroceder, então rodá-las contra o prod foi um não-evento: 76 posts marcados como publicados, 8 agendados, 0 divergências de slug. Se algum dia você ficar tentado a fazer um ajuste de dados pontual à mão no console, esse é o argumento para escrever a rake task descartável em vez disso - o “pontual” rodou duas vezes.
Um pré-requisito apareceu no caminho: produção não tinha uma API key do Postiz, o que significava que o job de reconciliação horário vinha pulando silenciosamente desde o deploy. Eu passei a chave do banco de dev via stdin, direto, para que ela nunca tocasse um terminal, argumentos de processo, ou o histórico de shell.
O reconciler estava quebrado em todo lugar e ninguém percebeu
O passo quatro do meu plano era “rodar o reconciler de RSS para capturar as URLs reais.” Ele quebrou: ArgumentError: not an HTTP URI. O reconciler monta a URL do feed a partir do campo profile da integração com o Postiz e presumia que era uma URL. O Postiz devolve só o handle puro, “vdaluz”. URI.parse("vdaluz") não tem host, o código montava "vdaluz/feed", e o Net::HTTP recusava. Mesma falha em dev, o que significa que o job horário vinha falhando em todo lugar, e a coisa que deveria marcar meus 8 posts agendados como publicados quando eles fossem ao ar simplesmente não funcionava.
O fix foi pequeno, mas a revisão o deixou melhor. Minha primeira versão mapeava qualquer profile sem host para um feed de handle do medium.com. A revisão apontou que URI.parse não retorna host para NENHUMA string sem scheme, então um domínio próprio guardado como “blog.example.com” viraria silenciosamente medium.com/feed/@blog.example.com, buscaria um 404, seria interpretado como um feed vazio, e reportaria sucesso para sempre. A versão que foi ao ar só trata strings no formato de handle (sem pontos, sem barras) como handles, levanta um erro nomeado para qualquer coisa não reconhecida, e levanta erro em respostas que não são 2xx. Um palpite errado agora falha ruidosamente na fila de jobs em vez de desativar a reconciliação silenciosamente.
Raspando URLs de um dashboard renderizado no cliente
66 dos posts recuperados no backfill não tinham URL do Medium - a interface mostrava “Publicado no Medium” sem nada para clicar. O Medium matou a própria API, e o RSS só expõe as 10 histórias mais recentes, então a lista completa existe em exatamente um lugar: o dashboard de histórias com sessão logada. Salvar essa página te dá um shell React de 99KB sem nenhum dado de história. O truque que funcionou:
// on medium.com/me/stories/public, scrolled to the bottom:
copy(document.body.outerHTML)
// then: pbpaste > /tmp/published_dom.html
O DOM renderizado tem URLs públicas diretas para cada história. Um script em Nokogiri combinou 76 das 77 histórias com posts por título normalizado - as divergências eram o Medium anexando meu nome a alguns títulos (com um espaço não separável, é claro) e um título que eu tinha reescrito desde 2022. A 77ª é uma duplicata só do Medium que eu me recusei a adivinhar. Um script runner idempotente depois, produção tem links em todos os 76 posts publicados. O Medium devolve 403 para qualquer cliente que não seja um navegador, então verifiquei por proveniência em vez de curl.
A falha de CI que era três falhas disfarçadas de uma só
Fazer o merge do fix do reconciler levou mais tempo do que escrevê-lo. O build de CI falhou com bundle: command not found - o runner tinha acabado de ser realocado para um container dedicado e ficou faltando o passo de configuração do PATH. O fix documentado anexava PATH=...:$PATH ao .env do runner, e depois de reiniciar o serviço o CI falhou mais cedo ainda, com tar: command not found. Foi aí que aprendi que o actions runner não expande variáveis no .env: os jobs agora tinham um PATH contendo uma string literal com cifrão e nenhum /usr/bin. O mecanismo real é o arquivo .path, que guarda o PATH completo, literal. Terceira falha: Errno::ENOENT - mise durante o bundle install, porque o plugin de RubyGems do mise chama mise reshim depois de cada instalação de gem, então o próprio binário do mise também precisa estar no PATH do job.
Cada fix movia a falha um passo mais fundo dentro do job, que é a barra de progresso mais honesta que um CI pode te dar. A parte que eu continuo reaprendendo: o passo do runbook tinha “funcionado” antes só porque ninguém tinha reiniciado o serviço do runner depois de editar o .env. Configuração que nunca foi recarregada é configuração que nunca foi testada.
O que vem a seguir
Os 8 posts agendados publicam semanalmente até o fim de agosto; a reconciliação horária, já corrigida, deve virar cada um para publicado com sua URL, sem intervenção manual. Esse é o teste real de toda essa cadeia. A correção do runbook está registrada, e o provisionamento do runner está sendo codificado em Ansible para que a lição do PATH fique registrada uma vez, em vez de reaprendida.
Leitura relacionada
Uma correção de desvio de documentação que não foi tão chata quanto parecia
Três itens da auditoria que cada um virou outra coisa: uma alegação meio corrigida, uma recuperação de senha silenciosamente morta, e um e-mail de staging que apontaria para produção.
Um 500 escondido dentro das rotas isoladas de uma engine montada
O painel de jobs retornava um 500 em vez de uma página de login: helpers de rota sem qualificação resolvem contra a engine, não contra a aplicação. Uma linha, mais o gêmeo dormente dela.
Apagando código morto, e pegando um motivo errado para uma resposta certa
Uma issue de limpeza com uma justificativa errada, uma varredura de documentação que não era necessária, e a credencial órfã que uma revisão pegou.
Você também pode achar útil
Proton Mail
E-mail criptografado de ponta a ponta, com arquitetura de acesso zero.
Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).
Saiba maisNordPass
Gerenciador de senhas da equipe por trás da NordVPN, com um plano gratuito.
Como afiliado da NordPass, ganho com compras qualificadas.
Saiba maisProton Pass
Gerenciador de senhas focado em privacidade, da equipe por trás do Proton Mail.
Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).
Saiba mais