Resumo

Este fluxo publica alterações do Notion sem usar webhook público. O cron da hospedagem executa um script no servidor; esse script chama a API do GitHub e dispara um workflow mínimo com workflow_dispatch. O workflow consulta rapidamente a data source do Notion, compara o estado atual com o último hash salvo em cache e só dispara o deploy principal se houver mudança.

Vantagens:

  • não precisa expor endpoint público para o Notion
  • não depende do schedule do GitHub Actions
  • evita rodar build quando nada mudou
  • mantém o deploy pesado separado da checagem rápida
  • usa o cron já disponível no painel da hospedagem
  • mantém token, repositório e branch fora do script

Use este fluxo se o site é estático e precisa rebuildar quando o conteúdo do Notion mudar. O processo não é instantâneo: ele roda no intervalo definido no cron da hospedagem.

Antes de começar, tenha em mãos:

  • workflow principal de deploy já funcionando
  • workflow principal com workflow_dispatch
  • acesso ao repositório no GitHub
  • permissão para criar secrets e variables no GitHub
  • token da integração do Notion
  • ID da data source do Notion
  • acesso SSH ao servidor
  • acesso ao cron da hospedagem
  • branch principal confirmada, normalmente main

Fluxo

cron da hospedagem executa ~/bin/notion-change-check.sh
> script carrega ~/.env do servidor
> script chama a API do GitHub
> GitHub dispara o workflow Notion Change Check via workflow_dispatch
> runner consulta a data source do Notion
> runner monta um hash com id, last_edited_time, archived e in_trash de cada página
> runner compara esse hash com o cache da última execução
> se nada mudou, o workflow termina sem build
> se mudou, o workflow salva o novo hash
> workflow mínimo dispara o workflow principal via workflow_dispatch
> workflow principal executa sync, build e deploy

Workflow do GitHub Actions

No projeto local:

mkdir -p .github/workflows
nano .github/workflows/notion-change-check.yml

Cole o conteúdo abaixo:

name: Notion Change Check

on:
  workflow_dispatch:

permissions:
  actions: write
  contents: read

concurrency:
  group: notion-change-check
  cancel-in-progress: true

jobs:
  check:
    name: Check Notion changes
    runs-on: ubuntu-latest
    env:
      NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
      NOTION_DATA_SOURCE_ID: ${{ secrets.NOTION_DATA_SOURCE_ID }}
      NOTION_VERSION: "2026-03-11"
      DEPLOY_WORKFLOW_FILE: ${{ vars.DEPLOY_WORKFLOW_FILE || 'ci-cd-static-pipeline.yml' }}
      DEPLOY_BRANCH: ${{ github.ref_name }}

    steps:
      - name: Validate configuration
        run: |
          if [ -z "$NOTION_TOKEN" ]; then
            echo "NOTION_TOKEN is empty or was not configured in GitHub Secrets."
            exit 1
          fi

          if [ -z "$NOTION_DATA_SOURCE_ID" ]; then
            echo "NOTION_DATA_SOURCE_ID is empty or was not configured in GitHub Secrets."
            exit 1
          fi

      - name: Build Notion state hash
        id: notion_state
        run: |
          python - <<'PY' >> "$GITHUB_OUTPUT"
          import hashlib
          import json
          import os
          import urllib.error
          import urllib.request

          token = os.environ["NOTION_TOKEN"]
          data_source_id = os.environ["NOTION_DATA_SOURCE_ID"]
          notion_version = os.environ["NOTION_VERSION"]
          url = f"https://api.notion.com/v1/data_sources/{data_source_id}/query"
          headers = {
              "Authorization": f"Bearer {token}",
              "Content-Type": "application/json",
              "Notion-Version": notion_version,
          }

          cursor = None
          pages = []

          while True:
              payload = {"page_size": 100}
              if cursor:
                  payload["start_cursor"] = cursor

              request = urllib.request.Request(
                  url,
                  data=json.dumps(payload).encode("utf-8"),
                  headers=headers,
                  method="POST",
              )

              try:
                  with urllib.request.urlopen(request, timeout=20) as response:
                      data = json.loads(response.read().decode("utf-8"))
              except urllib.error.HTTPError as error:
                  message = error.read().decode("utf-8")
                  raise SystemExit(f"Notion API returned {error.code}: {message}")

              for result in data.get("results", []):
                  if result.get("object") == "page":
                      pages.append({
                          "id": result.get("id"),
                          "last_edited_time": result.get("last_edited_time"),
                          "archived": result.get("archived", False),
                          "in_trash": result.get("in_trash", False),
                      })

              if not data.get("has_more"):
                  break

              cursor = data.get("next_cursor")

          state = json.dumps(
              sorted(pages, key=lambda page: page["id"] or ""),
              sort_keys=True,
              separators=(",", ":"),
          )
          digest = hashlib.sha256(state.encode("utf-8")).hexdigest()

          print(f"hash={digest}")
          print(f"page_count={len(pages)}")
          PY

      - name: Restore previous Notion state
        id: notion_cache
        uses: actions/cache/restore@v5
        with:
          path: .notion-state
          key: notion-state-${{ steps.notion_state.outputs.hash }}
          restore-keys: |
            notion-state-

      - name: Stop when nothing changed
        if: steps.notion_cache.outputs.cache-hit == 'true'
        run: |
          echo "No Notion changes detected."
          echo "Pages checked: ${{ steps.notion_state.outputs.page_count }}"

      - name: Save current Notion state
        if: steps.notion_cache.outputs.cache-hit != 'true'
        run: |
          mkdir -p .notion-state
          printf '%s\n' "${{ steps.notion_state.outputs.hash }}" > .notion-state/hash
          printf '%s\n' "${{ steps.notion_state.outputs.page_count }}" > .notion-state/page-count

      - name: Cache current Notion state
        if: steps.notion_cache.outputs.cache-hit != 'true'
        uses: actions/cache/save@v5
        with:
          path: .notion-state
          key: notion-state-${{ steps.notion_state.outputs.hash }}

      - name: Dispatch deploy workflow
        if: steps.notion_cache.outputs.cache-hit != 'true'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          echo "Notion changes detected."
          echo "Pages checked: ${{ steps.notion_state.outputs.page_count }}"

          gh workflow run "$DEPLOY_WORKFLOW_FILE" \
            --repo "${{ github.repository }}" \
            --ref "$DEPLOY_BRANCH"

No nano, salve e saia com: Ctrl + O, Enter, Ctrl + X.

Depois:

git add .github/workflows/notion-change-check.yml notion-change-check.md
git commit -m "Adiciona checagem de mudanças no Notion"
git push

Passo a passo da configuração

1. Confirmar o workflow principal

O workflow principal precisa aceitar workflow_dispatch.

Exemplo:

on:
push:
branches:
- main
workflow_dispatch:

Neste projeto, o checador usa ci-cd-static.yml como workflow principal padrão.

Se quiser usar outro workflow sem editar o arquivo, crie uma variável do repositório:

Settings > Secrets and variables > Actions > Variables > New repository variable

Criar:

  • DEPLOY_WORKFLOW_FILE Valor: ci-cd-static.yml

Se o deploy roda a partir de outra branch, ajuste BRANCH no ~/.env do servidor. O workflow mínimo usa a mesma branch recebida no workflow_dispatch.

2. Criar os secrets do Notion no GitHub

No GitHub, acesse:

Settings > Secrets and variables > Actions > New repository secret

Crie:

  • NOTION_TOKEN Valor: token da integração do Notion
  • NOTION_DATA_SOURCE_ID Valor: ID da data source usada pelo projeto

Esses valores são usados apenas dentro do GitHub Actions.

3. Criar token do GitHub para o cron da hospedagem

O cron da hospedagem precisa chamar a API do GitHub para disparar o workflow mínimo. Para isso, crie um Fine-grained Personal Access Token.

No GitHub, acesse:

GitHub > Foto do perfil > Settings > Developer settings > Personal access tokens > Fine-grained tokens > Generate new token

Preencha:

  • Token name: Cron - <nome-projeto>
  • Expiration: escolha um prazo de validade
  • Resource owner: usuário ou organização dona do repositório
  • Repository access: Only selected repositories
  • Selected repositories: repositório do projeto

Em Repository permissions, configure:

  • Actions: Read and write
  • Contents: Read
  • Metadata: Read-only

Depois clique em Generate token e copie o valor. O GitHub mostra esse token apenas uma vez; se perder o valor, será necessário gerar outro.

4. Atualizar o ~/.env do servidor

No servidor:

nano ~/.env

Adicione ou ajuste:

GITHUB_TOKEN="github_pat_xxxxxxxxxxxxxxxxx"
GITHUB_OWNER=<usuario-ou-organizacao>
GITHUB_REPO=<repositorio>
BRANCH=main

No nano, salve e saia com: Ctrl + O, Enter, Ctrl + X.

Esses valores são usados pelo script do cron. Se mudar token, repositório ou branch, altere o ~/.env; o script continua igual.

5. Criar o script no servidor

No servidor:

mkdir -p ~/bin
nano ~/bin/notion-change-check.sh

Cole:

#!/usr/bin/env bash
set -e

ENV_PATH="${HOME}/.env"

if [ ! -f "$ENV_PATH" ]; then
  echo "Env file not found:$ENV_PATH"
  exit 1
fi

set -a
. "$ENV_PATH"
set +a

: "${GITHUB_TOKEN:?GITHUB_TOKEN vazio}"
: "${GITHUB_OWNER:?GITHUB_OWNER vazio}"
: "${GITHUB_REPO:?GITHUB_REPO vazio}"
: "${BRANCH:?BRANCH vazio}"

curl -fsS -X POST \
  "https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/actions/workflows/notion-change-check.yml/dispatches" \
  -H "Accept: application/vnd.github+json" \
  -H "Authorization: Bearer${GITHUB_TOKEN}" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  -H "Content-Type: application/json" \
  -d "{\"ref\":\"${BRANCH}\"}"

No nano, salve e saia com: Ctrl + O, Enter, Ctrl + X.

Depois:

chmod 700 ~/bin/notion-change-check.sh

6. Testar o script no servidor

Rode manualmente:

bash ~/bin/notion-change-check.sh

Uma resposta vazia indica sucesso. A API do GitHub retorna 204 No Content quando aceita o dispatch.

Depois, confira no GitHub:

GitHub > Actions > Notion Change Check

Se a chamada funcionou, uma nova execução deve aparecer com evento workflow_dispatch.

7. Testar manualmente no GitHub

No GitHub, rode o workflow mínimo manualmente:

GitHub > Actions > Notion Change Check > Run workflow

Na primeira execução, o cache ainda não existe. Por isso, o workflow deve considerar que houve mudança e disparar o deploy principal.

Na segunda execução, se nada mudou no Notion, o workflow deve terminar com:

No Notion changes detected.

8. Criar o cron na hospedagem

No painel da hospedagem, crie um cron.

Use um nome claro:

Notion Change Check

No campo COMANDO, use apenas:

bash ~/bin/notion-change-check.sh

Para rodar às 08:00 e 16:00 todos os dias, use:

0 8,16 * * *

No painel, preencha:

MINUTO: 0
HORA: 8,16
DIA DO MÊS: *
MÊS: *
DIA DA SEMANA: *

9. Testar com mudança real

Faça uma alteração simples em uma página da data source usada pelo projeto.

Depois, rode o script manualmente ou aguarde o próximo horário configurado no cron.

O workflow deve detectar um novo hash e disparar o deploy principal.

Etapas de apoio

▸ Checklist antes de ativar o cron

Antes de depender do agendamento no dia a dia, confira:

  • workflow principal possui workflow_dispatch
  • NOTION_TOKEN criado em GitHub Secrets
  • NOTION_DATA_SOURCE_ID criado em GitHub Secrets
  • integração do Notion tem acesso à data source
  • DEPLOY_WORKFLOW_FILE, se existir em GitHub Variables, aponta para o workflow correto
  • GITHUB_TOKEN salvo no ~/.env do servidor
  • GITHUB_OWNER salvo no ~/.env do servidor
  • GITHUB_REPO salvo no ~/.env do servidor
  • BRANCH salvo no ~/.env do servidor
  • token usado no ~/.env possui Actions: Read and write
  • script ~/bin/notion-change-check.sh criado e com permissão de execução
  • script ~/bin/notion-change-check.sh executa manualmente no servidor
  • primeira execução manual do checador funcionou
  • segunda execução manual não disparou deploy sem mudança
  • cron da hospedagem chama bash ~/bin/notion-change-check.sh

▸ Como o deploy funciona no dia a dia

O cron da hospedagem executa o script no intervalo definido.

Se o conteúdo do Notion não mudou, nada acontece além da checagem. Se o conteúdo mudou, o workflow mínimo dispara o workflow principal de deploy.

O deploy manual continua disponível:

GitHub > Actions > CI/CD Static Pipeline > Run workflow

O checador manual também continua disponível:

GitHub > Actions > Notion Change Check > Run workflow

▸ Como saber que deu certo

Use o histórico do cron da hospedagem e do GitHub Actions e confira estes resultados:

  • script manual não imprime erro
  • Notion Change Check aparece no GitHub com evento workflow_dispatch
  • Notion Change Check termina rápido quando nada mudou
  • logs mostram No Notion changes detected. quando o hash é igual
  • Notion Change Check dispara CI/CD Static Pipeline quando o hash muda
  • workflow principal termina verde
  • conteúdo atualizado aparece no site depois do deploy

▸ Erros comuns

Use a saída do script, os logs do cron da hospedagem e os logs do workflow mínimo como ponto de partida:

  • resposta vazia do script: sucesso; a API do GitHub aceitou o dispatch
  • curl: (22) The requested URL returned error: 401: token ausente, inválido ou expirado
  • curl: (22) The requested URL returned error: 403: token sem permissão Actions: Read and write
  • curl: (22) The requested URL returned error: 404: repositório, workflow ou branch incorretos, ou token sem acesso ao repositório
  • GITHUB_TOKEN vazio: confira se o cron carrega o ~/.env do mesmo usuário
  • NOTION_TOKEN is empty: o secret NOTION_TOKEN não foi criado ou está vazio
  • NOTION_DATA_SOURCE_ID is empty: o secret NOTION_DATA_SOURCE_ID não foi criado ou está vazio
  • Notion API returned 401: token do Notion inválido ou integração sem permissão
  • Notion API returned 404: data source incorreta ou sem acesso para a integração
  • workflow principal não dispara: confira DEPLOY_WORKFLOW_FILE, BRANCH e se o workflow principal possui workflow_dispatch
  • primeira execução disparou deploy mesmo sem mudança recente: comportamento esperado, porque ainda não havia cache
  • mudança no Notion não foi detectada: confira se a página alterada pertence à data source consultada e se a integração tem acesso a ela