Ensinando o blog-manager a escrever, não só ler, repositórios do GitHub
O blog-manager era somente leitura em relação aos repositórios de blog desde a primeira versão: escanear um repositório, analisar o frontmatter, sindicar para o Medium e o Dev.to. Toda escrita acontecia no navegador, manualmente, no próprio repositório. Essa foi a primeira issue que pediu para o app fazer commit de algo de volta.
O que eu estava tentando fazer
O escopo foi deliberadamente estreito: não o editor, só a primitiva por baixo dele. Um método que recebe um caminho, algum conteúdo e uma mensagem de commit, e transforma isso numa chamada PUT /repos/{owner}/{repo}/contents/{path}, criando um arquivo se ele for novo, atualizando se você passar o sha atual do blob. O editor que de fato chama isso é um trabalho separado, para mais adiante.
O que eu construí
Já existe um Github::ContentClient, um wrapper artesanal e pequeno sobre Net::HTTP, sem Octokit, que o post scanner e o código de sindicação usam para GET. Eu o estendi em vez de escrever uma classe nova. O método antigo get construía sua própria requisição e embutia a lógica de tratamento de header/resposta; eu extraí isso para um request(req) privado e compartilhado, que tanto o get quanto um novo put chamam, então o caminho de escrita ganha de graça os mesmos headers de autenticação, a mesma costura de teste (um proc connection: que os testes trocam) e o mesmo tratamento de código de resposta:
def put_file(path, content, message:, sha: nil)
body = { message: message, content: Base64.strict_encode64(content) }
body[:sha] = sha if sha
response = put(contents_path(path), body)
{ sha: response.dig("content", "sha"), commit_sha: response.dig("commit", "sha") }
end
A parte interessante foi o mapeamento de erros. A documentação do GitHub é vaga sobre qual código de status exatamente significa “seu sha está desatualizado” versus “sua requisição estava malformada”, os dois vivem sob o mesmo guarda-chuva geral de “isso não funcionou”. Eu mapeei 409 para um novo ConflictError, distinto do Error genérico já existente no client, e deixei o 422 sob o erro genérico. O raciocínio: 409 é de fato o caso de lock otimista, alguém mais fez commit desde que você leu o arquivo, e a resposta certa na UI é “recarregue e deixe o usuário tentar de novo”. 422 significa que algo está errado na própria requisição (falta o sha numa atualização, conteúdo inválido), tentar de novo com o mesmo sha não resolve isso, então dizer ao editor “recarregue do repositório” seria ativamente enganoso.
Decisões que tomei e por quê
Serialização de escrita adiada. O escopo original da issue incluía serializar escritas por blog para o GitHub não ver commits concorrentes no mesmo repositório. Eu não construí isso. Ainda não existe chamador nenhum, nem editor, nem job, nada invocando put_file em produção, e um mecanismo de serialização só faz sentido depois que você conhece o padrão de invocação (job assíncrono vs. escrita síncrona no controller). As tabelas do Solid Queue do blog-manager já têm solid_queue_semaphores migrado e sem uso, então limits_concurrency está ali esperando por qualquer job futuro que precise dele. Construir isso agora seria chutar uma interface para um chamador que não existe. Além disso: um único desenvolvedor, um único processo worker, escritas concorrentes no mesmo blog estão perto de fisicamente impossíveis por enquanto.
Estender, não duplicar. Eu poderia ter escrito uma classe Github::ContentWriter paralela. Não fiz isso porque precisaria reimplementar exatamente a mesma infraestrutura de autenticação/conexão/mapeamento de erros que o cliente de leitura já tem, só para manter “leitura” e “escrita” conceitualmente separadas. Uma classe, um conjunto de headers, uma costura de teste.
O que me surpreendeu
A issue referenciava um arquivo de PRD, com IDs de requisito específicos, que não existe em lugar nenhum do repositório. O PRD de verdade lista explicitamente “editar o conteúdo do post dentro do app” como um não-objetivo. Isso não é bem uma contradição, é mais evidência de que a direção mudou desde que esse PRD foi escrito, e a issue simplesmente não teve o rastro documental atualizado para acompanhar. Sinalizei isso e segui em frente em vez de tratar como bloqueio, a própria issue já era específica o bastante para eu trabalhar em cima dela.
Uma surpresa maior: enquanto eu ia atrás do trabalho deixado por uma issue relacionada no mesmo arquivo de documentação, encontrei uma branch local desatualizada havia oito dias e cerca de vinte issues mescladas, com um stash em cima dela. Comparando com a main atual apareceram cerca de 120 arquivos e milhares de linhas de divergência, o que teria revertido um pedaço de trabalho já publicado se mesclada como estava. Mas o stash em cima dela, um diff pequeno e limpo, só de documentação, aplicou de forma limpa na main atual, sem nenhum conflito, porque um stash não está preso à ascendência de commits da branch do jeito que a própria branch está. Vale lembrar: uma branch desatualizada e o stash sentado em cima dela não são o mesmo artefato, e o segundo ainda pode valer a pena resgatar mesmo quando o primeiro precisa ser descartado.
Uma surpresa menor: rodei o diff por uma revisão automatizada de 8 ângulos antes de mesclar (correção linha a linha, auditoria de comportamento removido, rastreamento de chamadores entre arquivos, além de checagens de reuso/simplificação/eficiência/altitude/convenções). Os ângulos de correção voltaram limpos, o refactor preservou o comportamento. Mas ela pegou duas coisas reais que eu tinha introduzido sem perceber: eu tinha usado travessões longos na nova seção de documentação (uma regra rígida de “nunca” que tenho para toda a minha própria escrita), e um docstring em put_file que só repetia o próprio valor de retorno uma linha acima do código que já mostrava isso. Três dos oito ângulos convergiram de forma independente para a mesma pequena duplicação, o template de caminho da Contents API construído separadamente em três métodos, o que valeu a pena corrigir precisamente porque três lentes não relacionadas chegaram lá de forma independente.
O que vem a seguir
A UI do editor propriamente dita, e o que quer que invoque put_file em produção, é trabalho futuro separado, junto com a decisão de serialização que isso vai forçar. Também ainda em aberto: os PATs de blog existentes têm escopo somente leitura; o token de cada blog precisa ser re-escopado para Contents: Read and write na UI do GitHub antes que uma escrita de fato tenha sucesso contra aquele repositório. Não existe API para isso, é um passo manual por blog, sempre que o editor estiver pronto para usá-lo.
Leitura relacionada
Scanning de posts pela API do GitHub: o que permissão mínima realmente significa
Construindo o motor de sincronização de posts do blog-manager. Net::HTTP, PATs de granularidade fina e uma investigação no __NEXT_DATA__ para verificar a resposta.
O que acontece quando um job faz broadcast para ninguém
Fechando o loop da imagem de destaque: frontmatter só por inserção, uma guarda que pegou divergência real na primeira execução, e um broadcast sem ouvinte.
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.
Você também pode achar útil
Proton 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 maisProton Drive
Armazenamento em nuvem criptografado, 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 maisNordPass
Gerenciador de senhas da equipe por trás da NordVPN, com um plano gratuito.
Como afiliado da NordPass, ganho com compras qualificadas.
Saiba mais