Export Community Builds to JSON #27
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: Export Community Builds to JSON | |
| on: | |
| workflow_dispatch: {} | |
| issues: | |
| types: [labeled] | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: read | |
| concurrency: | |
| group: export-builds | |
| cancel-in-progress: true | |
| jobs: | |
| export-builds: | |
| # run only on manual OR when 'Showcase Approved' label is applied | |
| if: > | |
| github.event_name == 'workflow_dispatch' || | |
| (github.event_name == 'issues' && | |
| github.event.action == 'labeled' && | |
| github.event.label.name == 'Showcase Approved') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v3 | |
| - name: Brief wait (let other labelers finish) | |
| if: github.event_name == 'issues' | |
| run: sleep 10 | |
| - name: Fetch ALL issues (we filter in Node) | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| mkdir -p docs | |
| curl -s -H "Authorization: token $GH_TOKEN" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "https://api.github.com/repos/${GITHUB_REPOSITORY}/issues?state=all&per_page=100" \ | |
| > raw_issues.json | |
| echo "Fetched issues count:" | |
| node -e "const fs=require('fs');const x=JSON.parse(fs.readFileSync('raw_issues.json','utf8'));console.log(Array.isArray(x)?x.length:x)" | |
| - name: Parse APPROVED issues with Node.js | |
| run: | | |
| cat > parse-builds.js <<'EOF' | |
| const fs = require('fs'); | |
| const APPROVAL_LABEL = 'Showcase Approved'; | |
| const raw = JSON.parse(fs.readFileSync('raw_issues.json','utf8')); | |
| if (!Array.isArray(raw)) { | |
| console.log('WARN: raw_issues.json is not an array. Content:', raw); | |
| fs.writeFileSync('docs/builds.json', '[]'); | |
| process.exit(0); | |
| } | |
| const clean = s => (s || '').trim().replace(/^no response$/i, ''); | |
| function hasApprovalLabel(labels = []) { | |
| return labels.some(l => (l.name || '').trim().toLowerCase() === APPROVAL_LABEL.toLowerCase()); | |
| } | |
| function extract(section, body, fallback = "") { | |
| if (!body) return fallback; | |
| const re = new RegExp(`###\\s*${section}\\s*\\n+([\\s\\S]*?)(?=\\n###|$)`, 'i'); | |
| const m = body.match(re); | |
| return m ? clean(m[1]) : fallback; | |
| } | |
| // Grab image URLs from Markdown, HTML, and plain links (including GitHub user-attachments) | |
| function extractImageLinks(text) { | |
| const t = clean(text); | |
| if (!t) return []; | |
| const urls = new Set(); | |
| // Markdown images:  | |
| for (const m of t.matchAll(/!\[[^\]]*\]\((?<url>[^)\s]+)(?:\s+"[^"]*")?\)/g)) { | |
| if (m.groups?.url) urls.add(m.groups.url); | |
| } | |
| // HTML <img src="..."> | |
| for (const m of t.matchAll(/<img[^>]+src=["']([^"']+)["']/gi)) { | |
| urls.add(m[1]); | |
| } | |
| // Any plain http(s) link | |
| for (const m of t.matchAll(/https?:\/\/\S+/gi)) { | |
| urls.add(m[0].replace(/[),.]+$/, '')); // trim trailing punctuation | |
| } | |
| // Filter to "likely image" URLs: | |
| const likely = Array.from(urls).filter(u => | |
| /\.(png|jpg|jpeg|gif|webp)(\?|#|$)/i.test(u) || | |
| u.includes('/user-attachments/assets/') || | |
| u.includes('githubusercontent.com') | |
| ); | |
| return Array.from(new Set(likely)); // dedupe | |
| } | |
| const approved = raw.filter(it => !it.pull_request && hasApprovalLabel(it.labels || [])); | |
| console.log(`Approved issues found: ${approved.length}`); | |
| const builds = approved.map(issue => { | |
| const body = issue.body || ""; | |
| const projectName = extract('Project Name', body, issue.title); | |
| const author = extract('Your Name or Team', body, issue.user?.login || ''); | |
| const repoLink = clean(extract('Repository/Code Link', body)) || undefined; | |
| const liveDemo = clean(extract('Live Demo', body)) || undefined; | |
| const description = extract('Description', body); | |
| const instructions = extract('How to Run/Use It', body); | |
| const screenshots = extractImageLinks(extract('Screenshots or GIFs', body)); | |
| return { | |
| // Basic | |
| title: issue.title, | |
| url: issue.html_url, | |
| created_at: issue.created_at, | |
| // Submitter metadata | |
| submitter: issue.user?.login || '', | |
| avatarUrl: issue.user?.avatar_url || null, | |
| authorProfile: issue.user?.html_url || null, | |
| // Parsed fields | |
| projectName, author, repoLink, liveDemo, description, instructions, screenshots | |
| }; | |
| }); | |
| fs.writeFileSync('docs/builds.json', JSON.stringify(builds, null, 2)); | |
| EOF | |
| node parse-builds.js | |
| - name: Create Pull Request with updated builds.json (draft) | |
| uses: peter-evans/create-pull-request@v6 | |
| with: | |
| token: ${{ secrets.PR_CREATION_TOKEN }} # PAT | |
| commit-message: "Update enriched community builds JSON" | |
| branch: "auto/builds-update-${{ github.run_id }}" | |
| title: "chore: Update community builds JSON" | |
| body: | | |
| This PR updates `docs/builds.json` with the latest *approved* community build issues. | |
| Trigger: ${{ github.event_name }} | |
| See logs above for fetched/approved counts. | |
| labels: | | |
| automated-pr | |
| community-builds | |
| draft: true | |
| add-paths: | | |
| docs/builds.json | |
| committer: "Investec Bot <bot@investec.com>" | |
| author: "Investec Bot <bot@investec.com>" |