From f3c45fe1a57d16054635138bf4305d7ee85b7bd0 Mon Sep 17 00:00:00 2001 From: juansilvadesign Date: Thu, 9 Jul 2026 11:17:02 -0300 Subject: [PATCH] ci(social): rascunho de post por release, co-criado com a comunidade (#27) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A cada release (ou sob demanda), scripts/social-draft.mjs transforma a seção do CHANGELOG num rascunho de post pt-BR e o workflow abre um PR em social/drafts/ com label social/content — revisão humana obrigatória; o workflow não tem acesso a rede social nenhuma, de propósito. Com o segredo ANTHROPIC_API_KEY opcional o texto sai por IA (limitado ao changelog); sem ele, um template dos bullets (testado nos dados reais da v1.4.0 — o rascunho de exemplo está commitado). social/README.md documenta o fluxo de co-criação com crédito, no espírito do CONTRIBUTORS.md. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AiLQznJ15KbuK73b9YxAjS --- .github/workflows/social-draft.yml | 56 ++++++++++ CHANGELOG.md | 11 ++ scripts/social-draft.mjs | 170 +++++++++++++++++++++++++++++ social/README.md | 36 ++++++ social/drafts/v1.4.0.md | 25 +++++ 5 files changed, 298 insertions(+) create mode 100644 .github/workflows/social-draft.yml create mode 100644 scripts/social-draft.mjs create mode 100644 social/README.md create mode 100644 social/drafts/v1.4.0.md diff --git a/.github/workflows/social-draft.yml b/.github/workflows/social-draft.yml new file mode 100644 index 0000000..3124887 --- /dev/null +++ b/.github/workflows/social-draft.yml @@ -0,0 +1,56 @@ +# Issue #27 — social presence with community co-created posts. +# On every release (or on demand), draft a social post from that version's +# CHANGELOG section and open a PR with it under social/drafts/. ALWAYS a +# draft + PR — a human reviews, edits, and publishes; nothing goes out alone. +# +# The ANTHROPIC_API_KEY secret is OPTIONAL: with it the draft is AI-written +# (grounded only in the changelog); without it a template draft is assembled +# from the changelog bullets. Either way the flow works. +name: Social draft + +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: 'Version to draft (e.g. 1.4.0); empty = latest released' + required: false + +permissions: + contents: write + pull-requests: write + +jobs: + draft: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Draft the post from the CHANGELOG + run: node scripts/social-draft.mjs "${{ inputs.version || github.event.release.tag_name }}" + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + - name: Open a PR with the draft + run: | + if git diff --quiet && [ -z "$(git status --porcelain social/)" ]; then + echo "no draft produced — nothing to PR"; exit 0 + fi + VERSION="${{ inputs.version || github.event.release.tag_name }}" + BRANCH="social/draft-${VERSION:-latest}-${{ github.run_id }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git add social/ + git commit -m "social: rascunho de post da versão ${VERSION:-mais recente}" + git push origin "$BRANCH" + gh pr create \ + --title "Social: rascunho de post — ${VERSION:-versão mais recente}" \ + --body "Rascunho gerado automaticamente a partir do CHANGELOG (issue #27). + +**Antes de publicar:** revise o texto, ajuste o tom, confira que nada foi inventado e dê os créditos de quem contribuiu na versão (CONTRIBUTORS.md). O guia completo está em \`social/README.md\`." \ + --label "social/content" || true + env: + GH_TOKEN: ${{ github.token }} diff --git a/CHANGELOG.md b/CHANGELOG.md index f84d32f..eb0cf64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ Todas as mudanças relevantes deste projeto são documentadas neste arquivo. O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/) e o projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/). +## [Não lançado] + +### Adicionado (ferramentas do repositório) + +- **Rascunho de post por release** (`.github/workflows/social-draft.yml` + + `scripts/social-draft.mjs`, issue #27): a cada release, a seção do CHANGELOG + vira um rascunho de post e um PR em `social/drafts/` para revisão humana — + nada publica sozinho. Com o segredo `ANTHROPIC_API_KEY` o texto é escrito + por IA (limitado ao que o changelog diz); sem ele, sai um template dos + bullets. Guia de co-criação da comunidade em `social/README.md`. + ## [1.4.0] - 2026-07-07 ### Adicionado diff --git a/scripts/social-draft.mjs b/scripts/social-draft.mjs new file mode 100644 index 0000000..39209ea --- /dev/null +++ b/scripts/social-draft.mjs @@ -0,0 +1,170 @@ +// ZeroDelay — keep YouTube live streams in real time +// Author: João Gustavo França (https://github.com/joaogfc) +// +// Drafts a social post from a released CHANGELOG section (issue #27): every +// release turns into a REVIEWABLE draft under social/drafts/ — never a direct +// publication. The workflow (.github/workflows/social-draft.yml) opens a PR +// with the draft; a human edits, approves, and posts it. +// +// node scripts/social-draft.mjs # latest released version +// node scripts/social-draft.mjs 1.4.0 # a specific version +// +// With ANTHROPIC_API_KEY set the draft is written by Claude, grounded ONLY in +// the changelog section (repo tooling — the extension itself never talks to +// any server). Without the key it falls back to a template draft assembled +// from the changelog bullets — the workflow works with zero secrets. +// +// Raw fetch on purpose: every script in this repo is dependency-free (see +// scripts/build.mjs), and one small completions call doesn't justify an SDK. +import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = fileURLToPath(new URL('..', import.meta.url)); +const MODEL = process.env.SOCIAL_MODEL || 'claude-opus-4-8'; +const STORE_CHROME = 'https://chromewebstore.google.com/detail/zerodelay/gblbnnkemjblakamnbclcehoaobnhlpm'; +const STORE_FIREFOX = 'https://addons.mozilla.org/firefox/addon/zerodelay/'; +const REPO = 'https://github.com/joaogfc/ZeroDelay'; + +// --- CHANGELOG parsing ------------------------------------------------------ +// Sections look like "## [1.4.0] - 2026-07-07"; "## [Não lançado]" is skipped +// when resolving the default (you announce releases, not work in flight). +function parseChangelog(text) { + const sections = []; + const re = /^## \[([^\]]+)\](?: - (\S+))?$/gm; + let match, prev = null; + while ((match = re.exec(text)) !== null) { + if (prev) prev.body = text.slice(prev.end, match.index).trim(); + prev = { version: match[1], date: match[2] || null, end: re.lastIndex }; + sections.push(prev); + } + if (prev) prev.body = text.slice(prev.end).trim(); + return sections; +} + +// --- Template fallback (no API key) ---------------------------------------- +// First-level bullets (with their wrapped continuation lines joined), markdown +// links/emphasis stripped, each trimmed to headline length. +function templateDraft(version, body) { + const raw = []; + for (const line of body.split('\n')) { + const start = line.match(/^- (.+)$/); + const cont = line.match(/^ {2,}(\S.*)$/); + if (start) raw.push(start[1]); + else if (cont && raw.length) raw[raw.length - 1] += ' ' + cont[1]; + } + const bullets = raw.slice(0, 4).map(text => { + const clean = text + .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') // [text](url) -> text + .replace(/\*\*([^*]+)\*\*/g, '$1') // **bold** -> bold + .replace(/`([^`]+)`/g, '$1'); + // Headline = up to the first sentence end (or hard cap), never mid-word. + const firstStop = clean.search(/[.:!?] /); + const head = firstStop > 15 ? clean.slice(0, firstStop) : clean; + return '• ' + (head.length > 110 ? head.slice(0, 107).replace(/\s+\S*$/, '') + '…' : head); + }); + return [ + `🔴 ZeroDelay v${version} chegou!`, + '', + ...bullets, + '', + `Chrome: ${STORE_CHROME}`, + `Firefox: ${STORE_FIREFOX}`, + ].join('\n'); +} + +// --- Claude draft (optional) ------------------------------------------------ +async function claudeDraft(version, body) { + const prompt = [ + `Você escreve os posts do ZeroDelay, uma extensão open-source (GPL) que`, + `mantém lives do YouTube em tempo real. Abaixo está a seção do CHANGELOG`, + `da versão ${version}. Escreva DOIS rascunhos de post em pt-BR:`, + '', + '1. **X/Twitter** (máx ~280 caracteres): direto, uma melhoria em destaque,', + ' tom de quem fala com espectador de live, zero jargão corporativo.', + '2. **Instagram** (caption curta): 2-4 linhas + até 4 hashtags discretas.', + '', + 'Regras: baseie-se SOMENTE no changelog abaixo (não invente recursos);', + 'nada de hype vazio ("revolucionário", "incrível"); pode usar 1-2 emojis;', + `termine com o link da loja: ${STORE_CHROME}`, + '', + '--- CHANGELOG ---', + body, + ].join('\n'); + + const res = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'x-api-key': process.env.ANTHROPIC_API_KEY, + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json', + }, + body: JSON.stringify({ + model: MODEL, + max_tokens: 2048, + messages: [{ role: 'user', content: prompt }], + }), + }); + if (!res.ok) throw new Error(`Claude API: HTTP ${res.status} — ${(await res.text()).slice(0, 300)}`); + const data = await res.json(); + if (data.stop_reason === 'refusal') throw new Error('Claude API: request refused'); + const text = (data.content || []).filter(b => b.type === 'text').map(b => b.text).join('\n').trim(); + if (!text) throw new Error('Claude API: empty response'); + return text; +} + +// --- Main -------------------------------------------------------------------- +const changelog = readFileSync(join(root, 'CHANGELOG.md'), 'utf8'); +const sections = parseChangelog(changelog); + +const wanted = (process.argv[2] || '').replace(/^v/, ''); +const section = wanted + ? sections.find(s => s.version === wanted) + : sections.find(s => /^\d/.test(s.version)); // newest released (skips "Não lançado") + +if (!section || !section.body) { + console.error(`✗ CHANGELOG section not found${wanted ? ` for version ${wanted}` : ''}.`); + process.exit(1); +} + +let draft, source; +if (process.env.ANTHROPIC_API_KEY) { + try { + draft = await claudeDraft(section.version, section.body); + source = `IA (${MODEL})`; + } catch (e) { + console.error(`! ${e.message} — falling back to the template draft.`); + } +} +if (!draft) { + draft = templateDraft(section.version, section.body); + source = source || 'template (sem ANTHROPIC_API_KEY)'; +} + +const outDir = join(root, 'social', 'drafts'); +mkdirSync(outDir, { recursive: true }); +const outPath = join(outDir, `v${section.version}.md`); +if (existsSync(outPath)) console.error(`! overwriting existing draft: social/drafts/v${section.version}.md`); + +writeFileSync(outPath, [ + '---', + 'status: rascunho # vira "aprovado" só depois de revisão humana', + `versao: ${section.version}`, + `fonte: ${source}`, + `gerado_em: ${new Date().toISOString()}`, + '---', + '', + `# Rascunho de post — ZeroDelay v${section.version}`, + '', + '> Revisão humana obrigatória antes de publicar (social/README.md).', + `> Créditos de quem contribuiu na versão: ver CONTRIBUTORS.md e o CHANGELOG.`, + '', + draft, + '', + '---', + '', + `Repositório: ${REPO}`, + '', +].join('\n')); + +console.log(`✓ social/drafts/v${section.version}.md (${source})`); diff --git a/social/README.md b/social/README.md new file mode 100644 index 0000000..1d7210d --- /dev/null +++ b/social/README.md @@ -0,0 +1,36 @@ +# Social — posts co-criados pela comunidade + +Materialização da [issue #27](https://github.com/joaogfc/ZeroDelay/issues/27): +o ZeroDelay melhora quase todo dia, mas as novidades ficavam escondidas no +CHANGELOG. Esta pasta é o canal entre o repositório e as redes sociais. + +## Como funciona + +1. **A cada release**, o workflow [`social-draft.yml`](../.github/workflows/social-draft.yml) + lê a seção da versão no `CHANGELOG.md` e abre um PR com um rascunho de post + em [`drafts/`](drafts/) — escrito por IA quando o segredo `ANTHROPIC_API_KEY` + está configurado (baseado SOMENTE no changelog), ou montado dos bullets do + changelog quando não está. +2. **Um humano revisa** o rascunho no PR: ajusta o tom, corta exagero, confere + que nada foi inventado. Nada é publicado sem essa revisão — o workflow não + tem acesso a nenhuma rede social, de propósito. +3. **Publicação é manual** nos perfis oficiais, por quem tem acesso. + +## Co-criação + +Qualquer pessoa pode propor posts: + +- Abra uma issue com o label **`social/content`** com a ideia ou o texto; ou +- Mande um PR direto com um arquivo em `drafts/` (use o frontmatter dos + rascunhos gerados como modelo). + +Ideias aprovadas viram publicação **com crédito ao autor**, no espírito do +[`CONTRIBUTORS.md`](../CONTRIBUTORS.md). + +## Regras editoriais + +- Tom de quem assiste live: direto, leve, zero jargão corporativo. +- Nunca prometer o que a extensão não faz; o changelog é o limite do factual. +- O "antes e depois" (live atrasada → tempo real) é a história principal. +- Privacidade é argumento, não rodapé: tudo roda local, nada é coletado. +- Melhorias de comunidade citam o autor e o PR (como o CHANGELOG já faz). diff --git a/social/drafts/v1.4.0.md b/social/drafts/v1.4.0.md new file mode 100644 index 0000000..88c5beb --- /dev/null +++ b/social/drafts/v1.4.0.md @@ -0,0 +1,25 @@ +--- +status: rascunho # vira "aprovado" só depois de revisão humana +versao: 1.4.0 +fonte: template (sem ANTHROPIC_API_KEY) +gerado_em: 2026-07-09T14:16:00.325Z +--- + +# Rascunho de post — ZeroDelay v1.4.0 + +> Revisão humana obrigatória antes de publicar (social/README.md). +> Créditos de quem contribuiu na versão: ver CONTRIBUTORS.md e o CHANGELOG. + +🔴 ZeroDelay v1.4.0 chegou! + +• Convite de doação dentro do player (Brasil) +• "Temperatura" dos modos (ideia e implementação de @leandroohsr, PR #41) +• Valores de doação ancorados um pouco mais para cima +• Freio anti-travamento mais conservador + +Chrome: https://chromewebstore.google.com/detail/zerodelay/gblbnnkemjblakamnbclcehoaobnhlpm +Firefox: https://addons.mozilla.org/firefox/addon/zerodelay/ + +--- + +Repositório: https://github.com/joaogfc/ZeroDelay