[Game]: Built a daily game where you sort historical events chronologically #374
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: Create Game PR from Issue | |
| on: | |
| issues: | |
| types: [opened, labeled] | |
| jobs: | |
| create-game-pr: | |
| if: >- | |
| github.event.action == 'labeled' && | |
| github.event.label.name == 'reviewed' && | |
| contains(github.event.issue.labels.*.name, 'game-submission') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Parse issue form | |
| id: parse | |
| uses: stefanbuck/github-issue-parser@v3 | |
| with: | |
| template-path: .github/ISSUE_TEMPLATE/submit-game.yml | |
| - name: Generate game file | |
| uses: actions/github-script@v7 | |
| id: generate | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const data = JSON.parse(process.env.ISSUE_JSON); | |
| const gameName = data['game-name']; | |
| const playUrl = data['play-url']; | |
| const hnThread = data['hn-thread']; | |
| const authorName = data['author-name']; | |
| const authorUrl = data['author-url'] || ''; | |
| const sourceUrl = data['source-url'] || ''; | |
| const screenshotUrl = data['screenshot-url'] || ''; | |
| const tags = data['tags']; | |
| const description = data['description']; | |
| // Extract HN item ID from thread URL | |
| const hnIdMatch = hnThread.match(/item\?id=(\d+)/); | |
| const hnId = hnIdMatch ? hnIdMatch[1] : null; | |
| // Fetch HN points if we have an item ID | |
| let points = 0; | |
| if (hnId) { | |
| try { | |
| const hnResp = await fetch(`https://hn.algolia.com/api/v1/items/${hnId}`); | |
| if (hnResp.ok) { | |
| const hnData = await hnResp.json(); | |
| points = hnData.points || 0; | |
| console.log(`Fetched HN points for item ${hnId}: ${points}`); | |
| } | |
| } catch (error) { | |
| console.warn(`Failed to fetch HN points: ${error.message}`); | |
| } | |
| } | |
| // Generate slug from game name | |
| const slug = gameName | |
| .toLowerCase() | |
| .replace(/[^a-z0-9]+/g, '-') | |
| .replace(/^-|-$/g, ''); | |
| // Parse tags (comma-separated from multi-select dropdown) | |
| const tagList = tags.split(',').map(t => t.trim()).filter(Boolean); | |
| const tagsYaml = tagList.join(', '); | |
| // Clean description: remove "Show HN:" prefix and "Discovered via HN scraper" suffix | |
| let cleanDesc = description.trim(); | |
| cleanDesc = cleanDesc.replace(/^Show HN:\s*/i, ''); | |
| cleanDesc = cleanDesc.replace(/\s*Discovered via HN scraper\.?\s*$/i, ''); | |
| cleanDesc = cleanDesc.replace(/\s*Originally posted \d{4}-\d{2}\.\s*Discovered via HN archive scraper for newsletter "From the Archives" section\.?\s*$/i, ''); | |
| // First line for frontmatter description, sanitized for YAML | |
| let shortDesc = cleanDesc.split('\n')[0].trim(); | |
| // Strip characters that are problematic in YAML double-quoted strings | |
| shortDesc = shortDesc.replace(/[\\"`]/g, '').replace(/\s+/g, ' ').trim(); | |
| // Truncate to a reasonable length for a one-line description | |
| if (shortDesc.length > 200) { | |
| shortDesc = shortDesc.slice(0, 197) + '...'; | |
| } | |
| // Determine submission method based on issue labels | |
| const issueLabels = context.payload.issue.labels.map(l => l.name); | |
| const submissionMethod = issueLabels.includes('game-submission') ? 'scraped' : 'manual'; | |
| // Current date in YYYY-MM-DD format | |
| const dateAdded = new Date().toISOString().split('T')[0]; | |
| // Build author cell | |
| const authorCell = authorUrl | |
| ? `[${authorName}](${authorUrl})` | |
| : authorName; | |
| // Build play domain for display | |
| const playDisplay = new URL(playUrl).hostname; | |
| // Build source row (optional) | |
| let sourceRow = ''; | |
| if (sourceUrl) { | |
| const srcUrl = new URL(sourceUrl); | |
| const sourceDisplay = srcUrl.hostname + srcUrl.pathname.replace(/\/$/, ''); | |
| sourceRow = `| **Source** | [${sourceDisplay}](${sourceUrl}) |`; | |
| } | |
| // Build the markdown content (screenshot will be added after capture) | |
| const lines = [ | |
| '---', | |
| `title: "${gameName.replace(/"/g, '\\"')}"`, | |
| `tags: [${tagsYaml}]`, | |
| `description: "${shortDesc.replace(/"/g, '\\"')}"`, | |
| ...(screenshotUrl ? [`screenshot: "${screenshotUrl}"`] : []), | |
| `dateAdded: ${dateAdded}`, | |
| `submissionMethod: ${submissionMethod}`, | |
| ...(hnId ? [`hnId: ${hnId}`] : []), | |
| ...(hnId ? [`points: ${points}`] : []), | |
| '---', | |
| '', | |
| `# ${gameName}`, | |
| '', | |
| '| | |', | |
| '|---|---|', | |
| `| **Author** | ${authorCell} |`, | |
| `| **Play** | [${playDisplay}](${playUrl}) |`, | |
| `| **HN Thread** | [Show HN: ${gameName}](${hnThread}) |`, | |
| ]; | |
| if (sourceRow) { | |
| lines.push(sourceRow); | |
| } | |
| // Add metadata rows | |
| if (hnId) { | |
| lines.push(`| **HN Points** | ${points} |`); | |
| } | |
| lines.push(`| **Date Added** | ${dateAdded} |`); | |
| lines.push(`| **Tags** | ${tagsYaml} |`); | |
| lines.push('', '## About', '', cleanDesc, ''); | |
| const content = lines.join('\n'); | |
| const filePath = path.join('docs', 'games', `${slug}.md`); | |
| fs.writeFileSync(filePath, content); | |
| core.setOutput('slug', slug); | |
| core.setOutput('game-name', gameName); | |
| core.setOutput('play-url', playUrl); | |
| core.setOutput('has-screenshot', screenshotUrl ? 'true' : 'false'); | |
| env: | |
| ISSUE_JSON: ${{ steps.parse.outputs.jsonString }} | |
| - name: Install Playwright | |
| if: steps.generate.outputs.has-screenshot == 'false' | |
| run: npx playwright install chromium | |
| - name: Take screenshot | |
| if: steps.generate.outputs.has-screenshot == 'false' | |
| run: | | |
| node --input-type=module <<'EOF' | |
| import { chromium } from 'playwright'; | |
| import fs from 'fs'; | |
| import path from 'path'; | |
| const slug = process.env.SLUG; | |
| const playUrl = process.env.PLAY_URL; | |
| const gameName = process.env.GAME_NAME; | |
| const filePath = path.join('docs', 'games', `${slug}.md`); | |
| const screenshotDir = path.join('static', 'img', 'games'); | |
| const screenshotFile = `${slug}.png`; | |
| const screenshotPath = path.join(screenshotDir, screenshotFile); | |
| const frontmatterPath = `/img/games/${screenshotFile}`; | |
| // Ensure screenshot directory exists | |
| fs.mkdirSync(screenshotDir, { recursive: true }); | |
| console.log(`Taking screenshot of ${playUrl}`); | |
| const browser = await chromium.launch({ headless: true }); | |
| const browserContext = await browser.newContext({ | |
| viewport: { width: 1280, height: 720 }, | |
| deviceScaleFactor: 1, | |
| }); | |
| const page = await browserContext.newPage(); | |
| try { | |
| await page.goto(playUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }); | |
| await page.waitForTimeout(3000); | |
| await page.screenshot({ path: screenshotPath, type: 'png' }); | |
| console.log(`Screenshot saved to ${screenshotPath}`); | |
| // Update the markdown file with screenshot | |
| let content = fs.readFileSync(filePath, 'utf-8'); | |
| // Add screenshot to frontmatter | |
| content = content.replace( | |
| /^(---\n[\s\S]*?)(---\n)/, | |
| `$1screenshot: ${frontmatterPath}\n$2` | |
| ); | |
| fs.writeFileSync(filePath, content); | |
| console.log('Updated frontmatter with screenshot path'); | |
| } catch (error) { | |
| console.error(`Failed to take screenshot: ${error.message}`); | |
| } finally { | |
| await browser.close(); | |
| } | |
| EOF | |
| env: | |
| SLUG: ${{ steps.generate.outputs.slug }} | |
| PLAY_URL: ${{ steps.generate.outputs.play-url }} | |
| GAME_NAME: ${{ steps.generate.outputs.game-name }} | |
| - name: Create pull request | |
| id: pr | |
| uses: peter-evans/create-pull-request@v7 | |
| with: | |
| commit-message: "Add game: ${{ steps.generate.outputs.game-name }}" | |
| title: "Add game: ${{ steps.generate.outputs.game-name }}" | |
| body: | | |
| Adds **${{ steps.generate.outputs.game-name }}** to the game directory. | |
| Closes #${{ github.event.issue.number }} | |
| branch: "add-game/${{ steps.generate.outputs.slug }}" | |
| base: main | |
| - name: Comment on issue | |
| if: steps.pr.outputs.pull-request-number | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: `Thanks for submitting! A pull request has been created: #${process.env.PR_NUMBER}\n\nWe'll review and merge it to add this game to the directory.` | |
| }); | |
| env: | |
| PR_NUMBER: ${{ steps.pr.outputs.pull-request-number }} |