Propuesta: aplicar cambios a La Jaquería (issue #357) #54
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Recordatorio de difusión en redes sociales | |
| on: | |
| pull_request: | |
| types: [closed] | |
| paths: | |
| - 'public/data/communities.json' | |
| jobs: | |
| social-share-reminder: | |
| if: github.event.pull_request.merged == true | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| pull-requests: write | |
| contents: read | |
| steps: | |
| - name: Checkout del repositorio | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Detectar comunidades modificadas y publicar comentario | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const { execSync } = require('child_process'); | |
| const APP_BASE_URL = 'https://combuilderses.github.io/communities-directory/'; | |
| const prNumber = context.payload.pull_request?.number ?? context.issue.number; | |
| const prBody = context.payload.pull_request?.body || ''; | |
| const { owner, repo } = context.repo; | |
| const changedFiles = await github.paginate(github.rest.pulls.listFiles, { | |
| owner, | |
| repo, | |
| pull_number: prNumber, | |
| per_page: 100, | |
| }); | |
| const communitiesFileChanged = changedFiles.some( | |
| file => file.filename === 'public/data/communities.json' | |
| ); | |
| if (!communitiesFileChanged) { | |
| core.info('La PR no modificó public/data/communities.json; no se publica recordatorio.'); | |
| return; | |
| } | |
| // Compare the merged commit against its first parent. | |
| // In this repository PRs are merged into the base branch, so that diff | |
| // represents the actual net changes introduced by the PR. | |
| const currentSha = execSync('git rev-parse HEAD').toString().trim(); | |
| let baseSha; | |
| try { | |
| baseSha = execSync('git rev-parse HEAD^1').toString().trim(); | |
| } catch (e) { | |
| core.setFailed(`No se pudo resolver el commit base del merge: ${e.message}`); | |
| return; | |
| } | |
| let baseCommunities = []; | |
| let currentCommunities = []; | |
| try { | |
| const baseJson = execSync(`git show ${baseSha}:public/data/communities.json`).toString(); | |
| const currentJson = execSync(`git show ${currentSha}:public/data/communities.json`).toString(); | |
| baseCommunities = JSON.parse(baseJson); | |
| currentCommunities = JSON.parse(currentJson); | |
| } catch (e) { | |
| core.setFailed(`No se pudo comparar communities.json entre ${baseSha} y ${currentSha}: ${e.message}`); | |
| return; | |
| } | |
| const baseMap = new Map(baseCommunities.map(c => [c.id, c])); | |
| const changedCommunities = currentCommunities.filter(c => { | |
| const base = baseMap.get(c.id); | |
| if (!base) return true; | |
| return JSON.stringify(c) !== JSON.stringify(base); | |
| }); | |
| if (changedCommunities.length === 0) { | |
| core.info('No se detectaron comunidades añadidas o modificadas en esta PR.'); | |
| return; | |
| } | |
| // Extract social handle from a URL | |
| function extractHandle(platform, url) { | |
| if (!url) return null; | |
| try { | |
| const u = new URL(url); | |
| const pathname = u.pathname.replace(/\/$/, ''); | |
| const segments = pathname.split('/').filter(Boolean); | |
| if (!segments.length) return null; | |
| if (platform === 'mastodon') { | |
| const handle = segments[segments.length - 1].replace(/^@/, ''); | |
| return `@${handle}@${u.hostname}`; | |
| } | |
| const handle = segments[segments.length - 1].replace(/^@/, ''); | |
| return `@${handle}`; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| // Community Builders handles extracted from communities.json | |
| const cbEntry = currentCommunities.find(c => c.name === 'Community Builders'); | |
| const cbUrls = cbEntry?.urls || {}; | |
| const cbTwitterHandle = cbUrls.twitter ? extractHandle('generic', cbUrls.twitter) : null; | |
| const cbBskyHandle = cbUrls.bluesky ? extractHandle('generic', cbUrls.bluesky) : null; | |
| // Build share intent URLs for each platform | |
| function buildShareLinks(community, isNew, directLink) { | |
| const urls = community.urls || {}; | |
| function enc(text) { return encodeURIComponent(text); } | |
| // Builds the share text for a given platform. | |
| // communityHandle: the community's handle on that platform (or null). | |
| // cbHandle: CB's handle on that platform (or null → fallback to "Community Builders"). | |
| function shareText(communityHandle, cbHandle) { | |
| const verb = isNew ? 'añadir a' : 'actualizar los datos de'; | |
| const subject = communityHandle || community.name; | |
| const cbRef = cbHandle || 'Community Builders'; | |
| const discover = communityHandle | |
| ? `Descubre más sobre ${community.name} en: ${directLink}` | |
| : `Descubre más en: ${directLink}`; | |
| return `Acabamos de ${verb} ${subject} en el directorio de comunidades de ${cbRef}. ${discover}`; | |
| } | |
| const communityTwitterHandle = urls.twitter ? extractHandle('generic', urls.twitter) : null; | |
| const communityBskyHandle = urls.bluesky ? extractHandle('generic', urls.bluesky) : null; | |
| const twitterText = shareText(communityTwitterHandle, cbTwitterHandle); | |
| const bskyText = shareText(communityBskyHandle, cbBskyHandle); | |
| const plainText = shareText(null, null); // LinkedIn/WhatsApp/Telegram: no handles | |
| return [ | |
| `[𝕏 Twitter](https://twitter.com/intent/tweet?text=${enc(twitterText)})`, | |
| `[🦋 Bluesky](https://bsky.app/intent/compose?text=${enc(bskyText)})`, | |
| `[💼 LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=${enc(directLink)})`, | |
| `[💬 WhatsApp](https://api.whatsapp.com/send?text=${enc(plainText)})`, | |
| `[✈️ Telegram](https://t.me/share/url?url=${enc(directLink)}&text=${enc(plainText)})`, | |
| ].join(' · '); | |
| } | |
| // Build the block for a single community | |
| function buildCommunitySection(community) { | |
| const isNew = !baseMap.has(community.id); | |
| const directLink = `${APP_BASE_URL}?community=${community.id}`; | |
| const urls = community.urls || {}; | |
| const lines = []; | |
| lines.push(`### ${isNew ? '🆕' : '✏️'} ${community.name}`); | |
| lines.push(''); | |
| lines.push(`🔗 **Ver en el directorio:** ${directLink}`); | |
| const socialPlatforms = [ | |
| { key: 'twitter', label: 'X / Twitter', isMastodon: false }, | |
| { key: 'bluesky', label: 'Bluesky', isMastodon: false }, | |
| { key: 'mastodon', label: 'Mastodon', isMastodon: true }, | |
| { key: 'linkedin', label: 'LinkedIn', isMastodon: false }, | |
| { key: 'instagram', label: 'Instagram', isMastodon: false }, | |
| ]; | |
| const socialLines = []; | |
| for (const { key, label, isMastodon } of socialPlatforms) { | |
| if (!urls[key]) continue; | |
| const handle = extractHandle(isMastodon ? 'mastodon' : 'generic', urls[key]); | |
| socialLines.push(handle | |
| ? `- **${label}:** [${handle}](${urls[key]})` | |
| : `- **${label}:** ${urls[key]}` | |
| ); | |
| } | |
| if (socialLines.length > 0) { | |
| lines.push(''); | |
| lines.push('**Cuentas de la comunidad:**'); | |
| lines.push(...socialLines); | |
| } | |
| lines.push(''); | |
| lines.push('**Compartir:**'); | |
| lines.push(buildShareLinks(community, isNew, directLink)); | |
| return lines.join('\n'); | |
| } | |
| const isAnyNew = changedCommunities.some(c => !baseMap.has(c.id)); | |
| const isAnyUpdate = changedCommunities.some(c => baseMap.has(c.id)); | |
| let intro; | |
| if (isAnyNew && !isAnyUpdate) { | |
| intro = changedCommunities.length === 1 | |
| ? `🎉 ¡**${changedCommunities[0].name}** ya está en el directorio!` | |
| : `🎉 ¡${changedCommunities.length} nuevas comunidades ya están en el directorio!`; | |
| } else if (isAnyUpdate && !isAnyNew) { | |
| intro = changedCommunities.length === 1 | |
| ? `✅ ¡Los datos de **${changedCommunities[0].name}** han sido actualizados en el directorio!` | |
| : `✅ ¡Se han actualizado los datos de ${changedCommunities.length} comunidades en el directorio!`; | |
| } else { | |
| intro = `🎉 ¡El directorio ha sido actualizado con ${changedCommunities.length} comunidades nuevas o modificadas!`; | |
| } | |
| const communitySections = changedCommunities.map(buildCommunitySection).join('\n\n---\n\n'); | |
| const commentBody = [ | |
| intro, | |
| '', | |
| '¿Quieres compartir la actualización? Comparte la actualización en redes sociales 📣', | |
| '', | |
| communitySections, | |
| '', | |
| '---', | |
| '_#CommunityBuilders #TechComunidades_', | |
| ].join('\n'); | |
| // Post on the PR | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: prNumber, | |
| body: commentBody, | |
| }); | |
| core.info(`Comentario publicado en PR #${prNumber}`); | |
| // Find linked issues in the PR body (Closes/Fixes/Resolves #N) | |
| const issueMatches = [...prBody.matchAll(/(?:closes?|fixes?|resolves?)\s+#(\d+)/gi)]; | |
| const issueNumbers = [...new Set(issueMatches.map(m => Number(m[1])))]; | |
| for (const issueNumber of issueNumbers) { | |
| try { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: issueNumber, | |
| body: commentBody, | |
| }); | |
| core.info(`Comentario publicado en issue #${issueNumber}`); | |
| } catch (e) { | |
| core.warning(`No se pudo comentar en issue #${issueNumber}: ${e.message}`); | |
| } | |
| } | |
| // Notify all-contributors bot for the issue opener (unless already credited for data) | |
| if (issueNumbers.length === 0) { | |
| core.info('No se encontraron issues enlazados; se omite el comentario all-contributors.'); | |
| return; | |
| } | |
| const linkedIssueNumber = issueNumbers[0]; | |
| let issueAuthor; | |
| try { | |
| const { data: linkedIssue } = await github.rest.issues.get({ | |
| owner, | |
| repo, | |
| issue_number: linkedIssueNumber, | |
| }); | |
| issueAuthor = linkedIssue.user.login; | |
| } catch (e) { | |
| core.warning(`No se pudo obtener el issue #${linkedIssueNumber}: ${e.message}`); | |
| return; | |
| } | |
| // Check if contributor already has 'data' in .all-contributorsrc | |
| let alreadyCredited = false; | |
| try { | |
| const { data: rcFile } = await github.rest.repos.getContent({ | |
| owner, | |
| repo, | |
| path: '.all-contributorsrc', | |
| }); | |
| const rcContent = JSON.parse(Buffer.from(rcFile.content, 'base64').toString('utf8')); | |
| const existing = rcContent.contributors.find( | |
| c => c.login.toLowerCase() === issueAuthor.toLowerCase() | |
| ); | |
| alreadyCredited = existing?.contributions?.includes('data') ?? false; | |
| } catch (e) { | |
| core.warning(`No se pudo leer .all-contributorsrc: ${e.message}`); | |
| } | |
| if (alreadyCredited) { | |
| core.info(`@${issueAuthor} ya figura como contribuidor de datos; se omite el comentario all-contributors.`); | |
| return; | |
| } | |
| try { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: 22, | |
| body: `@all-contributors please add @${issueAuthor} for data`, | |
| }); | |
| core.info(`Comentario all-contributors publicado para @${issueAuthor} en issue #22`); | |
| } catch (e) { | |
| core.warning(`No se pudo comentar en issue #22: ${e.message}`); | |
| } |