feat: add author achievements system#169
Conversation
Compute and persist GitHub/content-stat-based achievements automatically via a new grant-author-achievements worker task, triggered whenever an author's posts, collections, or profile metadata are synced. Manual achievements continue to come from author meta and are stored alongside the automatic ones in a new profile_achievements table. Exposes author achievement data through a new GET authors route.
|
Warning Review limit reached
More reviews will be available in 35 minutes and 5 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more credits in the billing tab to continue. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate. For paid Pro and Pro+ PR reviews, CodeRabbit uses rolling per-developer review limits. Reviews become available again as older review attempts age out of the rolling limit window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughImplements end-to-end author achievements: adds a ChangesAuthor Achievements System
Sequence Diagram(s)sequenceDiagram
participant SyncProc as sync-author/post/collection
participant BullMQ
participant GrantProc as grant-author-achievements processor
participant DB
participant GitHubAPI as getAuthorGitHubStats
SyncProc->>DB: upsert profile / post / collection
SyncProc->>DB: upsert manual profileAchievements (sync-author only)
SyncProc->>BullMQ: enqueue GRANT_AUTHOR_ACHIEVEMENTS { profileSlug, ref }
BullMQ->>GrantProc: dequeue job
GrantProc->>DB: fetch profile + word-count/post/collection stats
GrantProc->>GitHubAPI: getAuthorGitHubStats(githubLogin)
GitHubAPI-->>GrantProc: { issueCount, pullRequestCount, commitsInYear } | undefined
GrantProc->>GrantProc: evaluate ACHIEVEMENT_RULES → earnedIds
GrantProc->>DB: tx: DELETE WHERE achievementId IN ALL_POSSIBLE_AUTO_IDS
GrantProc->>DB: tx: INSERT earned profileAchievements rows
note over DB,GrantProc: Manual achievement rows untouched
participant Client
participant AuthorsAPI as GET /content/authors/:slug
Client->>AuthorsAPI: GET /content/authors/:slug
AuthorsAPI->>DB: fetch profile with profileAchievements
AuthorsAPI->>DB: (if words-words-words earned) aggregate English word count
AuthorsAPI-->>Client: AuthorResponse { achievements: [...] }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
apps/api/src/routes/content/authors.ts (1)
164-175: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDeclare the 404 response schema for this endpoint.
The handler returns a 404 body at Line 187, but the route schema currently only documents 200.
💡 Suggested update
+const ErrorResponseSchema = Type.Object({ + error: Type.String(), +}); + fastify.get<{ Params: Static<typeof AuthorParamsSchema>; Reply: AuthorResponse | { error: string }; }>( "/content/authors/:slug", { schema: { description: "Fetch an author profile with their earned achievements", params: AuthorParamsSchema, response: { description: "Successful", content: { "application/json": { schema: AuthorResponseSchema }, }, }, + 404: { + description: "Author not found", + content: { + "application/json": { schema: ErrorResponseSchema }, + }, + }, }, }, },Also applies to: 186-188
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/routes/content/authors.ts` around lines 164 - 175, The route schema for the author endpoint in the schema object (with description "Fetch an author profile with their earned achievements") only documents a 200 response, but the handler returns a 404 status code. Add a 404 response schema declaration to the response object following the same structure as the existing 200 response, documenting the error response body that is returned when the author is not found.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/worker/src/tasks/grant-author-achievements/processor.ts`:
- Around line 89-96: The error handling in the github.getAuthorGitHubStats call
is suppressing transient failures and returning undefined, which causes the
achievement deletion and re-insertion logic to proceed without GitHub data,
potentially dropping previously earned achievements. Remove the catch handler
that logs a warning and returns undefined for the github.getAuthorGitHubStats
method call around line 89-95, and allow the error to propagate instead so the
operation fails completely rather than proceeding with incomplete data. Apply
the same fix to the similar error handling pattern mentioned at lines 116-134.
In `@apps/worker/src/tasks/sync-author/processor.ts`:
- Around line 97-100: The earnedManualIds variable created by filtering
authorData.achievements may contain duplicate values if the source array has
duplicates, which causes duplicate (profileSlug, achievementId) rows to be
inserted and violates the composite primary key constraint in the transaction
around lines 120-126. Deduplicate the earnedManualIds array before it is used in
the insert operation by converting it to a Set to remove duplicates and then
back to an array to ensure each achievement ID is only inserted once per
profile.
In `@apps/worker/src/tasks/sync-post/processor.ts`:
- Around line 185-195: The achievement recomputation loop only queues jobs for
authors currently associated with the post (authorSlugs), but fails to handle
authors that were removed when postAuthors was refreshed earlier in the
function. To fix this, capture the list of authors before the postAuthors
refresh occurs (around Line 176), then after the refresh completes, identify
which authors were removed by comparing the old and new author lists. Modify the
loop that calls createJob with Tasks.GRANT_AUTHOR_ACHIEVEMENTS to iterate over
both the current authorSlugs and the removed authors so that achievements are
properly recomputed for all affected authors including those no longer
associated with the post.
In `@packages/github-api/src/getAuthorGitHubStats.ts`:
- Around line 65-67: The user query in the client.graphql call directly
interpolates the githubLogin variable into the query string, which creates a
query injection vulnerability. Replace the interpolated githubLogin in the query
string with a GraphQL variable (e.g., $login), update the query to accept this
variable as a parameter, and pass the githubLogin value as part of the second
argument to the client.graphql call following the parameterized query pattern
used elsewhere in the file.
---
Nitpick comments:
In `@apps/api/src/routes/content/authors.ts`:
- Around line 164-175: The route schema for the author endpoint in the schema
object (with description "Fetch an author profile with their earned
achievements") only documents a 200 response, but the handler returns a 404
status code. Add a 404 response schema declaration to the response object
following the same structure as the existing 200 response, documenting the error
response body that is returned when the author is not found.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 93df9faa-9596-4f42-a540-3a67be645532
📒 Files selected for processing (20)
.env.exampleapps/api/src/createApp.tsapps/api/src/routes/content/authors.tsapps/worker/src/index.tsapps/worker/src/tasks/grant-author-achievements/achievement-ids.tsapps/worker/src/tasks/grant-author-achievements/processor.tsapps/worker/src/tasks/sync-author/processor.tsapps/worker/src/tasks/sync-author/types.tsapps/worker/src/tasks/sync-collection/processor.tsapps/worker/src/tasks/sync-post/processor.tsapps/worker/test-utils/setup.tspackages/bullmq/src/tasks/grant-author-achievements.tspackages/bullmq/src/tasks/index.tspackages/bullmq/src/tasks/types.tspackages/db/drizzle/20260620230617_thankful_angel/migration.sqlpackages/db/drizzle/20260620230617_thankful_angel/snapshot.jsonpackages/db/src/relations.tspackages/db/src/schema/profiles.tspackages/github-api/src/getAuthorGitHubStats.tspackages/github-api/src/index.ts
- Propagate GitHub stats errors instead of silently returning undefined - Deduplicate manual achievement IDs before insert - Recompute achievements for removed post authors - Use GraphQL variable for user lookup query (prevents injection) - Document 404 response schema on authors route Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/worker/src/tasks/sync-post/processor.ts (1)
37-48: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winRecompute achievements when a post is deleted (404 path).
When the post returns 404 it's deleted (Line 43) and its
post_authorsrows cascade away, but noGRANT_AUTHOR_ACHIEVEMENTSjobs are enqueued. The former authors lose a post yet keep stale post-count/word-count achievements — the same staleness class the rest of this PR fixes for in-place refreshes. Capture the authors before deleting and enqueue jobs for them.💡 Suggested fix
if (folderResponse.data === undefined) { if (folderResponse.status === 404) { console.log( `Post ${post} (${basePath}) returned 404 - removing from database.`, ); + const removedAuthorRows = await db + .select({ authorSlug: postAuthors.authorSlug }) + .from(postAuthors) + .where(eq(postAuthors.postSlug, post)); + await db.delete(posts).where(eq(posts.slug, post)); + for (const { authorSlug } of removedAuthorRows) { + await createJob( + Tasks.GRANT_AUTHOR_ACHIEVEMENTS, + `grant-author-achievements:${authorSlug}`, + { profileSlug: authorSlug, ref }, + ); + } + return; } throw new Error(`Failed to fetch post folder: ${basePath}`); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/worker/src/tasks/sync-post/processor.ts` around lines 37 - 48, In the 404 handling block where the post is deleted (the section with the console.log and db.delete call), you need to capture the authors before deleting the post and then enqueue achievement recomputation jobs for them. Before executing the db.delete statement that removes the post from the posts table, query the database to retrieve all authors associated with the post from the post_authors table, store those author IDs, then proceed with the deletion. After the deletion completes, iterate through the captured author IDs and enqueue a GRANT_AUTHOR_ACHIEVEMENTS job for each author to refresh their stale achievement counts (similar to how in-place refreshes are handled elsewhere in this PR).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@apps/worker/src/tasks/sync-post/processor.ts`:
- Around line 37-48: In the 404 handling block where the post is deleted (the
section with the console.log and db.delete call), you need to capture the
authors before deleting the post and then enqueue achievement recomputation jobs
for them. Before executing the db.delete statement that removes the post from
the posts table, query the database to retrieve all authors associated with the
post from the post_authors table, store those author IDs, then proceed with the
deletion. After the deletion completes, iterate through the captured author IDs
and enqueue a GRANT_AUTHOR_ACHIEVEMENTS job for each author to refresh their
stale achievement counts (similar to how in-place refreshes are handled
elsewhere in this PR).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 65c50a99-d35b-47a9-b269-05a03f247d25
📒 Files selected for processing (6)
apps/api/src/routes/content/authors.tsapps/worker/src/tasks/grant-author-achievements/processor.tsapps/worker/src/tasks/sync-author/processor.tsapps/worker/src/tasks/sync-post/processor.test.tsapps/worker/src/tasks/sync-post/processor.tspackages/github-api/src/getAuthorGitHubStats.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- apps/worker/src/tasks/grant-author-achievements/processor.ts
- packages/github-api/src/getAuthorGitHubStats.ts
- apps/api/src/routes/content/authors.ts
- apps/worker/src/tasks/sync-author/processor.ts
CodeRabbit follow-up on PR playfulprogramming#169: the 404 path in sync-post deleted the post but never re-evaluated achievements for its authors, leaving stale post-count/word-count achievements behind. Capture the post's authors before the delete and enqueue grant-author-achievements for each, matching the removed-authors handling already in the normal sync path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
crutchcorn
left a comment
There was a problem hiding this comment.
Super minor things we should change before merging
|
|
||
| type AchievementDisplay = { name: string; body: string }; | ||
|
|
||
| const FIXED_ACHIEVEMENT_DISPLAY: Record<string, AchievementDisplay> = { |
There was a problem hiding this comment.
I actually think I'd rather us just send the raw achievements to the client and map them to the language there.
That way, we can handle translations in the frontend and keep our backend generic with what language is displayed in the UI (other than blog post language)
| // Only fetch total word count if the author has the words-words-words | ||
| // achievement — avoids an unnecessary join for everyone else. | ||
| const hasWordsAchievement = profile.achievements.some( | ||
| (a) => a.achievementId === "words-words-words", | ||
| ); |
There was a problem hiding this comment.
I think this makes a lot of sense as a defensive programming move, but I think I'd personally just have us eat the DB lookup cost for API return shape consistency. Shouldn't be too bad
| const FIRST_CONTRIBUTOR_YEAR = 2019; | ||
|
|
||
| function contributorYears(): number[] { | ||
| const years: number[] = []; | ||
| for (let y = FIRST_CONTRIBUTOR_YEAR; y <= new Date().getFullYear(); y++) { | ||
| years.push(y); | ||
| } | ||
| return years; | ||
| } |
There was a problem hiding this comment.
Not a huge deal, but this code threw me off cuz it was:
- Duplicated
- Made me think we were giving everyone achievements for every year
Might be worth making sure we're not duping loops and probably leave a comment
Summary
Adds a system for tracking and surfacing author achievements:
profile_achievementstable (drizzle migration20260620230617_thankful_angel) storing manual and auto-computed achievements per author.grant-author-achievementsworker task that evaluates GitHub/content-stat-based achievement rules (post count, word count, co-authorship, collection count, GitHub stats) and writes the earned set, leaving manually-granted achievements untouched.sync-author,sync-collection, andsync-postprocessors now enqueue agrant-author-achievementsjob (deduped by author slug) whenever an author's profile, collection, or post is synced.achievementsfield) and are persisted in the same table.getAuthorGitHubStatshelper in@playfulprogramming/github-apifor fetching GitHub-derived stats (requiresGITHUB_TOKEN, documented in.env.example).GETauthors route exposing achievement data (with a fixed display map for names/descriptions) for the frontend.Closes #97
Reviewer notes
apps/worker/src/createWorker.tsalso has unrelated changes in my working tree (a Windows ESM-compatibility fix forimport.meta.resolve, plus astalledevent handler) — intentionally excluded from this PR and will land separately.GITHUB_TOKEN(optional, only needed for GitHub-based achievement calculation) — documented in.env.example.Test plan
grant-author-achievementsjob is enqueued and achievements are written toprofile_achievementsSummary by CodeRabbit