Skip to content

[Build Submission] Zebra Finance - AI-Powered Personal Finance Assistant #30

[Build Submission] Zebra Finance - AI-Powered Personal Finance Assistant

[Build Submission] Zebra Finance - AI-Powered Personal Finance Assistant #30

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: ![alt](url "title")
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>"