Pular para conteúdo

Pipeline Instagram · End-to-end

Última revisão: 2026-04-29

Costura render do squad (workflows/junior-instagram-post.md) com publicação automática (INSTAGRAM.md) num único caminho operacional.

┌──────────────────────────────────────────────────────────────────────┐
│                         SQUAD (junior-squad)                         │
│   Helena → Cláudia → Caio → Yara/Rafa → Bia → studio/outputs/*.png   │
└──────────────────────────────┬───────────────────────────────────────┘
                               ↓
┌──────────────────────────────────────────────────────────────────────┐
│              scripts/publish-junior-carousel.sh                      │
│   1. lê PNGs em outputs/junior-maia/em-producao/<slug>/              │
│   2. extrai caption de copy.md (versão curta) ou --caption-file      │
│   3. sobe pro Supabase Storage (instagram-media/<account>/YYYY-MM/)  │
│   4. POST /instagram/posts com scheduled_at                          │
│   5. salva PUBLISH-LOG.md na pasta da peça                           │
└──────────────────────────────┬───────────────────────────────────────┘
                               ↓
┌──────────────────────────────────────────────────────────────────────┐
│   Supabase Postgres · instagram_posts(status='scheduled')            │
└──────────────────────────────┬───────────────────────────────────────┘
                               ↓
┌──────────────────────────────────────────────────────────────────────┐
│   Cron `instagram-publisher` (apiclickup, */1 * * * *)               │
│   Lê posts onde scheduled_at <= now(), publica via Graph API,        │
│   marca published + ig_media_id ou failed + erro                     │
└──────────────────────────────┬───────────────────────────────────────┘
                               ↓
                       📱 Instagram (no ar)

Quando usar

Cenário Caminho
Peça nova rodada pelo squad (squad termina em PNGs) scripts/publish-junior-carousel.sh
Quer agendar manualmente sem render do squad (já tem URLs) curl direto pra /instagram/posts (ver INSTAGRAM.md)
Publicação editorial via Sheets (calendário) server/scripts/instagram-from-drive.js
Pausar publicações sem perder agendamentos Admin → Crons → toggle instagram-publisher

Caminho rápido (script)

Pré-requisitos (uma vez): - server/.env com SUPABASE_URL, SUPABASE_SERVICE_KEY, DASHBOARD_TOKEN - Bucket instagram-media público no Supabase (já criado) - jq e curl instalados (brew install jq se faltar) - Pasta da peça em outputs/junior-maia/em-producao/<slug>/ com: - PNGs nomeados *slide<N>.png ou *card<N>.png (ordem alfanumérica determina ordem do carrossel) - copy.md com a seção ### Versão curta (caption será extraída) ou caption.md na pasta

Comando

./scripts/publish-junior-carousel.sh \
  --slug tudo-e-codigo \
  --scheduled-at 2026-04-29T12:00:00Z

Flags úteis

Flag Default Quando usar
--account <junior\|gita> junior Publicar na conta da Gita
--source <pasta> outputs/junior-maia/em-producao/<slug>/ Peça em outra pasta
--caption-file <path> extrai de copy.md Caption customizada
--type <feed\|reel\|carousel\|story> auto (>1 mídia = carousel) Forçar tipo
--server <url> https://gita-apiclickup.ewzc9p.easypanel.host Apontar pra outro server
--dry-run Imprime payload sem subir nada
--yes / -y Pula confirmação interativa

Validações que o script faz

  1. scheduled_at em ISO 8601 UTC com Z
  2. Conta válida (junior ou gita)
  3. Limites Meta: carousel 2-10 itens, feed/story 1, caption ≤ 2200 chars
  4. Bucket público (smoke test: a 1ª URL responde HTTP 200 antes de criar o post)
  5. Resposta do servidor traz id (caso contrário, falha explícita)

Saída

  • Post criado no Supabase com status=scheduled
  • Cron instagram-publisher pega no próximo tick (≤ 1 min) se scheduled_at <= now(). Se for futuro, espera.
  • Log salvo em outputs/junior-maia/em-producao/<slug>/PUBLISH-LOG.md com:
  • Post ID
  • URLs públicas das mídias (auditáveis)
  • Comandos prontos pra checar status, forçar tick, debug

Caminho manual (sem script)

Use quando quiser controle granular ou quando o script não cobre o caso (ex: misturar imagens já hospedadas em outro lugar).

# 1. Carregar env
set -a && . server/.env && set +a

# 2. Subir cada mídia (repetir pra cada slide)
TS=$(date +%s)
for i in 1 2 3 4 5 6 7; do
  KEY="instagram-media/junior/$(date -u +%Y-%m)/tudo-e-codigo-$TS-slide$i.png"
  curl -sS -X POST "$SUPABASE_URL/storage/v1/object/$KEY" \
    -H "Authorization: Bearer $SUPABASE_SERVICE_KEY" \
    -H "Content-Type: image/png" \
    -H "x-upsert: true" \
    --data-binary "@studio/outputs/2026-04-29_tudo-e-codigo-slide$i.png"
done

# 3. Criar post
curl -X POST "https://gita-apiclickup.ewzc9p.easypanel.host/instagram/posts" \
  -H "Authorization: Bearer $DASHBOARD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "junior",
    "post_type": "carousel",
    "caption": "...",
    "media_items": [
      {"url": "<URL_pública_slide1>", "type": "image"},
      ...
    ],
    "scheduled_at": "2026-04-29T12:00:00Z",
    "status": "scheduled"
  }'

Estrutura completa do payload em INSTAGRAM.md § API REST.


Monitoring

Status global do scheduler

curl -sS "https://gita-apiclickup.ewzc9p.easypanel.host/instagram/scheduler/status" \
  -H "Authorization: Bearer $DASHBOARD_TOKEN"

Retorna {cronRegistered, enabled, pendingCount, batchLimit, cronExpression}. Se enabled=falseENABLE_INSTAGRAM_PUBLISHER=false no Easypanel ou cron pausado pelo admin.

Detalhes de um post específico

curl -sS "https://gita-apiclickup.ewzc9p.easypanel.host/instagram/posts?status=scheduled&account=junior" \
  -H "Authorization: Bearer $DASHBOARD_TOKEN" \
  | jq '.posts[] | select(.id=="<POST_ID>")'

Forçar tick agora (debug)

curl -sS -X POST "https://gita-apiclickup.ewzc9p.easypanel.host/instagram/publish-pending?limit=5" \
  -H "Authorization: Bearer $DASHBOARD_TOKEN"

Útil pra: posts vencidos que ficaram presos, validar que o cron consome corretamente, debug de credenciais Graph API.

Admin UI

https://gita-apiclickup.ewzc9p.easypanel.host/instagram/admin?token=<DASHBOARD_TOKEN>

Preview visual tipo Instagram. Read-only — não cria/edita pelo admin.


Recuperação de falhas

Status O que aconteceu O que fazer
scheduled mas pendingCount=0 no scheduler depois do horário Cron não rodou (server fora do ar?) Checar /health. Se voltar, próximo tick consome.
failed + error: code=190 Token Meta expirou Regerar Page Token (passo 5 em INSTAGRAM.md) → atualizar META_IG_TOKEN_* no Easypanel → redeploy.
failed + error: code=4 Rate limit (>25 posts em 24h) Esperar. Cron tenta de novo no próximo tick.
failed + error: code=9004 URL de mídia inacessível Bucket Supabase não está público. Tornar público e fazer PATCH no post (status volta pra scheduled).
failed + status_code=ERROR no container Meta rejeitou (formato/tamanho) Ver limites em INSTAGRAM.md § Limites da Meta. Re-render com formato correto + re-upload + criar post novo (descartar antigo).
publishing há >5min Travou no fluxo de 2 etapas Pode ser timeout do polling. Checar Easypanel logs do apiclickup.

Re-tentar manualmente

Pelo admin: Instagram → card vermelho → Tentar novamente. Pelo banco: UPDATE instagram_posts SET status='scheduled', error=NULL WHERE id='<uuid>' (Supabase SQL editor).


Limpeza pós-publicação

Quando status=published, o post fica registrado em instagram_posts com ig_media_id. Não deletar — é audit trail. As mídias no bucket podem ser limpas depois de N dias se o storage encher (>10GB).

Tomás (arquivista) faz post-mortem em data/brand/junior-maia/08-aprendizados/posts/<DATA>_<slug>.md puxando métricas e o ig_media_id.


Troubleshooting do script

Sintoma Causa provável Fix
mapfile: command not found bash do macOS é antigo (3.2) Já tratado — script usa while read
jq: invalid JSON text Caption tem caractere especial não escapado Salvar caption em arquivo + --caption-file
Smoke test failed (HTTP 400) Bucket não está público Console Supabase → Storage → instagram-media → toggle Public
Servidor não retornou id Token DASHBOARD inválido OU server fora curl /health no server primeiro
Imagens fora de ordem no Instagram Naming não-natural (slide1, slide11, slide2) Renomear pra slide01, slide02, ... ou card01, card02, ...

Refs