Nibleaf: documentação vinda do GitHub a cada push
Olá meus Unicórnios! 🦄✨
Documentação boa é aquela que fica ao lado do código. Você mexe numa
função, abre o arquivo .md na mesma pasta, corrige o
parágrafo, e tudo entra no mesmo commit. Só que aí vem a parte
chata: alguém precisa levar esse texto para um site bonito, que os seus
usuários consigam ler sem clonar o repositório. 😅
É exatamente esse pedaço que o Nibleaf resolve. Você aponta o painel
para um repositório do GitHub, ele lê os arquivos Markdown de uma pasta e
transforma cada um numa página do seu site de documentação. E dá para
fazer isso acontecer sozinho: a cada push, o site se
atualiza.
Neste artigo eu conecto o repositório, importo as páginas e deixo o webhook funcionando. Mas antes tenho de contar uma limitação que muda a forma como você vai trabalhar, e que é melhor descobrir agora do que depois de perder um texto. 🙈
Nibleaf: documentação self-hosted com DockerA instalação completa numa VPS: as variáveis obrigatórias, o primeiro acesso e os dois erros que travam todo mundo.blog.palomamacetko.com.br⚠️ A limitação que você precisa saber antes de tudo
Vou começar pelo fim, porque essa parte decide se a ferramenta serve para você. A sincronização entre o GitHub e o Nibleaf é de mão única.
Traduzindo: o que você escreve no repositório vira página no site. Mas o que você escrever no editor visual do Nibleaf não volta para o GitHub. Nunca. Não existe botão de "enviar de volta", não é uma configuração escondida, é uma decisão do projeto.
E a consequência prática é cruel: se você editar uma página pelo editor bonitinho e depois alguém der um push no repositório, o próximo import passa por cima do que você escreveu. O seu texto some, sem aviso e sem lixeira.
Não é pegadinha escondida no meio da documentação. A própria tela de conexão avisa, em letras pequenas, logo abaixo do título:
Está lá: "One-way, edits in Nibleaf are not pushed back". Mão única, edições no Nibleaf não são empurradas de volta.
Junto com ela vem uma segunda limitação, igualmente importante: só funciona com repositório público. Não existe o botão de "entrar com a sua conta do GitHub" que autorizaria o Nibleaf a ler um repositório privado. Ele simplesmente baixa o repositório como qualquer visitante faria, e um repositório privado responde "não existe" para visitantes.
Então a regra para trabalhar em paz é uma só, e vale a pena escrever num post-it: escolha um lado. Ou a sua documentação mora no Git e o Nibleaf é só a vitrine, ou ela mora no editor e você nem conecta repositório nenhum. Misturar os dois é o caminho para perder trabalho. 😬
🗺️ Onde fica a tela
Com o painel aberto e o seu projeto escolhido, o caminho é Settings, na barra da esquerda, e depois Git, dentro do bloco DEPLOYMENT do menu do meio.
Repare que existem dois itens parecidos ali, e é fácil clicar no errado. O Import, logo abaixo, serve para subir arquivos de uma vez só, à mão. O Git é o que a gente quer: a ligação permanente com o repositório.
A tela oferece três abas de origem: GitHub,
GitLab e Public Git URL. A terceira é a
saída para quem hospeda o código em outro lugar (um Gitea da própria
empresa, por exemplo): você cola o endereço https completo do
repositório em vez do nome. Aqui eu fico no GitHub.
📝 Preenchendo os quatro campos
São poucos campos, e três deles já vêm com um valor bom. O que confunde é o primeiro.
| Campo | O que escrever |
|---|---|
Repository path | O dono e o nome do repositório, separados por barra: dono/repositorio. Não é a URL completa. |
Branch | O branch de onde vem o texto. O padrão main serve para quase todo mundo. |
Content path | A pasta, dentro do repositório, onde moram os arquivos .md. O padrão é docs. |
Import into branch | Para qual versão do seu site o texto vai. Deixe em Default branch. |
Import into language | Em qual idioma as páginas entram. Deixe em Default language. |
O campo do repositório merece atenção porque a tentação de colar a URL inteira é grande. O que ele espera é só o pedaço final do endereço:
# Se o endereco do seu repositorio e este:
# https://github.com/dono/repositorio
# entao no campo escreva apenas:
dono/repositorio
Sobre o Content path: ele é o motivo de o Nibleaf não
transformar o seu README.md e o CONTRIBUTING.md
em páginas do site. Só o que estiver dentro da pasta indicada vira
documentação. Se os seus arquivos estão na raiz do repositório, apague o
docs e deixe o campo vazio; se estão em
documentacao/, escreva esse nome.
Preenchido, é só clicar em Connect repository.
📥 O import, e o que ele traz
Conectar não importa nada ainda: só guarda o endereço. O que traz o texto é o botão Import now, no bloco Pipeline que apareceu.
Cliquei, esperei uns quinze segundos (ele está clonando o repositório lá do outro lado do mundo, tenha paciência ☕) e apareceu o aviso verde no canto:
Imported 43 new, updated 0. Quarenta e três páginas
que estavam no repositório como arquivos .md agora são
páginas do painel. Zero atualizadas porque nenhuma existia antes.
Essa contagem é a parte mais útil da mensagem, e vale entender os dois
números: new são arquivos que o Nibleaf nunca tinha
visto, e updated são os que já existiam e mudaram. Da
segunda importação em diante, o normal é ver 0 new e um
punhado de updated. Se você esperava atualizar uma página e
ela veio como new, é sinal de que o arquivo mudou de nome ou de
pasta no repositório, e agora você tem duas.
O bloco Pipeline conta a história em três etapas, e a ordem delas explica muita coisa:
| Etapa | O que significa |
|---|---|
| Import content | O Markdown do repositório virou página aqui dentro, como rascunho. |
| Build | A versão do site que foi montada da última vez. |
| Live | O que os seus leitores estão vendo agora, de verdade. |
Repare no detalhe cruel dessa captura: importei 43 páginas e o Build e o Live continuam dizendo pages: 6. 😳 O import não publica. Ele enche os rascunhos e para ali, esperando que alguém decida que aquilo está bom. Quem publica é o botão Publish, lá no editor, ou a chave que vou ligar mais adiante.
Indo ao editor, dá para ver as páginas que chegaram, com a estrutura de pastas do repositório virando a árvore da esquerda:
As pastas do repositório viraram pastas na árvore, e cada arquivo
.md virou uma página. É literalmente a sua pasta
docs, agora navegável e bonita. 🎉
🪝 Fazendo acontecer sozinho: o webhook
Clicar em Import now toda vez que alguém mexe na documentação é o tipo de tarefa que a gente esquece na terceira semana. O jeito certo é o GitHub avisar o Nibleaf sozinho.
Isso se chama webhook, e a ideia é simples: você dá ao GitHub um endereço, e ele bate nesse endereço toda vez que alguém empurra código. É um telefonema automático, não um robô ficando de olho.
Todo o material está no bloco Deploy on push, logo abaixo do Pipeline. Ele tem três passos numerados, e o primeiro já está verde porque você acabou de conectar o repositório.
🔑 Gerando o segredo
Antes de ir ao GitHub, clique em Generate secret. Ele sorteia uma chave longa e a guarda do lado do Nibleaf:
Repare que o campo do segredo aparece como pontinhos, e que existe um olhinho para revelá-lo e um botão para copiar. Trate essa chave como senha: quem a tiver consegue mandar o seu site reimportar quando quiser. Na captura acima ela está escondida de propósito. 🔒
E para que serve esse segredo? Para o Nibleaf ter certeza de que quem está batendo na porta é mesmo o GitHub. O endereço do webhook é público (precisa ser, senão o GitHub não o alcança), então sem uma prova qualquer pessoa que descobrisse o endereço poderia disparar imports no seu servidor.
A prova funciona assim: o GitHub calcula uma assinatura do conteúdo da
mensagem usando o segredo e a manda num cabeçalho chamado
X-Hub-Signature-256. O Nibleaf refaz a mesma conta do lado de
cá e compara. Bateu, é o GitHub; não bateu, a porta continua fechada.
🔗 Colando tudo no GitHub
Copie a Payload URL do painel (o botão de copiar ao lado do campo evita erro de digitação) e o segredo. Ela tem esta cara, com o identificador do seu projeto no fim:
http://SEU_IP_AQUI:4310/api/public/git/webhook/SEU_ID_DE_PROJETO
Agora, no GitHub, o caminho é:
| Onde | O que fazer |
|---|---|
| Página do repositório | Clique em Settings, na barra de cima (é o do repositório, não o da sua conta). |
| Menu da esquerda | Clique em Webhooks, e depois no botão Add webhook. |
| Payload URL | Cole o endereço que você copiou do Nibleaf. |
| Content type | Troque para application/json. O padrão é outro e não serve. |
| Secret | Cole o segredo gerado no painel. |
| Eventos | Deixe marcado "Just the push event", que já é o padrão. |
O Content type é o campo que mais gente erra, porque o GitHub
vem com application/x-www-form-urlencoded selecionado. Nesse
formato ele embrulha a mensagem de um jeito que o Nibleaf não lê, e a
resposta é um erro pedindo justamente que o corpo seja JSON.
Salvo o webhook, o GitHub manda na hora uma mensagem de teste do tipo
ping, só para ver se o endereço responde. Ela aparece na
aba Recent Deliveries do próprio webhook, e é ali que você
confere se deu certo.
🧪 Vendo o webhook responder
Dá para conferir cada resposta do webhook sem depender do GitHub, e eu
acho isso muito mais didático do que ficar dando push para testar. A ideia
é montar a mesma chamada que o GitHub monta, à mão, com o
curl.
Primeiro guarde os três valores em variáveis, para não repetir cadeia gigante em todo comando (troque pelos seus):
SEGREDO=cole_aqui_o_segredo_gerado_no_painel
URL=http://SEU_IP_AQUI:4310/api/public/git/webhook/SEU_ID_DE_PROJETO
CORPO='{"ref":"refs/heads/main"}'
O CORPO é um pedacinho do que o GitHub manda de verdade
num push. A mensagem completa dele tem dezenas de campos (autor, commits,
mensagem), mas para decidir o que fazer o Nibleaf olha um só: o
ref, que diz qual branch recebeu o push.
Agora a assinatura, que é a parte que parece difícil e não é:
ASSINATURA=$(printf %s "$CORPO" | openssl dgst -sha256 -hmac "$SEGREDO" | sed 's/^.*= //')
Lendo da esquerda para a direita: o printf escreve o corpo
da mensagem, o openssl dgst -sha256 -hmac calcula a
assinatura dele usando o segredo como chave, e o sed corta o
rótulo que o openssl escreve na frente do resultado, deixando
só a cadeia de caracteres.
Com isso montado, comecei pelo caminho da falha, que é o que interessa de verdade. Mandei uma assinatura falsa, cheia de zeros:
curl -s -w "\n%{http_code}\n" -X POST "$URL" \
-H 'Content-Type: application/json' \
-H 'X-GitHub-Event: push' \
-H 'X-Hub-Signature-256: sha256=0000000000000000000000000000000000000000000000000000000000000000' \
-d "$CORPO"
{
"error": {
"code": "http:unauthorized",
"message": "Invalid webhook signature."
}
}
401
401, porta fechada. 🚪 É a prova de que o segredo está
fazendo trabalho de verdade, e não é enfeite de tela: sem a assinatura
certa, ninguém dispara import nenhum no seu servidor.
Agora com a assinatura correta, imitando o ping que o
GitHub manda ao salvar o webhook:
curl -s -w "\n%{http_code}\n" -X POST "$URL" \
-H 'Content-Type: application/json' \
-H 'X-GitHub-Event: ping' \
-H "X-Hub-Signature-256: sha256=$ASSINATURA" \
-d "$CORPO"
{
"data": {
"ok": true,
"event": "ping"
}
}
200
É essa resposta que o GitHub mostra como um tiquinho verde no Recent Deliveries depois que você salva o webhook.
🌿 O push que não faz nada (e está certo)
Antes do teste final, um comportamento que vale conhecer para não achar que quebrou. Troquei o branch da mensagem para um branch qualquer, gerei a assinatura de novo (ela é do conteúdo, então muda junto) e mandei:
CORPO='{"ref":"refs/heads/rascunho"}'
ASSINATURA=$(printf %s "$CORPO" | openssl dgst -sha256 -hmac "$SEGREDO" | sed 's/^.*= //')
{
"data": {
"ok": true,
"ignored": "branch",
"branch": "rascunho"
}
}
200
Repare na palavra ignored. O Nibleaf recebeu, conferiu a
assinatura, viu que o push foi para um branch que não é o de publicação, e
ignorou de propósito. Respondeu 200 assim mesmo, para o
GitHub não achar que houve erro e ficar tentando de novo.
Isso é ótimo na prática: você pode trabalhar à vontade nos seus branches de rascunho, empurrando quantas vezes quiser, que o site publicado não se mexe. Só o branch que você configurou dispara a atualização. 🎯
🚀 O push de verdade
Voltando ao main, com a assinatura certa:
{
"data": {
"accepted": true,
"branch": "main"
}
}
202
E aqui aparece o detalhe mais bonito da implementação. O código não é
200, é 202, que em HTTP quer
dizer "recebi e vou cuidar disso, mas não agora".
O motivo é bem prático. O GitHub desiste de uma entrega que demora mais de uns dez segundos, e marca como falha. Clonar um repositório e importar dezenas de arquivos demora mais que isso. Se o Nibleaf segurasse a resposta até terminar, o GitHub concluiria que o webhook está quebrado e ficaria repetindo a entrega, disparando import em cima de import.
Então ele faz o contrário: responde 202 na hora e vai
importar em segundo plano. Nos registros do servidor dá para ver os dois
tempos, e a diferença é gritante:
"method":"POST","path":"/api/public/git/webhook/...","status":202,"durationMs":20.76
"projectId":"...","branch":"main","files":43,"imported":0,"updated":43,"msg":"git webhook sync imported"
A resposta ao GitHub saiu em 20 milissegundos. O import de verdade terminou quatro segundos depois, muito tempo depois de o GitHub já ter ido embora satisfeito, e trouxe as 43 páginas atualizadas. 🥳
Essa arquitetura tem uma consequência que confunde: como a resposta sai
antes de o trabalho começar, o 202 não garante que o
import deu certo. Ele garante só que a entrega foi aceita. Se o
import falhar (o repositório sumiu, a pasta mudou de nome), o GitHub vai
continuar mostrando o tiquinho verde, feliz da vida.
O lugar onde a verdade aparece é o painel. Depois de um push, o rótulo do Pipeline muda de "Last imported" para "Last push sync", e é assim que você sabe que foi o webhook que rodou, não alguém clicando no botão. Deu erro? O painel mostra o motivo ali mesmo, e os registros do servidor têm o detalhe:
cd /opt/nibleaf
docker compose -f docker-compose.prod.yml logs server --tail 40
🎚️ Auto-publish: rascunho ou no ar?
Falta a terceira parte do bloco, e ela é uma chavinha só, desligada por padrão. O texto embaixo dela explica em cinco palavras: "Off = pushes only update drafts".
É a resposta para aquele mistério lá de cima, das 43 páginas importadas com o Live parado em 6. Com a chave desligada, o push atualiza os rascunhos e para por aí. Alguém precisa entrar no painel e clicar em Publish para o site mudar.
| Auto-publish | O que acontece a cada push |
|---|---|
| Desligada (padrão) | O texto entra como rascunho. O site publicado continua igual até alguém publicar à mão. |
| Ligada | Cada push publica uma versão nova do site sozinho, sem ninguém no meio. |
Qual escolher depende de quem escreve. Numa equipe onde a documentação
passa por revisão no pull request, ligar faz todo sentido: o
texto já foi revisado antes de chegar no main, e segurar de
novo no painel é burocracia dobrada.
Já se várias pessoas empurram direto no branch principal, deixar desligada é a rede de proteção: um parágrafo pela metade fica de rascunho esperando alguém olhar, em vez de aparecer no ar para os seus usuários. 🙈
Uma coisa que me agradou: publicar cria uma versão do site, e o painel guarda as anteriores. Então mesmo com a chave ligada e um push infeliz, dá para voltar para a versão de antes pelo painel. O que essa volta não faz é desfazer o import, e é a mesma história do começo do artigo: o rascunho continua com o texto novo, porque a fonte da verdade é o repositório. Para consertar de verdade, conserte lá e empurre de novo. 🔁
Por hoje é só, meus unicórnios! 🦄✨
Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟
Perguntas frequentes
O que eu escrevo no editor do Nibleaf volta para o GitHub?
Dá para conectar um repositório privado?
Para que serve a chave Auto-publish?
Por que o webhook responde 202 e não 200?
O que acontece se eu empurrar para outro branch?
{"ok": true, "ignored": "branch"} com HTTP 200. Só o branch de publicação dispara o import.O que aparece se a assinatura do webhook estiver errada?
401 com a mensagem Invalid webhook signature.. O Nibleaf confere o cabeçalho X-Hub-Signature-256, que é o HMAC-SHA256 do corpo da requisição assinado com o segredo, então ninguém dispara import sem ele.Leia também
OpenSEO: instalando na VPS e auditando um site
Como instalar o OpenSEO, alternativa aberta ao Semrush, numa VPS com Docker, auditar um site de verdade e entender a chave da DataForSEO.
changedetection.io: vigiando mudanças em sites na sua VPS
Instale o changedetection.io numa VPS com Docker, vigie um site com filtro CSS, receba o aviso no celular e acabe com o alarme falso do RSS.
Project NOMAD: seu servidor offline de emergência
Instale o Project NOMAD numa VPS Ubuntu e tenha Wikipédia, mapas, cursos e IA funcionando sem nenhuma conexão com a internet.