Open Source Friday - OpenMind - 5-22-2026 1PM #139
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: Guest Welcome Bot | |
| on: | |
| issues: | |
| types: [labeled] | |
| workflow_dispatch: | |
| inputs: | |
| issue_number: | |
| description: "Issue number to test with" | |
| required: true | |
| type: number | |
| jobs: | |
| generate-prep: | |
| if: ${{ github.event.label.name == 'scheduled' || github.event_name == 'workflow_dispatch' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| - name: Install dependencies | |
| run: | | |
| npm install -g @github/copilot | |
| npm install @github/copilot-sdk puppeteer | |
| - name: Get issue data | |
| id: get-issue | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| let issueNumber; | |
| let issue; | |
| if (context.eventName === 'workflow_dispatch') { | |
| issueNumber = ${{ github.event.inputs.issue_number || 0 }}; | |
| const response = await github.rest.issues.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber | |
| }); | |
| issue = response.data; | |
| } else { | |
| issue = context.payload.issue; | |
| issueNumber = issue.number; | |
| } | |
| core.setOutput('issue_number', issueNumber); | |
| core.setOutput('issue_body', issue.body || ''); | |
| core.setOutput('issue_url', issue.html_url); | |
| - name: Parse issue data | |
| id: parse | |
| env: | |
| ISSUE_BODY: ${{ steps.get-issue.outputs.issue_body }} | |
| run: | | |
| cat > parse.mjs << 'ENDSCRIPT' | |
| import { appendFileSync } from 'fs'; | |
| const body = process.env.ISSUE_BODY || ''; | |
| const nameMatch = body.match(/### Name\s*\n\s*([^\n]+)/i); | |
| const handleMatch = body.match(/### GitHub Handle\s*\n\s*@?([^\n\s]+)/i); | |
| const projectMatch = body.match(/### Project Name\s*\n\s*([^\n]+)/i); | |
| const repoMatch = body.match(/### Project Repo Link\s*\n\s*(https:\/\/github\.com\/[^\s]+)/i); | |
| const descMatch = body.match(/### Tell us about yourself\s*\n\s*([\s\S]*?)(?=\n###|$)/i); | |
| let guestName = nameMatch ? nameMatch[1].trim() : 'Guest'; | |
| let handle = handleMatch ? handleMatch[1].trim().replace(/^@/, '') : ''; | |
| const projectName = projectMatch ? projectMatch[1].trim() : ''; | |
| const projectRepo = repoMatch ? repoMatch[1].trim() : ''; | |
| const guestBackground = descMatch ? descMatch[1].trim() : ''; | |
| if (!handle && projectRepo) { | |
| const repoUserMatch = projectRepo.match(/github\.com\/([^\/]+)/); | |
| if (repoUserMatch) { | |
| handle = repoUserMatch[1]; | |
| console.log('Extracted handle from repo URL:', handle); | |
| } | |
| } | |
| console.log('=== PARSED ISSUE DATA ==='); | |
| console.log('Guest Name:', guestName); | |
| console.log('Handle:', handle); | |
| console.log('Project:', projectName); | |
| console.log('Repo:', projectRepo); | |
| const outputFile = process.env.GITHUB_OUTPUT; | |
| appendFileSync(outputFile, 'guest_name=' + guestName + '\n'); | |
| appendFileSync(outputFile, 'handle=' + handle + '\n'); | |
| appendFileSync(outputFile, 'project_name=' + projectName + '\n'); | |
| appendFileSync(outputFile, 'project_repo=' + projectRepo + '\n'); | |
| const bg = guestBackground.replace(/\n/g, ' ').substring(0, 500); | |
| appendFileSync(outputFile, 'guest_background=' + bg + '\n'); | |
| console.log('Outputs written successfully'); | |
| ENDSCRIPT | |
| node parse.mjs | |
| - name: Determine generation mode | |
| id: mode | |
| run: | | |
| echo "mode=${{ case( | |
| steps.parse.outputs.handle != '' && steps.parse.outputs.project_repo != '', 'full-thumbnail', | |
| steps.parse.outputs.guest_name != '' && steps.parse.outputs.handle == '', 'name-only', | |
| 'skip' | |
| ) }}" >> $GITHUB_OUTPUT | |
| - name: Generate welcome message with Copilot | |
| id: personalize | |
| env: | |
| GH_TOKEN: ${{ secrets.COPILOT_PAT }} | |
| GUEST_NAME: ${{ steps.parse.outputs.guest_name }} | |
| HANDLE: ${{ steps.parse.outputs.handle }} | |
| PROJECT_NAME: ${{ steps.parse.outputs.project_name }} | |
| PROJECT_REPO: ${{ steps.parse.outputs.project_repo }} | |
| GUEST_BACKGROUND: ${{ steps.parse.outputs.guest_background }} | |
| run: | | |
| cat > copilot.mjs << 'ENDSCRIPT' | |
| import { appendFileSync } from 'fs'; | |
| const guestName = process.env.GUEST_NAME || 'Guest'; | |
| const projectName = process.env.PROJECT_NAME || ''; | |
| const projectRepo = process.env.PROJECT_REPO || ''; | |
| const guestBackground = process.env. GUEST_BACKGROUND || ''; | |
| console.log('=== GENERATING WELCOME MESSAGE ==='); | |
| console.log('Guest:', guestName); | |
| console.log('Project:', projectName); | |
| console.log('Background:', guestBackground); | |
| let personalizedMessage = ''; | |
| const promptLines = [ | |
| 'You are writing on behalf of the Open Source Friday team a group at GitHub who host a weekly livestream featuring open source maintainers.', | |
| '', | |
| 'The team vibe: Genuinely curious about open source, supportive and encouraging to guests, technically-minded but approachable, excited without being over-the-top.', | |
| '', | |
| 'Guest details:', | |
| '- Name: ' + guestName, | |
| '- Project: ' + (projectName || 'their project'), | |
| '- Repo: ' + projectRepo, | |
| '- About them (in their own words): ' + guestBackground, | |
| '', | |
| 'Write a 2-3 sentence welcome message that:', | |
| '1. Greets them by name', | |
| '2. Matches their energy - if they are playful, be playful back; if they are formal, be warm but professional', | |
| '3. References something specific they said about themselves or their project', | |
| '4. Shows genuine excitement about having them on the stream, and encourages them to share their story and demo with the audience', | |
| '', | |
| 'Sign off as "The Open Source Friday Team" but do not include a formal greeting like "Dear" or "Hi".', | |
| '', | |
| 'Keep it natural and conversational. No emojis. No markdown. Plain text only.' | |
| ]; | |
| try { | |
| const { CopilotClient } = await import('@github/copilot-sdk'); | |
| console. log('Initializing Copilot SDK...'); | |
| const client = new CopilotClient(); | |
| await client.start(); | |
| console.log('Creating session.. .'); | |
| const session = await client.createSession(); | |
| const prompt = promptLines.join('\n'); | |
| console.log('Sending prompt to Copilot...'); | |
| const response = await session. sendAndWait({ prompt: prompt }); | |
| if (response && response.data && response.data. content) { | |
| personalizedMessage = response.data.content; | |
| console.log('Received personalized message from Copilot'); | |
| } | |
| await session. destroy(); | |
| await client.stop(); | |
| } catch (error) { | |
| console.error('Copilot SDK error:', error.message); | |
| console.error('Stack:', error.stack); | |
| } | |
| if (!personalizedMessage) { | |
| console.log('Using fallback message'); | |
| if (projectName) { | |
| personalizedMessage = guestName + ', we are excited to have you on Open Source Friday! ' + projectName + ' sounds like a fantastic project, and we cannot wait to hear more about your journey building it.\n\nThe Open Source Friday Team'; | |
| } else { | |
| personalizedMessage = guestName + ', we are thrilled to have you joining us on Open Source Friday. Our audience loves meeting new voices in open source, and we know this is going to be a great session.\n\nThe Open Source Friday Team'; | |
| } | |
| } | |
| const outputFile = process.env. GITHUB_OUTPUT; | |
| appendFileSync(outputFile, 'personalized_message<<EOFMSG\n' + personalizedMessage + '\nEOFMSG\n'); | |
| console. log('Welcome message generated successfully'); | |
| ENDSCRIPT | |
| node copilot.mjs || true | |
| - name: Generate thumbnail | |
| id: thumbnail | |
| if: ${{ steps.mode.outputs.mode == 'full-thumbnail' }} | |
| env: | |
| GUEST_NAME: ${{ steps.parse.outputs.guest_name }} | |
| HANDLE: ${{ steps.parse.outputs.handle }} | |
| PROJECT_REPO: ${{ steps.parse.outputs.project_repo }} | |
| PROJECT_NAME: ${{ steps.parse.outputs.project_name }} | |
| run: | | |
| cat > thumbnail.mjs << 'ENDSCRIPT' | |
| import puppeteer from 'puppeteer'; | |
| import { appendFileSync, writeFileSync } from 'fs'; | |
| const guestName = process.env.GUEST_NAME || 'Guest'; | |
| let handle = process.env.HANDLE || ''; | |
| const projectRepo = process.env.PROJECT_REPO || ''; | |
| const projectName = process.env.PROJECT_NAME || ''; | |
| console.log('=== THUMBNAIL GENERATION ==='); | |
| console.log('Guest:', guestName); | |
| console.log('Handle:', handle); | |
| console.log('Project:', projectName); | |
| console.log('Repo:', projectRepo); | |
| if (!handle && projectRepo) { | |
| const repoMatch = projectRepo.match(/github\.com\/([^\/]+)/); | |
| if (repoMatch) { | |
| handle = repoMatch[1]; | |
| console.log('Extracted handle from repo URL:', handle); | |
| } | |
| } | |
| if (!handle) { | |
| console.log('No GitHub handle available - skipping thumbnail generation'); | |
| appendFileSync(process.env.GITHUB_OUTPUT, 'thumbnail_generated=false\n'); | |
| process.exit(0); | |
| } | |
| const browser = await puppeteer.launch({ | |
| headless: true, | |
| args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'] | |
| }); | |
| try { | |
| const page = await browser.newPage(); | |
| await page.setViewport({ width: 1400, height: 900 }); | |
| page.on('console', function(msg) { | |
| if (msg.type() === 'error') { | |
| console.log('PAGE ERROR:', msg.text()); | |
| } | |
| }); | |
| console.log('Loading thumbnail generator...'); | |
| await page.goto('https://andreagriffiths11.github.io/thumbnail-gen/', { | |
| waitUntil: 'networkidle0', | |
| timeout: 60000 | |
| }); | |
| console.log('Page loaded successfully'); | |
| console.log('Filling guest name:', guestName); | |
| await page.evaluate(function() { document.getElementById('guestName').value = ''; }); | |
| await page.type('#guestName', guestName); | |
| console.log('Filling username:', handle); | |
| await page.evaluate(function() { document.getElementById('username').value = ''; }); | |
| await page.type('#username', handle); | |
| if (projectRepo) { | |
| console.log('Filling repo:', projectRepo); | |
| await page.evaluate(function() { document.getElementById('repo').value = ''; }); | |
| await page.type('#repo', projectRepo); | |
| } | |
| if (projectName) { | |
| const hasProjectName = await page.$('#projectName'); | |
| if (hasProjectName) { | |
| console.log('Filling project name:', projectName); | |
| await page.evaluate(function() { document.getElementById('projectName').value = ''; }); | |
| await page.type('#projectName', projectName); | |
| } | |
| } | |
| console.log('Clicking generate button...'); | |
| await page.click('#generateBtn'); | |
| console.log('Waiting for thumbnail generation...'); | |
| let attempts = 0; | |
| const maxAttempts = 30; | |
| let isReady = false; | |
| while (attempts < maxAttempts && !isReady) { | |
| await new Promise(function(resolve) { setTimeout(resolve, 2000); }); | |
| const status = await page.evaluate(function() { | |
| const downloadBtn = document.getElementById('downloadBtn'); | |
| const banner = document.getElementById('banner'); | |
| return { | |
| downloadDisabled: downloadBtn ? downloadBtn.disabled : true, | |
| bannerText: banner ? banner.textContent : '', | |
| bannerClass: banner ? banner.className : '' | |
| }; | |
| }); | |
| console.log('Attempt ' + (attempts + 1) + '/' + maxAttempts + ': Download disabled=' + status.downloadDisabled); | |
| if (!status.downloadDisabled) { | |
| isReady = true; | |
| console.log('Thumbnail generation complete!'); | |
| } else if (status.bannerClass.includes('error')) { | |
| throw new Error('Thumbnail generation failed: ' + status.bannerText); | |
| } | |
| attempts++; | |
| } | |
| if (!isReady) { | |
| throw new Error('Thumbnail generation timed out'); | |
| } | |
| await new Promise(function(resolve) { setTimeout(resolve, 1000); }); | |
| console.log('Extracting canvas data...'); | |
| const thumbnailData = await page.evaluate(function() { | |
| const canvas = document.getElementById('preview'); | |
| return canvas.toDataURL('image/png'); | |
| }); | |
| const base64Data = thumbnailData.replace(/^data:image\/png;base64,/, ''); | |
| writeFileSync('thumbnail.png', Buffer.from(base64Data, 'base64')); | |
| console.log('Thumbnail saved successfully!'); | |
| appendFileSync(process.env.GITHUB_OUTPUT, 'thumbnail_generated=true\n'); | |
| } catch (error) { | |
| console.error('Error generating thumbnail:', error.message); | |
| appendFileSync(process.env.GITHUB_OUTPUT, 'thumbnail_generated=false\n'); | |
| } finally { | |
| await browser.close(); | |
| } | |
| ENDSCRIPT | |
| node thumbnail.mjs | |
| - name: Upload thumbnail artifact | |
| if: steps.thumbnail.outputs.thumbnail_generated == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: thumbnail-issue-${{ steps.get-issue.outputs.issue_number }} | |
| path: thumbnail.png | |
| retention-days: 90 | |
| - name: Post welcome comment | |
| uses: actions/github-script@v7 | |
| env: | |
| PERSONALIZED_MESSAGE: ${{ steps.personalize.outputs.personalized_message }} | |
| GUEST_NAME: ${{ steps.parse.outputs.guest_name }} | |
| HANDLE: ${{ steps.parse.outputs.handle }} | |
| ISSUE_NUMBER: ${{ steps.get-issue.outputs.issue_number }} | |
| THUMBNAIL_GENERATED: ${{ steps.thumbnail.outputs.thumbnail_generated }} | |
| RUN_ID: ${{ github.run_id }} | |
| with: | |
| script: | | |
| const personalizedMessage = process.env.PERSONALIZED_MESSAGE; | |
| const guestName = process.env.GUEST_NAME; | |
| const handle = process.env.HANDLE; | |
| const issueNumber = parseInt(process.env.ISSUE_NUMBER, 10); | |
| const thumbnailGenerated = process.env.THUMBNAIL_GENERATED === 'true'; | |
| const runId = process.env.RUN_ID; | |
| const repoUrl = 'https://github.com/' + context.repo.owner + '/' + context.repo.repo; | |
| const guideUrl = repoUrl + '/blob/main/admin/approved-guest.md'; | |
| const thumbnailToolUrl = 'https://andreagriffiths11.github.io/thumbnail-gen/'; | |
| const artifactUrl = repoUrl + '/actions/runs/' + runId; | |
| const mention = handle ? '@' + handle : guestName; | |
| let commentParts = [ | |
| '## Welcome to Open Source Friday! 🎉', | |
| '', | |
| personalizedMessage, | |
| '', | |
| '---', | |
| '' | |
| ]; | |
| if (thumbnailGenerated) { | |
| commentParts = commentParts.concat([ | |
| '### 🖼️ Your Promotional Thumbnail', | |
| '', | |
| '✅ Your custom thumbnail has been generated!', | |
| '', | |
| '**To download it:**', | |
| '1. [Click here to view the workflow run](' + artifactUrl + ')', | |
| '2. Scroll down to **Artifacts**', | |
| '3. Download `thumbnail-issue-' + issueNumber + '`', | |
| '', | |
| 'Use this thumbnail to promote your upcoming stream on social media!', | |
| '', | |
| '> 💡 Want to customize it? Use our [thumbnail generator](' + thumbnailToolUrl + ').', | |
| '', | |
| '---', | |
| '' | |
| ]); | |
| } else { | |
| commentParts = commentParts.concat([ | |
| '### 🖼️ Create Your Promotional Thumbnail', | |
| '', | |
| 'Generate a custom thumbnail for your stream using our [thumbnail generator](' + thumbnailToolUrl + ').', | |
| '', | |
| '---', | |
| '' | |
| ]); | |
| } | |
| commentParts = commentParts.concat([ | |
| '### ✅ Quick Prep Checklist', | |
| '', | |
| '- [ ] Stream starts at **1:00 PM ET** on your scheduled Friday', | |
| '- [ ] Please join at **12:45 PM ET** for prep and tech checks', | |
| '- [ ] Have your demo ready (live demos are our audience favorite!)', | |
| '- [ ] Share your thumbnail on social media to promote the stream', | |
| '', | |
| '📖 For full preparation guidelines, see our [complete guest guide](' + guideUrl + ').', | |
| '', | |
| 'Looking forward to your session, ' + mention + '!', | |
| '', | |
| '— The Open Source Friday Team' | |
| ]); | |
| const comment = commentParts.join('\n'); | |
| await github.rest.issues.createComment({ | |
| issue_number: issueNumber, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: comment | |
| }); | |
| console.log('Comment posted successfully!'); |