Pular para o conteúdo
Docker

Nibleaf: documentação vinda do GitHub a cada push

Uma coruja escriba mágica tira um pergaminho de um baú marcado com um símbolo de ramificação e o entrega, por um feixe de luz de mão única, a um unicórnio diante de um livro de documentação aberto

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:

A aba Git do Nibleaf ainda vazia, com as três abas GitHub, GitLab e Public Git URL e os campos de caminho do repositório, branch e pasta de conteúdo

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.

CampoO que escrever
Repository pathO dono e o nome do repositório, separados por barra: dono/repositorio. Não é a URL completa.
BranchO branch de onde vem o texto. O padrão main serve para quase todo mundo.
Content pathA pasta, dentro do repositório, onde moram os arquivos .md. O padrão é docs.
Import into branchPara qual versão do seu site o texto vai. Deixe em Default branch.
Import into languageEm 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:

O painel do Nibleaf com o repositório conectado, o bloco Pipeline com as etapas Import content, Build e Live, e o aviso verde Imported 43 new, updated 0

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:

EtapaO que significa
Import contentO Markdown do repositório virou página aqui dentro, como rascunho.
BuildA versão do site que foi montada da última vez.
LiveO 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:

O editor do Nibleaf com a árvore de páginas importadas à esquerda, mostrando as pastas API Reference, Contributing, Getting Started e Product vindas do repositório

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:

O bloco Deploy on push do Nibleaf com a Payload URL, o campo Secret preenchido e escondido por pontinhos e a chave Auto-publish desligada

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 é:

OndeO que fazer
Página do repositórioClique em Settings, na barra de cima (é o do repositório, não o da sua conta).
Menu da esquerdaClique em Webhooks, e depois no botão Add webhook.
Payload URLCole o endereço que você copiou do Nibleaf.
Content typeTroque para application/json. O padrão é outro e não serve.
SecretCole o segredo gerado no painel.
EventosDeixe 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-publishO que acontece a cada push
Desligada (padrão)O texto entra como rascunho. O site publicado continua igual até alguém publicar à mão.
LigadaCada 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?
Não. A própria tela avisa que a sincronia é de mão única: o repositório manda no site, e edições feitas no editor visual não sobem para o Git. Pior: um novo import passa por cima delas. Ou você escreve no repositório, ou escreve no editor.
Dá para conectar um repositório privado?
Não nesta versão. A importação é só de repositório público, porque não existe login por OAuth no GitHub. Documentação de código fechado precisa ficar em repositório público separado, ou ser escrita direto no editor.
Para que serve a chave Auto-publish?
Ela decide se o push já vai para o ar. Desligada (o padrão), o import só atualiza os rascunhos e alguém precisa clicar em Publish. Ligada, cada push publica uma nova versão do site sozinho.
Por que o webhook responde 202 e não 200?
Porque ele aceita a entrega e faz o import em segundo plano. O GitHub desiste da entrega em cerca de 10 segundos, e clonar o repositório demora mais que isso. Aqui a resposta saiu em 20 milissegundos e o import terminou quatro segundos depois.
O que acontece se eu empurrar para outro branch?
Nada, de propósito. O Nibleaf compara o branch do push com o configurado e responde {"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?
HTTP 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