| name | github-profile-dashboard |
|---|---|
| description | Build a GitHub profile README (the special `<username>/<username>` repo) with live stats/streak/top-languages cards, and optionally self-host the stats API on the user's own AWS account so it never depends on the flaky shared vercel.app deployment. Use when the user asks for a "GitHub profile README", "profile dashboard", "stats card for my GitHub", or shows a screenshot of a GitHub profile with stats cards and asks to replicate it. |
GitHub profile dashboard
Builds the special profile README (github.com/<username>/<username>) with stat/streak/top-language cards, skill icons, and social badges — the "dashboard" look popular on GitHub profiles. Optionally self-hosts the stats-card API on the user's own AWS account, because the public github-readme-stats.vercel.app deployment is shared by millions of profiles and frequently returns 503 DEPLOYMENT_PAUSED.
Never hardcode a specific person's GitHub username, AWS account ID, resource IDs/URLs, or tokens in this skill's files — everything below is resolved live from the current user/session. Treat this skill file itself as public and shareable.
1. Gather inputs
Ask (or infer from context/repo) before writing anything:
- GitHub username (
gh api user --jq .loginif aghsession exists) - Tagline / role, languages spoken, tech stack, social links (LinkedIn, site, X, etc.) — pull from an existing portfolio/CV in the repo if one exists, don't invent facts
- Color theme for the cards — default to
draculaunless the user prefers another (see https://github.com/anuraghazra/github-readme-stats#themes for the full list)
2. Check whether the profile repo already exists
gh repo view <username>/<username> 2>&1
If it exists, read the current README before overwriting — it may contain project lists, work history, etc. that shouldn't be silently discarded. Confirm with the user whether to replace, merge, or leave it.
3. Write the README
Use templates/README.template.md as the base. Replace every {{PLACEHOLDER}} and fill in the "Let's Connect" links only with socials the user actually confirmed. Point the stats/top-langs <img> tags at https://github-readme-stats.vercel.app/... initially — only switch them to a self-hosted origin (step 4) if the user hits the DEPLOYMENT_PAUSED problem or explicitly asks for reliability/self-hosting up front.
Push it:
gh repo clone <username>/<username> /tmp/profile-repo # or gh repo create <username>/<username> --public if it doesn't exist
cp README.md /tmp/profile-repo/README.md
cd /tmp/profile-repo && git add README.md && git commit -m "Update profile README" && git push
4. (Optional) Self-host the stats API on the user's AWS account
Only do this if the user has AWS credentials configured (aws sts get-caller-identity succeeds) and asks for it — this creates real, billable (though near-$0 for personal traffic) resources in their account. Confirm before creating cloud resources.
Run scripts/deploy.sh <github-username> (see that file for the full, commented pipeline). It:
- Clones
anuraghazra/github-readme-stats(it ships an officialexpress.jsself-host entrypoint — no need to fork it). - Builds a Lambda container image using the AWS Lambda Web Adapter (
templates/Dockerfile.lambda) — must build withdocker build --provenance=false --sbom=false, otherwise Lambda rejects the image manifest (InvalidParameterValueException: image manifest ... not supported). - Pushes it to a new ECR repo, creates an IAM execution role, and creates the Lambda function (
--architectures arm64matches Apple Silicon dev machines and is cheaper). - Creates a public Function URL. Critical, easy to miss: since Oct 2025, a
NONE-auth Function URL needs two separateadd-permissioncalls —lambda:InvokeFunctionUrlANDlambda:InvokeFunction(the second with--invoked-via-function-url). Missing the second one silently 403s even though the URL config looks correct. - Sets the GitHub token as Lambda env var
PAT_1(notGITHUB_TOKEN— that's the actual env var namegithub-readme-stats's retry logic looks for, matched via regex/PAT_\d*$/). Prefer reusing an already-authenticatedgh auth tokenover asking the user to paste a new token into chat. Never print the token value in any tool output or file. - Creates a CloudFront distribution in front of the Function URL with a custom cache policy that forwards all query strings (the cache key must vary by
username/theme/etc. — the AWS-managedCachingOptimizedpolicy drops query strings, which would serve the same cached image regardless of params). Seetemplates/cache-policy.jsonandtemplates/cf-dist.json.
Why CloudFront is necessary, not optional, for reliability
The top-langs endpoint's GraphQL query (languages for up to 100 repos) genuinely takes GitHub's API 3.5–5s to answer — this is server-side latency on GitHub's end, not something fixable via Lambda memory/CPU or DNS/IPv6 tuning (both were tried and made no difference). GitHub's own image proxy (camo.githubusercontent.com, which is what actually fetches badge images embedded in READMEs) times out around 4s and shows "Error Fetching Resource" on any cold fetch that takes longer. CloudFront absorbs that latency: first hit per cache key is slow, everything after is a fast edge hit, so camo essentially never sees the slow path once the cache is warm. After creating the distribution, proactively curl each final image URL once yourself to warm the cache before telling the user to check their profile.
After deploy.sh finishes
Update the profile README's stats/top-langs <img src> to the printed CloudFront domain, commit, push.
5. Verify before declaring done
curl -s -o /dev/null -w "%{http_code}\n" "<cloudfront-domain>/api?username=<user>&show_icons=true&theme=<theme>"
curl -s -o /dev/null -w "%{http_code}\n" "<cloudfront-domain>/api/top-langs/?username=<user>&layout=compact&theme=<theme>"
Both must return 200 before telling the user to refresh their profile. Remind them the browser/GitHub page itself may cache the old image — a hard refresh (Cmd/Ctrl+Shift+R) may be needed even after the backend is fixed.
Teardown (if the user wants to stop paying/using it later)
aws cloudfront get-distribution-config --id <dist-id> # get ETag, set Enabled=false, update, wait for deploy, THEN delete
aws cloudfront delete-distribution --id <dist-id> --if-match <etag>
aws lambda delete-function --function-name <fn-name>
aws ecr delete-repository --repository-name <ecr-repo> --force
aws iam detach-role-policy --role-name <role-name> --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name <role-name>
