Meu agente publicou uma página antes de a imagem existir
O endpoint de contents do GitHub é um arquivo por commit, um commit por deploy. Publique uma página e a imagem assim e elas competem. A Git Data API entrega a mudança inteira como um único commit, ou nada.
Um agente meu publicou um artigo num site em produção semana passada e, por cerca de um minuto, a página ficou no ar enquanto a imagem de capa devolvia um 404. O texto tinha subido num commit e a imagem em outro. A Vercel fez deploy do primeiro push, o artigo ficou no ar, e a imagem ainda estava a caminho. Dois commits, dois deploys, um buraco feio no meio.
É isso que ninguém te conta quando você deixa um agente publicar fazendo commits no git: cada escrita separada é um commit separado, e num host que faz deploy sozinho, cada commit é um deploy separado. Se o seu artigo e a imagem sobem em dois commits, existe uma janela em que o artigo está no ar e quebrado.
Foi assim que fechei essa janela.
A montagem
Eu publico guias de SEO para a Aldeia, um dos meus projetos, através de um hub de revisão. Uma skill escreve os arquivos do guia localmente, roda os testes e o build, e então faz POST de um rascunho para o hub. Quando eu aprovo no hub, o hub chama de volta um endpoint dentro do app da Aldeia, e esse endpoint faz commit dos arquivos para a main pela API do GitHub. A Vercel vê o push e faz deploy. Nenhum git push manual em todo o fluxo.
Cada guia é mais de um arquivo. Uma página nova, um append num arquivo de dados que lista todos os guias, e um ajuste no teste desse arquivo:
app/guia/<slug>/page.jsx (página nova)
src/lib/seoGuides.js (adicionar a entrada do guia)
src/lib/seoGuides.test.js (subir a contagem)
Depois adicionei imagens de capa. Agora são quatro arquivos, e um deles é binário.
O jeito óbvio, e por que ele morde
O jeito óbvio de fazer commit de um arquivo pela API do GitHub é o endpoint de contents: PUT /repos/{owner}/{repo}/contents/{path}. Uma chamada, um arquivo, um commit. É o exemplo de qualquer tutorial.
Quatro arquivos são quatro dessas chamadas. Quatro commits. Na Vercel, quatro deploys competindo entre si, e nenhuma garantia de que a página caia depois da imagem para a qual ela aponta. A primeira vez que publiquei um guia com capa, o deploy da página ganhou a corrida e a imagem deu 404 até o build seguinte se acertar.
Você não resolve isso ordenando as chamadas com cuidado. Os deploys são assíncronos e você não controla qual termina primeiro. O único conserto real é parar de fazer quatro commits.
Um commit com a Git Data API
O GitHub tem uma API de mais baixo nível que monta um commit a partir das suas peças: blobs, uma árvore, e então o objeto commit. Você monta tudo e depois move o branch uma única vez. A mudança inteira de vários arquivos cai como um único commit, o que significa um único deploy.
O formato é: criar um blob para cada arquivo, construir uma árvore que referencia todos os blobs sobre a árvore atual, criar um commit apontando para essa árvore, e então fazer patch no ref do branch para o novo commit.
async function commitFiles(token, files, message) {
const ref = await ghGet(`/git/refs/heads/${BRANCH}`, token)
const baseSha = ref.object.sha
const baseCommit = await ghGet(`/git/commits/${baseSha}`, token)
const treeItems = await Promise.all(files.map(async ({ path, content, encoding = 'utf-8' }) => {
const blob = await ghPost('/git/blobs', { content, encoding }, token)
return { path, mode: '100644', type: 'blob', sha: blob.sha }
}))
const newTree = await ghPost('/git/trees',
{ base_tree: baseCommit.tree.sha, tree: treeItems }, token)
const newCommit = await ghPost('/git/commits',
{ message, tree: newTree.sha, parents: [baseSha] }, token)
// mover o branch uma vez — é a única escrita à qual o deploy reage
await ghPatch(`/git/refs/heads/${BRANCH}`, { sha: newCommit.sha }, token)
return newCommit.sha
}
Quatro arquivos, um movimento de ref, um deploy. O artigo e a imagem ficam no ar no mesmo instante, ou não ficam.
O arquivo binário na mesma árvore
A imagem de capa são bytes, não texto, e tem que viajar na mesma árvore que o markup ou você volta a ter dois commits. O endpoint de blobs aceita um campo encoding que é utf-8 ou base64. O truque é fazer isso por arquivo em vez de fixar, para que texto e binário caibam numa só chamada de árvore.
É o encoding = 'utf-8' padrão do trecho acima. Os arquivos de texto não passam nada e recebem UTF-8. A imagem passa o seu base64 e a codificação certa:
const filesToCommit = [
{ path: jsxPath, content: jsxContent },
{ path: 'src/lib/seoGuides.js', content: updatedGuides },
{ path: 'src/lib/seoGuides.test.js', content: updatedTest },
]
if (heroImageB64 && heroImagePath) {
const cleanPath = heroImagePath.replace(/^\//, '')
filesToCommit.push({
path: `public/${cleanPath}`,
content: heroImageB64,
encoding: 'base64',
})
}
await commitFiles(token, filesToCommit, message)
O hub manda a imagem como hero_image_b64 mais um hero_image_path, o endpoint não decodifica nada (base64 é exatamente o que a API de blobs quer), e a imagem aterrissa em public/ no mesmo commit que a página que a exibe.
A segunda coisa que quebrou: confie menos nos dados
Tem uma lição mais afiada escondida embaixo desta. O primeiro guia que publiquei assim quebrou o build, e não por causa da imagem.
A entrada do arquivo de dados é gerada pela skill e inserida pelo endpoint. A entrada gerada já terminava em vírgula. Minha inserção adicionou outra. O resultado foi },, no array, que o JavaScript lê como um elemento de array indefinido. O prerender do sitemap percorreu o array e lançou um TypeError ao buscar .url no buraco que a vírgula dupla deixou.
O conserto foi uma linha, mas o que importa é o hábito:
// a entrada gerada pode ou não trazer uma vírgula final — remova-a,
// e então adicione exatamente uma
const normalizedEntry = seoEntry.replace(/,\s*$/, '')
const updatedGuides = guidesContent.replace(MARKER, `\n ${normalizedEntry},${MARKER}`)
Quando um agente gera um fragmento e o seu código o emenda dentro de um arquivo real, trate esse fragmento como entrada não confiável mesmo tendo escrito o agente. Você não controla o formato exato dele entre execuções. Normalize antes de concatenar. Uma vírgula perdida que você nunca escreveria à mão é justamente o tipo de coisa que um gerador te entrega na terceira execução.
O que usei
- Git Data API do GitHub:
/git/blobs,/git/trees,/git/commits,/git/refspara um commit atômico. - Uma codificação de blob
base64por arquivo para a capa binária viajar na mesma árvore que o markup. - Um passo de normalização em cada fragmento gerado antes de emendá-lo no código fonte.
Para levar
Se um agente publica fazendo commits no git, faça a mudança inteira virar um único commit. O endpoint de contents é um arquivo por commit e um commit por deploy, que é exatamente como você termina com uma página no ar apontando para uma imagem que ainda não está lá. A Git Data API custa algumas chamadas a mais e compra a única coisa que importa aqui: a página e tudo de que ela precisa aterrissam juntos, ou nada aterrissa. E seja lá o que o agente gerou, normalize antes que toque um arquivo real. Você escreveu o gerador, não escreveu a saída dele.
Deixe seu e-mail e eu te aviso quando o próximo sair, com as ferramentas e fluxos que eu de fato uso. Sem spam.
Mais tutoriais ↗