Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
98 changes: 87 additions & 11 deletions cli/analyze-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -200,6 +204,9 @@ program
.option("-d, --days <number>", "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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -388,4 +397,71 @@ program
}
});

program
.command("export-leaderboard")
.description("Generate static JSON leaderboard API endpoints")
.option("-v, --verbose", "Enable verbose logging", false)
.option(
"-c, --config <path>",
"Path to pipeline config file",
DEFAULT_CONFIG_PATH,
)
.option("--output-dir <dir>", "Output directory for API files", "./data/")
.option(
"-l, --limit <number>",
"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);
17 changes: 17 additions & 0 deletions src/__testing__/helpers/mock-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -21,6 +22,7 @@ type IssueComment = InferInsertModel<typeof issueComments>;
type PRReview = InferInsertModel<typeof schema.prReviews>;
type PRComment = InferInsertModel<typeof schema.prComments>;
type RawCommit = InferInsertModel<typeof schema.rawCommits>;
type WalletAddress = InferInsertModel<typeof walletAddresses>;

export function generateMockUsers(items: Partial<User>[]): User[] {
return items.map((overrides) => ({
Expand Down Expand Up @@ -250,3 +252,18 @@ export function generateMockRepoSummaries(
...overrides,
}));
}

export function generateMockWalletAddresses(
items: Partial<WalletAddress>[],
): 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,
}));
}
Loading