Developer guide

How the project is put together, how to run it locally, and how to add themes, widgets and achievements without breaking the design rules.

Requirements#

git clone https://github.com/billtruong003/git-profile-awaken.git
cd git-profile-awaken
npm install
cp .env.example .env    # put a token in GITHUB_TOKEN
CommandDoes
npm run devWebsite and API at http://localhost:3000, restarting on changes.
npm run generate -- --username NAME --no-readmeWhat the Action runs: writes every SVG to awaken/.
npm testGame rules, contrast rules for every theme, and a render of every widget in every combination.
npm run typecheckStrict TypeScript over source, tests and scripts.
npm run demoRebuilds the images in demo/ used by the README and the website.
npm run vendor:iconsRe-copies language logos from Simple Icons into src/presentation/svg/brandIcons.ts.

Layout#

src/
  domain/types.ts            shared types: RawProfile, Player, AwakenConfig…
  infrastructure/
    githubClient.ts          GraphQL queries, run in parallel
    fonts.ts                 subsets and embeds the two fonts into each SVG
    store.ts                 profile cache: Upstash Redis or memory
    players.ts               memory → store → GitHub, and the nightly refresh
    extras.ts                pinned repositories and RSS/Atom feeds for the Action
  application/               the game rules, no I/O
    population.ts            percentile maths: fitted distributions, z-score composite
    populationData.ts        the fitted model (generated by scripts/fit-population.ts)
    ranks.ts                 percentile ladder, overall rank, level curve
    arsenal.ts               technology names → logo slugs, monograms
    progress.ts              what changed since the last run (Level Up)
    quotes.ts                the bundled quotes and quote of the day
    quests.ts                daily quests: the pool, the draw, grading, the weekly boss, history
    classes.ts               language → job class
    activity.ts              calendar → weeks, streaks, best week
    achievements.ts          the achievements and title choice
    player.ts                RawProfile → Player
  config/                    awaken.json validation and defaults (config.ts, extras.ts)
  presentation/
    theme/                   tokens, contrast helpers, the 31 themes and their packs
    svg/                     kit (frame, sigil, rank effects, text), icons, logos, document wrapper
    widgets/                 one function per widget, Player → SVG body
    layouts.ts               README layouts, line pairing, the Bento composite
    http/router.ts           /api, /api/cron/refresh, static demo files
    web/                     the website: home, docs pages, layout
  cli/                       the Action entry point and README block writer
api/index.ts                 Vercel function, forwards to the router
action.yml                   the composite Action
docs/                        this guide and the user guide (also served at /docs)
test/                        node:test suites

The direction of dependencies is one way: presentation and cli use application, which uses domain. application never fetches anything, so every rule is tested with plain fixtures.

Data flow#

  1. Fetch (githubClient.ts). One profile is about a dozen GraphQL requests. The identity, the last year's calendar, merged pull requests, the active quest and the lifetime commit totals start at once. The repository list is paged (at most three pages, most-starred first) and each page's languages are requested in batches of ten as soon as that page arrives. Typical times: 2–4 s for a few hundred repositories, about 7 s for the largest accounts.
  2. Rules (application/). buildPlayer turns the raw numbers into stats, ranks, level, class, achievements, the equipped title and the activity summary.
  3. Draw (presentation/widgets). Each widget is a function (ctx, player) → { width, height, body, symbols, css }. toSvg wraps it with the type styles, motion keyframes, filters and the icon symbols it used.
  4. Fonts (fonts.ts). Chakra Petch and JetBrains Mono are cut down to the characters the SVG draws and embedded as base64 WOFF2, because GitHub's image proxy cannot load web fonts.

Design rules the code enforces#

The visual language is the Awaken System design system. The parts that matter in code:

Adding a theme#

Add a line to THEMES in src/presentation/theme/themes.ts:

fromSeed('my_theme', 'My Theme',
  seed(bg, panel, ink, muted, system, border),          // dark
  seed(bg, panel, ink, muted, system, border)),         // optional light

Run npm test; the contrast test tells you if a seed could not be brought up to standard. Then npm run demo to add its preview.

Adding an achievement#

Add an entry to ACHIEVEMENTS in src/application/achievements.ts:

{ id: 'archivist', name: 'Archivist', title: 'Archivist', icon: 'scroll',
  unit: 'issues closed', goals: [10, 50, 200], measure: (raw) => raw.issues },

measure returns null when the data behind it was not fetched; the badge then shows as unavailable instead of locked. The icon is a key of ICONS in src/presentation/svg/icons.ts. Add a case to test/rules.test.ts and a row to the table in docs/guide.md. The widget lays badges out four to a row, so a seventeenth one adds a row; raise the widget height in achievementsWidget to match.

Adding an icon#

Icons are single filled paths on a 16 × 16 grid in ICONS, drawn with fill-rule="evenodd" so cut-outs work and one fill color tints them. Keep shapes solid; they are drawn as small as 12 px.

Adding a widget#

  1. Add its id to WIDGET_IDS in src/domain/types.ts.
  2. Write (ctx, player) => Rendered in src/presentation/widgets/. Start with frame(ctx, width, height, 'TITLE'); it returns the drawable box. Width is 840 (full), 420 (half) or 200 (rune); the README writer pairs widgets by width.
  3. Register it in renderPieces in src/presentation/widgets/index.ts. Return [] when the player has not given the data it needs, so layouts can list it without leaving a hole; give it an href if the image should be a link.
  4. Add it to the layouts it belongs in (LAYOUTS and BENTOS in src/presentation/layouts.ts). Clip paths and other ids must be unique per SVG, because the Bento composite puts many widgets in one file: use a counter, as sigil does.
  5. Run npm test. The render test draws it in every theme, mode and motion level and checks that tags balance, that every <use> has its symbol, and that no undefined or NaN leaked into the output.

Adding a layout#

Add an entry to LAYOUTS in src/presentation/layouts.ts: a name, rows of widget ids (half widgets in one row sit side by side), the banner style for "auto", and whether it works with only a username. Add its id to LAYOUT_IDS in src/domain/types.ts, then a row to the layout tables in README.md and docs/guide.md.

The percentile model#

scripts/sample-population.ts draws random GitHub accounts (by id, through /users?since=), keeps the ones with 10 or more contributions in the past year, and writes their six stats to data/population-sample.jsonl (not committed). scripts/fit-population.ts fits each stat as a zero-inflated log-normal and writes src/application/populationData.ts, along with the quantiles of the composite score. Re-run both to refresh the model:

GITHUB_TOKEN=$(gh auth token) npm run sample-population -- 1000
npm run fit-population

The hosted endpoint#

api/index.ts runs handleRequest on Vercel.

RouteDoes
/The website.
/docs, /docs/developmentThese guides, rendered from docs/.
/api?username=…&widget=…One widget as SVG.
/api/cron/refreshRefreshes stored profiles. Needs Authorization: Bearer $CRON_SECRET.
/demo/*The demo images.
/health, /themes, /layoutsJSON.

Caching. A profile is looked up in memory (10 minutes), then in the store, and only then fetched from GitHub. A stored profile is served for up to three days; the nightly job replaces it long before that. Responses also carry CDN-Cache-Control: s-maxage=1800, stale-while-revalidate=86400, so Vercel's CDN answers most views without running the function.

The nightly job. vercel.json schedules /api/cron/refresh at 17:00 UTC (midnight in UTC+7). It refreshes every player viewed in the last 30 days and not refreshed in the last 20 hours, stalest first, three at a time, until 50 seconds have passed, and reports how many remain. If more players are waiting than one run can handle, call the endpoint again (the nightly-refresh workflow in this repository does that in a loop).

Environment variables#

VariableNeededFor
GITHUB_TOKENyesReading GitHub. A classic token with no scopes is enough for public data.
KV_REST_API_URL, KV_REST_API_TOKENrecommendedUpstash Redis, shared by all function instances. Vercel's Upstash integration sets them. UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN work too. Without them the cache lives in each instance's memory and the nightly job has nothing to refresh.
CRON_SECRETfor the nightly jobAny long random string. Vercel sends it to cron routes.

Deploying your own#

  1. Import the repository into Vercel.
  2. Add GITHUB_TOKEN and CRON_SECRET.
  3. In the Vercel dashboard, add the Upstash for Redis integration to the project (the free plan is enough).
  4. Redeploy. /health reports whether the store is shared.

The Action#

action.yml is a composite action: it sets up Node 22, installs this package's runtime dependencies, runs src/cli/generate.ts with tsx, and commits outDir and the README with the github-actions[bot] identity. Commits made with the workflow token do not trigger other workflows, so there is no loop.

To release, tag a version (git tag v2.0.0 && git push --tags) and move a major tag (v2) to it, so users can pin @v2.

Tests#

npm test runs two suites with node:test:

CI runs typecheck and tests on every push and pull request.

Edit this page on GitHub