From 1e72ba5676bd9b0bc2d1b308e5403bc22ef76642 Mon Sep 17 00:00:00 2001 From: standujar Date: Thu, 23 Oct 2025 13:15:23 +0200 Subject: [PATCH 1/4] feat: add JSON leaderboard API endpoints Add static JSON API endpoints for leaderboard data that can be consumed by external applications, mobile apps, or third-party integrations. Features: - Generate leaderboard JSON files for monthly, weekly, and lifetime periods - Include user rankings, scores (total, PR, issue, review, comment) - Include wallet addresses (Solana & Ethereum) from GitHub profiles - Calendar-based periods (Sunday start for weekly, 1st of month for monthly) - CLI command: `bun run pipeline export-leaderboard` - Integrated into CI/CD workflow for automatic updates - Comprehensive test suite (9 tests) API endpoints will be available at: - /data/api/leaderboard-monthly.json - /data/api/leaderboard-weekly.json - /data/api/leaderboard-lifetime.json --- .github/workflows/deploy.yml | 6 + cli/analyze-pipeline.ts | 67 ++++ src/__testing__/helpers/mock-data.ts | 17 + .../export/exportLeaderboardAPI.test.ts | 347 ++++++++++++++++++ .../pipelines/export/exportLeaderboardAPI.ts | 258 +++++++++++++ 5 files changed, 695 insertions(+) create mode 100644 src/lib/pipelines/export/exportLeaderboardAPI.test.ts create mode 100644 src/lib/pipelines/export/exportLeaderboardAPI.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f200f958f..f8fa38089 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -65,6 +65,12 @@ jobs: dump_dir: ${{ env.DATA_DIR }}/dump db_path: ${{ env.DATA_DIR }}/db.sqlite + # Export leaderboard API endpoints (monthly, weekly, lifetime) + - name: Export Leaderboard API Endpoints + run: bun run pipeline export-leaderboard --output-dir=${{ env.DATA_DIR }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Copy yesterday's stats for all tracked repositories run: | YESTERDAY=$(date -d "yesterday" +'%Y-%m-%d') diff --git a/cli/analyze-pipeline.ts b/cli/analyze-pipeline.ts index cb8363fe5..c266d9ad5 100755 --- a/cli/analyze-pipeline.ts +++ b/cli/analyze-pipeline.ts @@ -388,4 +388,71 @@ program } }); +program + .command("export-leaderboard") + .description("Generate static JSON leaderboard API endpoints") + .option("-v, --verbose", "Enable verbose logging", false) + .option( + "-c, --config ", + "Path to pipeline config file", + DEFAULT_CONFIG_PATH, + ) + .option("--output-dir ", "Output directory for API files", "./data/") + .option( + "-l, --limit ", + "Limit number of users in leaderboard (0 = no limit)", + "100", + ) + .action(async (options) => { + const logLevel: LogLevel = options.verbose ? "debug" : "info"; + const rootLogger = createLogger({ + minLevel: logLevel, + context: { + pipeline: "export-leaderboard", + }, + }); + + try { + const { exportLeaderboardAPI } = await import( + "@/lib/pipelines/export/exportLeaderboardAPI" + ); + + const limit = parseInt(options.limit, 10); + const exportOptions = { + limit: limit > 0 ? limit : undefined, + logger: rootLogger, + }; + + rootLogger.info(chalk.cyan("\nšŸ“Š Exporting Leaderboard API Endpoints")); + rootLogger.info(chalk.gray(`Output directory: ${options.outputDir}`)); + if (exportOptions.limit) { + rootLogger.info(chalk.gray(`User limit: ${exportOptions.limit}`)); + } + + async function exportAllLeaderboardAPIs( + outputDir: string, + exportOpts: typeof exportOptions, + ) { + const periods: Array<"monthly" | "weekly" | "lifetime"> = [ + "monthly", + "weekly", + "lifetime", + ]; + + for (const period of periods) { + await exportLeaderboardAPI(outputDir, period, exportOpts); + } + } + + await exportAllLeaderboardAPIs(options.outputDir, exportOptions); + + rootLogger.info( + chalk.green("\nāœ… Leaderboard API export completed successfully!"), + ); + } catch (error: unknown) { + console.error(chalk.red("Error exporting leaderboard API:"), error); + process.exit(1); + } + }); + program.parse(process.argv); diff --git a/src/__testing__/helpers/mock-data.ts b/src/__testing__/helpers/mock-data.ts index b9c225634..9679881cb 100644 --- a/src/__testing__/helpers/mock-data.ts +++ b/src/__testing__/helpers/mock-data.ts @@ -9,6 +9,7 @@ import type { issueComments, rawPullRequestFiles, repositories, + walletAddresses, } from "@/lib/data/schema"; import { toDateString } from "@/lib/date-utils"; import { UTCDate } from "@date-fns/utc"; @@ -21,6 +22,7 @@ type IssueComment = InferInsertModel; type PRReview = InferInsertModel; type PRComment = InferInsertModel; type RawCommit = InferInsertModel; +type WalletAddress = InferInsertModel; export function generateMockUsers(items: Partial[]): User[] { return items.map((overrides) => ({ @@ -250,3 +252,18 @@ export function generateMockRepoSummaries( ...overrides, })); } + +export function generateMockWalletAddresses( + items: Partial[], +): WalletAddress[] { + return items.map((overrides) => ({ + id: faker.number.int({ min: 1, max: 100000 }), + userId: overrides.userId ?? faker.internet.username(), + chainId: overrides.chainId ?? "eip155:1", + accountAddress: overrides.accountAddress ?? faker.finance.ethereumAddress(), + label: overrides.label ?? null, + isPrimary: overrides.isPrimary ?? false, + isActive: overrides.isActive ?? true, + ...overrides, + })); +} diff --git a/src/lib/pipelines/export/exportLeaderboardAPI.test.ts b/src/lib/pipelines/export/exportLeaderboardAPI.test.ts new file mode 100644 index 000000000..f55f2c497 --- /dev/null +++ b/src/lib/pipelines/export/exportLeaderboardAPI.test.ts @@ -0,0 +1,347 @@ +import { describe, expect, it, mock, beforeEach } from "bun:test"; +import { setupTestDb } from "@/__testing__/helpers/db"; +import { + generateMockUsers, + generateMockUserDailyScores, + generateMockWalletAddresses, +} from "@/__testing__/helpers/mock-data"; +import * as schema from "@/lib/data/schema"; +import { toDateString } from "@/lib/date-utils"; +import { UTCDate } from "@date-fns/utc"; +import { + exportLeaderboardAPI, + exportAllLeaderboardAPIs, +} from "./exportLeaderboardAPI"; +import { existsSync, readFileSync, rmSync } from "fs"; +import { join } from "path"; + +describe("exportLeaderboardAPI", () => { + let db: ReturnType; + const testOutputDir = "./test-data"; + + beforeEach(() => { + db = setupTestDb(); + mock.module("@/lib/data/db", () => ({ db })); + + // Clean up test output directory + if (existsSync(testOutputDir)) { + rmSync(testOutputDir, { recursive: true, force: true }); + } + }); + + describe("exportLeaderboardAPI - monthly", () => { + it("should export monthly leaderboard with correct structure", async () => { + // Setup test data + const users = generateMockUsers([ + { username: "user1", isBot: 0 }, + { username: "user2", isBot: 0 }, + ]); + await db.insert(schema.users).values(users); + + const today = new UTCDate(); + const startOfMonth = new UTCDate( + today.getFullYear(), + today.getMonth(), + 1, + ); + + const scores = generateMockUserDailyScores( + [ + { username: "user1", score: 100, prScore: 80, issueScore: 20 }, + { username: "user2", score: 50, prScore: 40, issueScore: 10 }, + ], + toDateString(startOfMonth), + ); + await db.insert(schema.userDailyScores).values(scores); + + // Export + await exportLeaderboardAPI(testOutputDir, "monthly"); + + // Verify file exists + const filePath = join(testOutputDir, "api", "leaderboard-monthly.json"); + expect(existsSync(filePath)).toBe(true); + + // Read and verify content + const content = JSON.parse(readFileSync(filePath, "utf-8")); + expect(content.period).toBe("monthly"); + expect(content.totalUsers).toBe(2); + expect(content.leaderboard).toHaveLength(2); + + // Verify top user + expect(content.leaderboard[0].rank).toBe(1); + expect(content.leaderboard[0].username).toBe("user1"); + expect(content.leaderboard[0].score).toBe(100); + expect(content.leaderboard[0].prScore).toBe(80); + + // Verify second user + expect(content.leaderboard[1].rank).toBe(2); + expect(content.leaderboard[1].username).toBe("user2"); + expect(content.leaderboard[1].score).toBe(50); + }); + + it("should include wallet addresses when available", async () => { + const users = generateMockUsers([{ username: "user1", isBot: 0 }]); + await db.insert(schema.users).values(users); + + const wallets = generateMockWalletAddresses([ + { + userId: "user1", + chainId: "eip155:1", + accountAddress: "0x123...", + isPrimary: true, + }, + { + userId: "user1", + chainId: "mainnet-beta", + accountAddress: "abc123...", + isPrimary: true, + }, + ]); + await db.insert(schema.walletAddresses).values(wallets); + + const today = new UTCDate(); + const startOfMonth = new UTCDate( + today.getFullYear(), + today.getMonth(), + 1, + ); + const scores = generateMockUserDailyScores( + [{ username: "user1", score: 100 }], + toDateString(startOfMonth), + ); + await db.insert(schema.userDailyScores).values(scores); + + await exportLeaderboardAPI(testOutputDir, "monthly"); + + const filePath = join(testOutputDir, "api", "leaderboard-monthly.json"); + const content = JSON.parse(readFileSync(filePath, "utf-8")); + + expect(content.leaderboard[0].wallets).toBeDefined(); + expect(content.leaderboard[0].wallets.ethereum).toBe("0x123..."); + expect(content.leaderboard[0].wallets.solana).toBe("abc123..."); + }); + + it("should return empty wallets object when no wallets are linked", async () => { + const users = generateMockUsers([{ username: "user1", isBot: 0 }]); + await db.insert(schema.users).values(users); + + const today = new UTCDate(); + const startOfMonth = new UTCDate( + today.getFullYear(), + today.getMonth(), + 1, + ); + const scores = generateMockUserDailyScores( + [{ username: "user1", score: 100 }], + toDateString(startOfMonth), + ); + await db.insert(schema.userDailyScores).values(scores); + + await exportLeaderboardAPI(testOutputDir, "monthly"); + + const filePath = join(testOutputDir, "api", "leaderboard-monthly.json"); + const content = JSON.parse(readFileSync(filePath, "utf-8")); + + expect(content.leaderboard[0].wallets).toEqual({}); + }); + + it("should exclude bot users from leaderboard", async () => { + const users = generateMockUsers([ + { username: "user1", isBot: 0 }, + { username: "bot-user", isBot: 1 }, + ]); + await db.insert(schema.users).values(users); + + const today = new UTCDate(); + const startOfMonth = new UTCDate( + today.getFullYear(), + today.getMonth(), + 1, + ); + const scores = generateMockUserDailyScores( + [ + { username: "user1", score: 100 }, + { username: "bot-user", score: 200 }, + ], + toDateString(startOfMonth), + ); + await db.insert(schema.userDailyScores).values(scores); + + await exportLeaderboardAPI(testOutputDir, "monthly"); + + const filePath = join(testOutputDir, "api", "leaderboard-monthly.json"); + const content = JSON.parse(readFileSync(filePath, "utf-8")); + + expect(content.totalUsers).toBe(1); + expect(content.leaderboard[0].username).toBe("user1"); + }); + + it("should respect limit parameter", async () => { + const users = generateMockUsers([ + { username: "user1", isBot: 0 }, + { username: "user2", isBot: 0 }, + { username: "user3", isBot: 0 }, + ]); + await db.insert(schema.users).values(users); + + const today = new UTCDate(); + const startOfMonth = new UTCDate( + today.getFullYear(), + today.getMonth(), + 1, + ); + const scores = generateMockUserDailyScores( + [ + { username: "user1", score: 100 }, + { username: "user2", score: 80 }, + { username: "user3", score: 60 }, + ], + toDateString(startOfMonth), + ); + await db.insert(schema.userDailyScores).values(scores); + + await exportLeaderboardAPI(testOutputDir, "monthly", { limit: 2 }); + + const filePath = join(testOutputDir, "api", "leaderboard-monthly.json"); + const content = JSON.parse(readFileSync(filePath, "utf-8")); + + expect(content.totalUsers).toBe(2); + expect(content.leaderboard).toHaveLength(2); + }); + }); + + describe("exportLeaderboardAPI - weekly", () => { + it("should export weekly leaderboard starting from Sunday", async () => { + const users = generateMockUsers([{ username: "user1", isBot: 0 }]); + await db.insert(schema.users).values(users); + + const today = new UTCDate(); + const day = today.getDay(); + const startOfWeek = new UTCDate(today); + startOfWeek.setDate(today.getDate() - day); + + const scores = generateMockUserDailyScores( + [{ username: "user1", score: 100 }], + toDateString(startOfWeek), + ); + await db.insert(schema.userDailyScores).values(scores); + + await exportLeaderboardAPI(testOutputDir, "weekly"); + + const filePath = join(testOutputDir, "api", "leaderboard-weekly.json"); + const content = JSON.parse(readFileSync(filePath, "utf-8")); + + expect(content.period).toBe("weekly"); + expect(content.startDate).toBe(toDateString(startOfWeek)); + expect(content.leaderboard[0].username).toBe("user1"); + }); + }); + + describe("exportLeaderboardAPI - lifetime", () => { + it("should export lifetime leaderboard from 2024-10-15", async () => { + const users = generateMockUsers([{ username: "user1", isBot: 0 }]); + await db.insert(schema.users).values(users); + + const scores = generateMockUserDailyScores( + [{ username: "user1", score: 100 }], + "2024-10-16", + ); + await db.insert(schema.userDailyScores).values(scores); + + await exportLeaderboardAPI(testOutputDir, "lifetime"); + + const filePath = join(testOutputDir, "api", "leaderboard-lifetime.json"); + const content = JSON.parse(readFileSync(filePath, "utf-8")); + + expect(content.period).toBe("lifetime"); + expect(content.startDate).toBe("2024-10-15"); + expect(content.leaderboard[0].username).toBe("user1"); + }); + }); + + describe("exportAllLeaderboardAPIs", () => { + it("should export all three leaderboard files", async () => { + const users = generateMockUsers([{ username: "user1", isBot: 0 }]); + await db.insert(schema.users).values(users); + + const today = new UTCDate(); + const startOfMonth = new UTCDate( + today.getFullYear(), + today.getMonth(), + 1, + ); + const scores = generateMockUserDailyScores( + [{ username: "user1", score: 100 }], + toDateString(startOfMonth), + ); + await db.insert(schema.userDailyScores).values(scores); + + await exportAllLeaderboardAPIs(testOutputDir); + + // Verify all three files exist + expect( + existsSync(join(testOutputDir, "api", "leaderboard-monthly.json")), + ).toBe(true); + expect( + existsSync(join(testOutputDir, "api", "leaderboard-weekly.json")), + ).toBe(true); + expect( + existsSync(join(testOutputDir, "api", "leaderboard-lifetime.json")), + ).toBe(true); + }); + }); + + describe("Date range calculations", () => { + it("should have correct date ranges for each period", async () => { + const users = generateMockUsers([{ username: "user1", isBot: 0 }]); + await db.insert(schema.users).values(users); + + const today = new UTCDate(); + const scores = generateMockUserDailyScores( + [{ username: "user1", score: 100 }], + toDateString(today), + ); + await db.insert(schema.userDailyScores).values(scores); + + await exportAllLeaderboardAPIs(testOutputDir); + + // Check monthly + const monthly = JSON.parse( + readFileSync( + join(testOutputDir, "api", "leaderboard-monthly.json"), + "utf-8", + ), + ); + const startOfMonth = new UTCDate( + today.getFullYear(), + today.getMonth(), + 1, + ); + expect(monthly.startDate).toBe(toDateString(startOfMonth)); + expect(monthly.endDate).toBe(toDateString(today)); + + // Check weekly + const weekly = JSON.parse( + readFileSync( + join(testOutputDir, "api", "leaderboard-weekly.json"), + "utf-8", + ), + ); + const day = today.getDay(); + const startOfWeek = new UTCDate(today); + startOfWeek.setDate(today.getDate() - day); + expect(weekly.startDate).toBe(toDateString(startOfWeek)); + expect(weekly.endDate).toBe(toDateString(today)); + + // Check lifetime + const lifetime = JSON.parse( + readFileSync( + join(testOutputDir, "api", "leaderboard-lifetime.json"), + "utf-8", + ), + ); + expect(lifetime.startDate).toBe("2024-10-15"); + expect(lifetime.endDate).toBe(toDateString(today)); + }); + }); +}); diff --git a/src/lib/pipelines/export/exportLeaderboardAPI.ts b/src/lib/pipelines/export/exportLeaderboardAPI.ts new file mode 100644 index 000000000..921897d81 --- /dev/null +++ b/src/lib/pipelines/export/exportLeaderboardAPI.ts @@ -0,0 +1,258 @@ +import { db } from "@/lib/data/db"; +import { users, userDailyScores, walletAddresses } from "@/lib/data/schema"; +import { eq, and, desc, sql } from "drizzle-orm"; +import { toDateString } from "@/lib/date-utils"; +import { writeToFile } from "@/lib/fsHelpers"; +import { join } from "path"; +import { mkdirSync } from "fs"; +import { generateScoreSelectFields } from "@/lib/scoring/queries"; +import { buildCommonWhereConditions } from "@/lib/pipelines/queryHelpers"; +import { UTCDate } from "@date-fns/utc"; +import { Logger } from "@/lib/logger"; + +/** + * Leaderboard entry structure for API responses + */ +export interface LeaderboardEntry { + rank: number; + username: string; + avatarUrl: string; + score: number; + prScore: number; + issueScore: number; + reviewScore: number; + commentScore: number; + wallets: { + solana?: string; + ethereum?: string; + }; +} + +/** + * Leaderboard API response structure + */ +export interface LeaderboardAPIResponse { + period: "monthly" | "weekly" | "lifetime"; + startDate: string; + endDate: string; + generatedAt: string; + totalUsers: number; + leaderboard: LeaderboardEntry[]; +} + +/** + * Get wallet addresses for users and format them by chain + */ +async function getUserWallets( + usernames: string[], +): Promise> { + if (usernames.length === 0) { + return new Map(); + } + + const wallets = await db + .select({ + userId: walletAddresses.userId, + chainId: walletAddresses.chainId, + accountAddress: walletAddresses.accountAddress, + isPrimary: walletAddresses.isPrimary, + }) + .from(walletAddresses) + .where( + and( + sql`${walletAddresses.userId} IN (${sql.join( + usernames.map((u) => sql`${u}`), + sql`, `, + )})`, + eq(walletAddresses.isActive, true), + ), + ) + .all(); + + const walletMap = new Map(); + + for (const wallet of wallets) { + const userId = wallet.userId; + const existing = walletMap.get(userId) || {}; + + // Map chain IDs to readable names + // Ethereum uses CAIP-2 format: eip155:1 + // Solana uses various chain IDs + if (wallet.chainId.startsWith("eip155:")) { + // Ethereum - prefer primary wallet if multiple exist + if (!existing.ethereum || wallet.isPrimary) { + existing.ethereum = wallet.accountAddress; + } + } else if ( + wallet.chainId.includes("solana") || + wallet.chainId === "mainnet-beta" + ) { + // Solana - prefer primary wallet if multiple exist + if (!existing.solana || wallet.isPrimary) { + existing.solana = wallet.accountAddress; + } + } + + walletMap.set(userId, existing); + } + + return walletMap; +} + +/** + * Get leaderboard data for a specific time period + */ +async function getLeaderboardData( + startDate?: string, + endDate?: string, + limit?: number, +): Promise { + // Build conditions + const conditions = [ + eq(users.isBot, 0), // Exclude bots + eq(userDailyScores.category, "day"), + ...buildCommonWhereConditions( + { dateRange: { startDate, endDate } }, + userDailyScores, + ["date"], + ), + ]; + + // Generate score fields + const scoreFields = generateScoreSelectFields(userDailyScores); + + // Query top users by score + const baseQuery = db + .select({ + username: userDailyScores.username, + avatarUrl: users.avatarUrl, + ...scoreFields, + }) + .from(userDailyScores) + .innerJoin(users, eq(userDailyScores.username, users.username)) + .where(and(...conditions)) + .groupBy(userDailyScores.username) + .orderBy(desc(scoreFields.totalScore)); + + // Apply limit if specified + const results = limit + ? await baseQuery.limit(limit).all() + : await baseQuery.all(); + + // Get wallet addresses for all users + const usernames = results.map((r) => r.username); + const walletMap = await getUserWallets(usernames); + + // Format results with ranks and wallets + return results.map((row, index) => ({ + rank: index + 1, + username: row.username, + avatarUrl: row.avatarUrl || "", + score: Number(row.totalScore || 0), + prScore: Number(row.prScore || 0), + issueScore: Number(row.issueScore || 0), + reviewScore: Number(row.reviewScore || 0), + commentScore: Number(row.commentScore || 0), + wallets: walletMap.get(row.username) || {}, + })); +} + +/** + * Calculate date ranges for different periods + * Uses the same logic as getDateRangeForPeriod in queryHelpers.ts + */ +function calculateDateRange(period: "monthly" | "weekly" | "lifetime"): { + startDate: string; + endDate: string; +} { + const now = new UTCDate(); + const endDate = toDateString(now); + + switch (period) { + case "monthly": { + // Start of current month (same as leaderboard frontend) + const startOfMonth = new UTCDate(now.getFullYear(), now.getMonth(), 1); + return { startDate: toDateString(startOfMonth), endDate }; + } + case "weekly": { + // Start of current week - Sunday as first day (same as leaderboard frontend) + const day = now.getDay(); // 0 for Sunday, 1 for Monday, etc. + const startOfWeek = new UTCDate(now); + startOfWeek.setDate(now.getDate() - day); + return { startDate: toDateString(startOfWeek), endDate }; + } + case "lifetime": { + // From contribution start date (hardcoded in config as 2024-10-15) + return { startDate: "2024-10-15", endDate }; + } + } +} + +/** + * Export leaderboard data as JSON API endpoint + */ +export async function exportLeaderboardAPI( + outputDir: string, + period: "monthly" | "weekly" | "lifetime", + options?: { + limit?: number; + logger?: Logger; + }, +): Promise { + const logger = options?.logger; + const limit = options?.limit; + + logger?.info(`Generating ${period} leaderboard API endpoint...`); + + // Calculate date range + const { startDate, endDate } = calculateDateRange(period); + + // Get leaderboard data + const leaderboard = await getLeaderboardData(startDate, endDate, limit); + + // Build response + const response: LeaderboardAPIResponse = { + period, + startDate, + endDate, + generatedAt: new Date().toISOString(), + totalUsers: leaderboard.length, + leaderboard, + }; + + // Create API directory if it doesn't exist + const apiDir = join(outputDir, "api"); + mkdirSync(apiDir, { recursive: true }); + + // Write JSON file + const filename = `leaderboard-${period}.json`; + const outputPath = join(apiDir, filename); + await writeToFile(outputPath, JSON.stringify(response, null, 2)); + + logger?.info(`āœ“ Exported ${period} leaderboard to ${outputPath}`, { + totalUsers: leaderboard.length, + topUser: leaderboard[0]?.username, + topScore: leaderboard[0]?.score, + }); +} + +/** + * Export all leaderboard API endpoints + */ +export async function exportAllLeaderboardAPIs( + outputDir: string, + options?: { + limit?: number; + logger?: Logger; + }, +): Promise { + const logger = options?.logger; + logger?.info("Exporting all leaderboard API endpoints..."); + + // Export all three periods + await exportLeaderboardAPI(outputDir, "monthly", options); + await exportLeaderboardAPI(outputDir, "weekly", options); + await exportLeaderboardAPI(outputDir, "lifetime", options); + + logger?.info("āœ“ All leaderboard API endpoints exported successfully"); +} From a53cbf1e7c30984f56848ec7c7dae6c6828effd5 Mon Sep 17 00:00:00 2001 From: standujar Date: Thu, 23 Oct 2025 13:29:45 +0200 Subject: [PATCH 2/4] fix: only require OPENROUTER_API_KEY for AI summary commands --- cli/analyze-pipeline.ts | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/cli/analyze-pipeline.ts b/cli/analyze-pipeline.ts index c266d9ad5..ee7527a61 100755 --- a/cli/analyze-pipeline.ts +++ b/cli/analyze-pipeline.ts @@ -15,17 +15,15 @@ import { calculateDateRange } from "@/lib/date-utils"; // Load environment variables from .env file loadEnv(); -// Validate required environment variables -const requiredEnvVars = ["GITHUB_TOKEN", "OPENROUTER_API_KEY"]; -const missingEnvVars = requiredEnvVars.filter((envVar) => !process.env[envVar]); - -if (missingEnvVars.length > 0) { - console.error( - `Error: Missing required environment variables: ${missingEnvVars.join( - ", ", - )}`, - ); - process.exit(1); +// Helper to validate environment variables +function validateEnvVars(requiredVars: string[]) { + const missingVars = requiredVars.filter((envVar) => !process.env[envVar]); + if (missingVars.length > 0) { + console.error( + `Error: Missing required environment variables: ${missingVars.join(", ")}`, + ); + process.exit(1); + } } import { Command } from "@commander-js/extra-typings"; @@ -76,6 +74,9 @@ program false, ) .action(async (options) => { + // Validate required environment variables for ingestion + validateEnvVars(["GITHUB_TOKEN"]); + try { // Dynamically import the config const configPath = join(import.meta.dir, options.config); @@ -140,6 +141,9 @@ program false, ) .action(async (options) => { + // Validate required environment variables for processing + validateEnvVars(["GITHUB_TOKEN"]); + try { // Dynamically import the config const configPath = join(import.meta.dir, options.config); @@ -200,6 +204,9 @@ program .option("-d, --days ", "Number of days to look back from before date") .option("--all", "Process all data since contributionStartDate", false) .action(async (options) => { + // Validate required environment variables for export + validateEnvVars(["GITHUB_TOKEN"]); + try { // Dynamically import the config const configPath = join(import.meta.dir, options.config); @@ -288,6 +295,8 @@ program .option("--weekly", "Generate weekly summaries") .option("--monthly", "Generate monthly summaries") .action(async (options) => { + // Validate required environment variables for AI summaries + validateEnvVars(["GITHUB_TOKEN", "OPENROUTER_API_KEY"]); try { // Dynamically import the config const configPath = join(import.meta.dir, options.config); From 0e270f9116d6104e76927ea8447bce813ccd18dd Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 23 Oct 2025 13:30:53 +0200 Subject: [PATCH 3/4] Update cli/analyze-pipeline.ts --- cli/analyze-pipeline.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/analyze-pipeline.ts b/cli/analyze-pipeline.ts index ee7527a61..a98f1c868 100755 --- a/cli/analyze-pipeline.ts +++ b/cli/analyze-pipeline.ts @@ -456,7 +456,7 @@ program await exportAllLeaderboardAPIs(options.outputDir, exportOptions); rootLogger.info( - chalk.green("\nāœ… Leaderboard API export completed successfully!"), + chalk.green("\n Leaderboard API export completed successfully!"), ); } catch (error: unknown) { console.error(chalk.red("Error exporting leaderboard API:"), error); From 0ba46de0859872a8278ca101890adc683258fa94 Mon Sep 17 00:00:00 2001 From: Stan Date: Thu, 23 Oct 2025 13:30:57 +0200 Subject: [PATCH 4/4] Update cli/analyze-pipeline.ts --- cli/analyze-pipeline.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/analyze-pipeline.ts b/cli/analyze-pipeline.ts index a98f1c868..5c384114f 100755 --- a/cli/analyze-pipeline.ts +++ b/cli/analyze-pipeline.ts @@ -432,7 +432,7 @@ program logger: rootLogger, }; - rootLogger.info(chalk.cyan("\nšŸ“Š Exporting Leaderboard API Endpoints")); + rootLogger.info(chalk.cyan("\n Exporting Leaderboard API Endpoints")); rootLogger.info(chalk.gray(`Output directory: ${options.outputDir}`)); if (exportOptions.limit) { rootLogger.info(chalk.gray(`User limit: ${exportOptions.limit}`));