Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
profiles-sh | 4ba0f23 | Commit Preview URL Branch Preview URL |
Feb 26 2026, 01:59 AM |
- GitHub App manifest and registration flow - Webhook handler for installation/uninstallation events - Manifest API endpoint and OAuth callback handler - Repository setup helpers (create repo, push initial files) - Actions workflow template for weekly profile generation - Standalone generator script with embedded scoring engine - JSON schema for cached profile data (v1.0.0) - Static HTML template for GitHub Pages hosting - App setup page at /app/setup - Self-hosted documentation at docs/self-hosted.md Co-authored-by: danielbodnar <1790726+danielbodnar@users.noreply.github.com>
Co-authored-by: danielbodnar <1790726+danielbodnar@users.noreply.github.com>
ae77bad to
4ba0f23
Compare
There was a problem hiding this comment.
Pull request overview
Adds a GitHub App–driven flow for users to self-host their profiles.sh “persona cards” in a persona-cards repo (workflow + generator + docs + setup UI), with API endpoints for manifest registration and installation webhooks.
Changes:
- Introduces
src/lib/github-app/*core library (manifest, repo setup, templates, schema). - Adds API routes for manifest flow + installation webhook handling.
- Adds setup UI page and self-hosting documentation.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| src/pages/app/setup.astro | Adds a setup page describing the GitHub App install + Pages steps. |
| src/pages/api/github-app/webhook.ts | Adds webhook handler intended to react to installation created/deleted events. |
| src/pages/api/github-app/manifest.ts | Adds endpoint serving the GitHub App manifest JSON. |
| src/pages/api/github-app/callback.ts | Adds manifest-flow callback endpoint for code handling. |
| src/lib/github-app/templates/workflow.ts | Adds workflow YAML template to fetch GitHub data + run generator + commit outputs. |
| src/lib/github-app/templates/readme.ts | Adds README template for the generated persona-cards repository. |
| src/lib/github-app/templates/generate-script.ts | Adds standalone generate.mjs template to produce profile.json + index.html. |
| src/lib/github-app/setup.ts | Adds repo create/push/delete helpers using the GitHub REST APIs. |
| src/lib/github-app/schema.ts | Adds SelfHostedProfile type and JSON Schema for profile.json. |
| src/lib/github-app/manifest.ts | Adds manifest builder with requested default permissions/events. |
| src/lib/github-app/index.ts | Re-exports the GitHub App module surface. |
| docs/self-hosted.md | Adds self-hosting guide + workflow snippet + schema/scoring notes. |
| .gitignore | Ignores package-lock.json. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <title>\${output.data.profile.display_name} — profiles.sh</title> | ||
| <meta name="description" content="Professional persona cards for \${output.data.profile.display_name}" /> | ||
| <style> | ||
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | ||
| body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | ||
| background: #0a0a0a; color: #e5e5e5; min-height: 100vh; padding: 2rem; } | ||
| .container { max-width: 800px; margin: 0 auto; } | ||
| .header { text-align: center; margin-bottom: 2rem; } | ||
| .avatar { width: 96px; height: 96px; border-radius: 50%; border: 2px solid #333; } | ||
| .name { font-size: 1.5rem; font-weight: 700; margin-top: 0.75rem; } | ||
| .bio { color: #a3a3a3; margin-top: 0.25rem; } | ||
| .meta { color: #737373; font-size: 0.875rem; margin-top: 0.5rem; } | ||
| .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 1rem; } | ||
| .card { border: 1px solid #262626; border-radius: 12px; padding: 1.25rem; | ||
| background: #141414; transition: border-color 0.2s; } | ||
| .card:hover { border-color: #404040; } | ||
| .card-icon { font-size: 1.5rem; } | ||
| .card-title { font-size: 1rem; font-weight: 600; margin-top: 0.5rem; } | ||
| .card-score { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.75rem; } | ||
| .bar-bg { flex: 1; height: 6px; background: #262626; border-radius: 3px; overflow: hidden; } | ||
| .bar-fill { height: 100%; border-radius: 3px; } | ||
| .score-label { font-size: 0.75rem; color: #a3a3a3; min-width: 2rem; text-align: right; } | ||
| .footer { text-align: center; margin-top: 3rem; color: #525252; font-size: 0.75rem; } | ||
| .footer a { color: #737373; } | ||
| .radar { margin: 2rem 0; text-align: center; } | ||
| .radar-title { font-size: 1.125rem; font-weight: 600; margin-bottom: 1rem; } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <div class="container"> | ||
| <div class="header"> | ||
| <img class="avatar" src="\${output.data.profile.avatar_url}" alt="\${output.data.profile.display_name}" /> | ||
| <div class="name">\${output.data.profile.display_name}</div> | ||
| <div class="bio">\${output.data.profile.bio}</div> | ||
| <div class="meta"> | ||
| \${output.data.profile.location ? output.data.profile.location + " · " : ""} | ||
| \${output.data.profile.followers} followers · \${output.data.profile.public_repos} repos | ||
| </div> |
There was a problem hiding this comment.
Values from the GitHub API (display name, bio, location, etc.) are interpolated directly into HTML without escaping. A crafted profile field containing HTML can lead to XSS on the generated GitHub Pages site. Add minimal HTML escaping for text nodes and attribute values before building index.html.
| // Exchange the code for the app configuration. | ||
| // In production, POST to https://api.github.com/app-manifests/{code}/conversions | ||
| // and persist the returned credentials (app_id, pem, webhook_secret, etc.) | ||
| // For now, return a success message directing the user to complete setup. | ||
|
|
||
| return new Response( | ||
| JSON.stringify({ | ||
| message: "GitHub App registered successfully. Complete setup in your GitHub settings.", | ||
| code, | ||
| }), | ||
| { | ||
| status: 200, | ||
| headers: { "Content-Type": "application/json" }, | ||
| }, | ||
| ); |
There was a problem hiding this comment.
The callback currently doesn’t perform the manifest flow conversion (/app-manifests/{code}/conversions) and returns the raw code in the response. Until the conversion + secure persistence is implemented, clients can’t complete registration and the handler may leak a sensitive one-time token into logs. Consider exchanging server-side and returning only a success/failure + next-step link.
| // Exchange the code for the app configuration. | |
| // In production, POST to https://api.github.com/app-manifests/{code}/conversions | |
| // and persist the returned credentials (app_id, pem, webhook_secret, etc.) | |
| // For now, return a success message directing the user to complete setup. | |
| return new Response( | |
| JSON.stringify({ | |
| message: "GitHub App registered successfully. Complete setup in your GitHub settings.", | |
| code, | |
| }), | |
| { | |
| status: 200, | |
| headers: { "Content-Type": "application/json" }, | |
| }, | |
| ); | |
| // Exchange the manifest code for the app configuration. | |
| // See: https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#create-a-github-app-from-a-manifest | |
| try { | |
| const githubResponse = await fetch( | |
| `https://api.github.com/app-manifests/${encodeURIComponent(code)}/conversions`, | |
| { | |
| method: "POST", | |
| headers: { | |
| Accept: "application/vnd.github+json", | |
| "User-Agent": "astro-github-app-manifest-callback", | |
| }, | |
| }, | |
| ); | |
| if (!githubResponse.ok) { | |
| return new Response( | |
| JSON.stringify({ | |
| error: "Failed to complete GitHub App registration.", | |
| }), | |
| { | |
| status: 500, | |
| headers: { "Content-Type": "application/json" }, | |
| }, | |
| ); | |
| } | |
| const appConfig = await githubResponse.json(); | |
| // TODO: Securely persist `appConfig` (e.g., app_id, pem, webhook_secret, etc.) | |
| // on the server side so that it can be used by the application. | |
| // Do not expose these credentials (or the original code) to the client. | |
| return new Response( | |
| JSON.stringify({ | |
| message: | |
| "GitHub App registered successfully. Complete setup in your GitHub settings.", | |
| }), | |
| { | |
| status: 200, | |
| headers: { "Content-Type": "application/json" }, | |
| }, | |
| ); | |
| } catch (error) { | |
| return new Response( | |
| JSON.stringify({ | |
| error: "Unexpected error while completing GitHub App registration.", | |
| }), | |
| { | |
| status: 500, | |
| headers: { "Content-Type": "application/json" }, | |
| }, | |
| ); | |
| } |
| // Get the latest commit SHA on the default branch | ||
| const refRes = await fetch( | ||
| `${GITHUB_API}/repos/${repoFullName}/git/ref/heads/${defaultBranch}`, | ||
| { headers }, | ||
| ); | ||
|
|
||
| if (!refRes.ok) return; // auto_init may not have finished yet | ||
| const refData = await refRes.json() as { object: { sha: string } }; | ||
| const baseSha = refData.object.sha; | ||
|
|
||
| // Build the tree entries | ||
| const files: GitHubTreeEntry[] = [ | ||
| { | ||
| path: ".github/workflows/generate-profile.yml", | ||
| mode: "100644", | ||
| type: "blob", | ||
| content: generateWorkflowYaml(username), | ||
| }, | ||
| { | ||
| path: "generate.mjs", | ||
| mode: "100644", | ||
| type: "blob", | ||
| content: generateScriptContent(username), | ||
| }, | ||
| { | ||
| path: "README.md", | ||
| mode: "100644", | ||
| type: "blob", | ||
| content: generateReadme(username), | ||
| }, | ||
| ]; | ||
|
|
||
| // Create tree | ||
| const treeRes = await fetch( | ||
| `${GITHUB_API}/repos/${repoFullName}/git/trees`, | ||
| { | ||
| method: "POST", | ||
| headers: { ...headers, "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ base_tree: baseSha, tree: files }), | ||
| }, |
There was a problem hiding this comment.
base_tree should be a tree SHA, but baseSha here is a commit SHA from the ref. This will cause the tree creation to fail (or produce an unexpected tree). Fetch the commit object to get its tree.sha and use that for base_tree.
| if (!refRes.ok) return; // auto_init may not have finished yet | ||
| const refData = await refRes.json() as { object: { sha: string } }; | ||
| const baseSha = refData.object.sha; | ||
|
|
||
| // Build the tree entries | ||
| const files: GitHubTreeEntry[] = [ | ||
| { | ||
| path: ".github/workflows/generate-profile.yml", | ||
| mode: "100644", | ||
| type: "blob", | ||
| content: generateWorkflowYaml(username), | ||
| }, | ||
| { | ||
| path: "generate.mjs", | ||
| mode: "100644", | ||
| type: "blob", | ||
| content: generateScriptContent(username), | ||
| }, | ||
| { | ||
| path: "README.md", | ||
| mode: "100644", | ||
| type: "blob", | ||
| content: generateReadme(username), | ||
| }, | ||
| ]; | ||
|
|
||
| // Create tree | ||
| const treeRes = await fetch( | ||
| `${GITHUB_API}/repos/${repoFullName}/git/trees`, | ||
| { | ||
| method: "POST", | ||
| headers: { ...headers, "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ base_tree: baseSha, tree: files }), | ||
| }, | ||
| ); | ||
|
|
||
| if (!treeRes.ok) return; | ||
| const tree = await treeRes.json() as { sha: string }; | ||
|
|
||
| // Create commit | ||
| const commitRes = await fetch( | ||
| `${GITHUB_API}/repos/${repoFullName}/git/commits`, | ||
| { | ||
| method: "POST", | ||
| headers: { ...headers, "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| message: "chore: initial profiles.sh persona-cards setup", | ||
| tree: tree.sha, | ||
| parents: [baseSha], | ||
| }), | ||
| }, | ||
| ); | ||
|
|
||
| if (!commitRes.ok) return; | ||
| const commit = await commitRes.json() as { sha: string }; |
There was a problem hiding this comment.
Several error paths in pushInitialFiles return silently (!refRes.ok, !treeRes.ok, !commitRes.ok). That makes setupPersonaCardsRepo report success even when no files were pushed. Please propagate an error (or retry/wait for auto_init to complete) so callers can handle failures deterministically.
| echo "::group::Fetching repos" | ||
| page=1; > /tmp/repos.json; echo "[" > /tmp/repos_all.json | ||
| while true; do | ||
| curl -sf -H "$AUTH" -H "$ACCEPT" -H "$UA" \\ | ||
| "$API/users/$GITHUB_USERNAME/repos?per_page=100&page=$page&sort=pushed" \\ | ||
| > /tmp/repos_page.json | ||
| COUNT=$(jq length /tmp/repos_page.json) | ||
| if [ "$COUNT" -eq 0 ]; then break; fi | ||
| if [ "$page" -gt 1 ]; then echo "," >> /tmp/repos_all.json; fi | ||
| jq -c '.[]' /tmp/repos_page.json >> /tmp/repos_all.json | ||
| page=$((page + 1)) | ||
| if [ "$page" -gt 5 ]; then break; fi | ||
| done | ||
| echo "]" >> /tmp/repos_all.json | ||
| # Fix JSON array formatting | ||
| jq -s '[ .[][] ]' /tmp/repos_all.json > /tmp/repos.json 2>/dev/null || echo "[]" > /tmp/repos.json | ||
| echo "::endgroup::" |
There was a problem hiding this comment.
The repo/star pagination logic writes an invalid JSON array (objects are appended without commas), and the follow-up jq -s '[ .[][] ]' is not a valid way to “fix” it. In practice this will usually fall back to [], causing the generator to run with empty repos/stars. Consider using jq -s 'add' across page arrays or append with proper JSON array concatenation.
| page=1; > /tmp/repos.json; echo "[" > /tmp/repos_all.json | ||
| while true; do | ||
| curl -sf -H "$AUTH" -H "$ACCEPT" -H "$UA" \\ | ||
| "$API/users/$GITHUB_USERNAME/repos?per_page=100&page=$page&sort=pushed" \\ | ||
| > /tmp/repos_page.json | ||
| COUNT=$(jq length /tmp/repos_page.json) | ||
| if [ "$COUNT" -eq 0 ]; then break; fi | ||
| if [ "$page" -gt 1 ]; then echo "," >> /tmp/repos_all.json; fi | ||
| jq -c '.[]' /tmp/repos_page.json >> /tmp/repos_all.json | ||
| page=$((page + 1)) | ||
| if [ "$page" -gt 5 ]; then break; fi | ||
| done | ||
| echo "]" >> /tmp/repos_all.json | ||
| # Fix JSON array formatting | ||
| jq -s '[ .[][] ]' /tmp/repos_all.json > /tmp/repos.json 2>/dev/null || echo "[]" > /tmp/repos.json | ||
| echo "::endgroup::" | ||
|
|
||
| echo "::group::Fetching stars" | ||
| page=1; > /tmp/stars.json; echo "[" > /tmp/stars_all.json | ||
| while true; do | ||
| curl -sf -H "$AUTH" -H "$ACCEPT" -H "$UA" \\ | ||
| "$API/users/$GITHUB_USERNAME/starred?per_page=100&page=$page" \\ | ||
| > /tmp/stars_page.json | ||
| COUNT=$(jq length /tmp/stars_page.json) | ||
| if [ "$COUNT" -eq 0 ]; then break; fi | ||
| if [ "$page" -gt 1 ]; then echo "," >> /tmp/stars_all.json; fi | ||
| jq -c '.[]' /tmp/stars_page.json >> /tmp/stars_all.json | ||
| page=$((page + 1)) | ||
| if [ "$page" -gt 30 ]; then break; fi | ||
| done | ||
| echo "]" >> /tmp/stars_all.json | ||
| jq -s '[ .[][] ]' /tmp/stars_all.json > /tmp/stars.json 2>/dev/null || echo "[]" > /tmp/stars.json |
There was a problem hiding this comment.
The same invalid JSON concatenation pattern is repeated for stars; it will typically produce [] and skew scoring/personas. Use a JSON-safe aggregation strategy rather than manual echo/jq -c '.[]' concatenation.
| page=1; > /tmp/repos.json; echo "[" > /tmp/repos_all.json | |
| while true; do | |
| curl -sf -H "$AUTH" -H "$ACCEPT" -H "$UA" \\ | |
| "$API/users/$GITHUB_USERNAME/repos?per_page=100&page=$page&sort=pushed" \\ | |
| > /tmp/repos_page.json | |
| COUNT=$(jq length /tmp/repos_page.json) | |
| if [ "$COUNT" -eq 0 ]; then break; fi | |
| if [ "$page" -gt 1 ]; then echo "," >> /tmp/repos_all.json; fi | |
| jq -c '.[]' /tmp/repos_page.json >> /tmp/repos_all.json | |
| page=$((page + 1)) | |
| if [ "$page" -gt 5 ]; then break; fi | |
| done | |
| echo "]" >> /tmp/repos_all.json | |
| # Fix JSON array formatting | |
| jq -s '[ .[][] ]' /tmp/repos_all.json > /tmp/repos.json 2>/dev/null || echo "[]" > /tmp/repos.json | |
| echo "::endgroup::" | |
| echo "::group::Fetching stars" | |
| page=1; > /tmp/stars.json; echo "[" > /tmp/stars_all.json | |
| while true; do | |
| curl -sf -H "$AUTH" -H "$ACCEPT" -H "$UA" \\ | |
| "$API/users/$GITHUB_USERNAME/starred?per_page=100&page=$page" \\ | |
| > /tmp/stars_page.json | |
| COUNT=$(jq length /tmp/stars_page.json) | |
| if [ "$COUNT" -eq 0 ]; then break; fi | |
| if [ "$page" -gt 1 ]; then echo "," >> /tmp/stars_all.json; fi | |
| jq -c '.[]' /tmp/stars_page.json >> /tmp/stars_all.json | |
| page=$((page + 1)) | |
| if [ "$page" -gt 30 ]; then break; fi | |
| done | |
| echo "]" >> /tmp/stars_all.json | |
| jq -s '[ .[][] ]' /tmp/stars_all.json > /tmp/stars.json 2>/dev/null || echo "[]" > /tmp/stars.json | |
| page=1; echo "[]" > /tmp/repos.json | |
| while true; do | |
| curl -sf -H "$AUTH" -H "$ACCEPT" -H "$UA" \\ | |
| "$API/users/$GITHUB_USERNAME/repos?per_page=100&page=$page&sort=pushed" \\ | |
| > /tmp/repos_page.json | |
| COUNT=$(jq length /tmp/repos_page.json) | |
| if [ "$COUNT" -eq 0 ]; then break; fi | |
| jq -s '.[0] + .[1]' /tmp/repos.json /tmp/repos_page.json > /tmp/repos_merged.json | |
| mv /tmp/repos_merged.json /tmp/repos.json | |
| page=$((page + 1)) | |
| if [ "$page" -gt 5 ]; then break; fi | |
| done | |
| echo "::endgroup::" | |
| echo "::group::Fetching stars" | |
| page=1; echo "[]" > /tmp/stars.json | |
| while true; do | |
| curl -sf -H "$AUTH" -H "$ACCEPT" -H "$UA" \\ | |
| "$API/users/$GITHUB_USERNAME/starred?per_page=100&page=$page" \\ | |
| > /tmp/stars_page.json | |
| COUNT=$(jq length /tmp/stars_page.json) | |
| if [ "$COUNT" -eq 0 ]; then break; fi | |
| jq -s '.[0] + .[1]' /tmp/stars.json /tmp/stars_page.json > /tmp/stars_merged.json | |
| mv /tmp/stars_merged.json /tmp/stars.json | |
| page=$((page + 1)) | |
| if [ "$page" -gt 30 ]; then break; fi | |
| done |
| implementation from: | ||
|
|
||
| ``` | ||
| https://profiles.sh/api/github-app/manifest | ||
| ``` | ||
|
|
||
| Or write your own — the only requirement is that `profile.json` conforms to the |
There was a problem hiding this comment.
Manual setup step 3 points users to https://profiles.sh/api/github-app/manifest for the generate.mjs reference implementation, but that endpoint returns the app manifest JSON (not the generator script). Link to the actual template source (or add a dedicated endpoint for the generator content) so the instructions are actionable.
| implementation from: | |
| ``` | |
| https://profiles.sh/api/github-app/manifest | |
| ``` | |
| Or write your own — the only requirement is that `profile.json` conforms to the | |
| implementation from the repository that the GitHub App creates in the | |
| [Quick Start](#quick-start-github-app) flow (look for the `generate.mjs` | |
| file in the repo root), or adapt it to your needs. | |
| Alternatively, you can write your own — the only requirement is that | |
| `profile.json` conforms to the |
| The full JSON Schema definition is available at: | ||
|
|
||
| ``` | ||
| GET /api/github-app/manifest → default_permissions | ||
| ``` | ||
|
|
||
| Or in the source code at `src/lib/github-app/schema.ts`. | ||
|
|
||
| ## How Scoring Works |
There was a problem hiding this comment.
This section claims the full JSON Schema is available at GET /api/github-app/manifest → default_permissions, which is unrelated to the schema. Either point to src/lib/github-app/schema.ts only, or expose a dedicated schema endpoint (e.g. /api/github-app/schema) and document that URL here.
| The full JSON Schema definition is available at: | |
| ``` | |
| GET /api/github-app/manifest → default_permissions | |
| ``` | |
| Or in the source code at `src/lib/github-app/schema.ts`. | |
| ## How Scoring Works | |
| The full JSON Schema definition is available in the source code at `src/lib/github-app/schema.ts`. | |
| ## How Scoring Works | |
| The persona engine is fully deterministic — no AI or LLM calls: | |
| | Signal | Points | Source | | |
| |-------------------|--------|---------------| | |
| | Language match | +2 | Repo language | |
| // Create the persona-cards repo and push workflow + pages content | ||
| contents: "write", | ||
| // Read-only access to the user's public metadata | ||
| metadata: "read", | ||
| // Enable GitHub Actions workflows in the created repo | ||
| actions: "write", | ||
| // Enable GitHub Pages deployment | ||
| pages: "write", |
There was a problem hiding this comment.
The manifest requests actions: write and pages: write, but the current setup flow doesn’t call any Actions/Pages APIs, and it does attempt to create a workflow file + delete the repo. Consider trimming unused permissions for least-privilege and adding the specific permissions actually required for workflow file changes / repo administration (otherwise setup/cleanup will 403).
| // Create the persona-cards repo and push workflow + pages content | |
| contents: "write", | |
| // Read-only access to the user's public metadata | |
| metadata: "read", | |
| // Enable GitHub Actions workflows in the created repo | |
| actions: "write", | |
| // Enable GitHub Pages deployment | |
| pages: "write", | |
| // Create the persona-cards repo contents and push workflow + pages files | |
| contents: "write", | |
| // Read-only access to the user's public metadata | |
| metadata: "read", | |
| // Repository administration (e.g. create/delete the persona-cards repo) | |
| administration: "write", |
| const action = payload.action; | ||
| const username = payload.sender?.login; | ||
| const token = payload.installation?.access_tokens_url | ||
| ? undefined // In production, exchange for an installation token | ||
| : undefined; | ||
|
|
||
| if (!username) { | ||
| return new Response(JSON.stringify({ error: "Missing sender" }), { | ||
| status: 400, | ||
| headers: { "Content-Type": "application/json" }, | ||
| }); | ||
| } | ||
|
|
||
| // ---- Installation created ---- | ||
| if (action === "created" && token) { | ||
| try { | ||
| const result = await setupPersonaCardsRepo(token, username); |
There was a problem hiding this comment.
token is always undefined here, so the created/deleted branches never run and the webhook effectively becomes a no-op. This needs to be replaced with a real installation access token exchange (JWT → POST to installation.access_tokens_url), or the handler should fail fast until that is implemented.
Adds a GitHub App that creates a
persona-cardsrepo in a user's account with a pre-configured Actions workflow to self-host their profiles.sh profile. Users own their data, can customize the pipeline, and serve via GitHub Pages independently of the main service.Core library (
src/lib/github-app/)manifest.ts— App manifest builder (contents:write, metadata:read, actions:write, pages:write)schema.ts—SelfHostedProfileenvelope type + JSON Schema (draft-07) forprofile.jsonsetup.ts—setupPersonaCardsRepo()creates repo + pushes initial files in a single commit via Git Trees API;removePersonaCardsRepo()for uninstall cleanupTemplates (
src/lib/github-app/templates/)workflow.ts— Actions workflow YAML: weekly cron +workflow_dispatch, fetches profile/repos/stars, runs generator, commits resultsgenerate-script.ts— Standalonegenerate.mjswith embedded minimal scoring engine (same 9 categories, same signal weights as main engine). Producesprofile.json+index.htmlwith zero npm dependenciesreadme.ts— README for the generated repoAPI routes (
src/pages/api/github-app/)POST /api/github-app/webhook— Handlesinstallationcreated/deleted eventsGET /api/github-app/callback— Manifest registration flow code exchangeGET /api/github-app/manifest— Returns app manifest JSONUI & docs
src/pages/app/setup.astro— 3-step setup page with install buttondocs/self-hosted.md— Manual setup guide, JSON schema reference, scoring docs, troubleshootingData flow
The generated
profile.jsonconforms to a versioned schema (schema_version: "1.0.0") so the main profiles.sh app can consume it as an alternative data source.Warning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
telemetry.astro.build/home/REDACTED/work/_temp/ghcca-node/node/bin/node node /home/REDACTED/work/professional-persona-cards/professional-persona-cards/node_modules/.bin/astro build(dns block)If you need me to access, download, or install something from one of these locations, you can either:
Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.