Investec Kotlin SDK #35
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: | |
| if: > | |
| github.event_name == 'workflow_dispatch' || | |
| (github.event_name == 'issues' && | |
| github.event.action == 'labeled' && | |
| github.event.label.name == 'Showcase Approved') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout default branch | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.repository.default_branch }} | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Brief wait | |
| if: github.event_name == 'issues' | |
| run: sleep 10 | |
| - name: Fetch all issues with pagination | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p docs | |
| rm -f raw_issues_pages.json | |
| url="https://api.github.com/repos/${REPO}/issues?state=all&per_page=100" | |
| page=1 | |
| while [ -n "$url" ]; do | |
| echo "Fetching page $page: $url" | |
| headers_file="$(mktemp)" | |
| body_file="$(mktemp)" | |
| curl -sS -L \ | |
| -D "$headers_file" \ | |
| -o "$body_file" \ | |
| -H "Authorization: Bearer $GH_TOKEN" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "$url" | |
| cat "$body_file" >> raw_issues_pages.json | |
| echo >> raw_issues_pages.json | |
| next_url="$(awk ' | |
| BEGIN { IGNORECASE=1 } | |
| /^link:/ { | |
| line=$0 | |
| while (match(line, /<[^>]+>; rel="[^"]+"/)) { | |
| part=substr(line, RSTART, RLENGTH) | |
| if (part ~ /rel="next"/) { | |
| gsub(/^</, "", part) | |
| sub(/>; rel="next"$/, "", part) | |
| print part | |
| exit | |
| } | |
| line=substr(line, RSTART + RLENGTH) | |
| } | |
| } | |
| ' "$headers_file")" | |
| rm -f "$headers_file" "$body_file" | |
| url="$next_url" | |
| page=$((page + 1)) | |
| done | |
| - name: Merge paginated issue payloads | |
| run: | | |
| set -euo pipefail | |
| node <<'EOF' | |
| const fs = require('fs'); | |
| const text = fs.readFileSync('raw_issues_pages.json', 'utf8').trim(); | |
| const blocks = text | |
| .split(/\n(?=\s*\[)/) | |
| .map(s => s.trim()) | |
| .filter(Boolean); | |
| const merged = []; | |
| for (const block of blocks) { | |
| const parsed = JSON.parse(block); | |
| if (Array.isArray(parsed)) merged.push(...parsed); | |
| } | |
| fs.writeFileSync('raw_issues.json', JSON.stringify(merged, null, 2)); | |
| console.log(`Fetched issues count: ${merged.length}`); | |
| EOF | |
| - name: Parse approved issues into docs/builds.json | |
| run: | | |
| set -euo pipefail | |
| 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', '[]\n'); | |
| 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; | |
| } | |
| function extractImageLinks(text) { | |
| const t = clean(text); | |
| if (!t) return []; | |
| const urls = new Set(); | |
| for (const m of t.matchAll(/!\[[^\]]*\]\((?<url>[^)\s]+)(?:\s+"[^"]*")?\)/g)) { | |
| if (m.groups?.url) urls.add(m.groups.url); | |
| } | |
| for (const m of t.matchAll(/<img[^>]+src=["']([^"']+)["']/gi)) { | |
| urls.add(m[1]); | |
| } | |
| for (const m of t.matchAll(/https?:\/\/\S+/gi)) { | |
| urls.add(m[0].replace(/[),.]+$/, '')); | |
| } | |
| 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)); | |
| } | |
| 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 || ''; | |
| return { | |
| title: issue.title, | |
| url: issue.html_url, | |
| created_at: issue.created_at, | |
| updated_at: issue.updated_at, | |
| issue_number: issue.number, | |
| submitter: issue.user?.login || '', | |
| avatarUrl: issue.user?.avatar_url || null, | |
| authorProfile: issue.user?.html_url || null, | |
| projectName: extract('Project Name', body, issue.title), | |
| author: extract('Your Name or Team', body, issue.user?.login || ''), | |
| repoLink: clean(extract('Repository/Code Link', body)) || undefined, | |
| liveDemo: clean(extract('Live Demo', body)) || undefined, | |
| description: extract('Description', body), | |
| instructions: extract('How to Run/Use It', body), | |
| screenshots: extractImageLinks(extract('Screenshots or GIFs', body)) | |
| }; | |
| }); | |
| builds.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); | |
| fs.writeFileSync('docs/builds.json', JSON.stringify(builds, null, 2) + '\n'); | |
| EOF | |
| node parse-builds.js | |
| - name: Create or update draft pull request | |
| id: cpr | |
| uses: peter-evans/create-pull-request@v8 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| branch-token: ${{ secrets.GITHUB_TOKEN }} | |
| branch: auto/builds-update | |
| delete-branch: true | |
| commit-message: "chore: update community builds JSON" | |
| title: "chore: update community builds JSON" | |
| body: | | |
| This PR updates `docs/builds.json` with the latest approved community build issues. | |
| Trigger: `${{ github.event_name }}` | |
| Workflow run: `${{ github.run_id }}` | |
| labels: | | |
| automated-pr | |
| community-builds | |
| draft: always-true | |
| add-paths: | | |
| docs/builds.json | |
| committer: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" | |
| author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" | |
| - name: Report result | |
| if: always() | |
| run: | | |
| echo "PR operation: ${{ steps.cpr.outputs.pull-request-operation }}" | |
| echo "PR number: ${{ steps.cpr.outputs.pull-request-number }}" | |
| echo "PR url: ${{ steps.cpr.outputs.pull-request-url }}" |