Skip to content

[Game]: Built a daily game where you sort historical events chronologically #374

[Game]: Built a daily game where you sort historical events chronologically

[Game]: Built a daily game where you sort historical events chronologically #374

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 }}