Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions lib/__tests__/bot-filter.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { isBotAuthor, getIgnoredAuthors, _resetBotFilterCache } from '../bot-filter';

const { mockExistsSync, mockReadFileSync } = vi.hoisted(() => ({
mockExistsSync: vi.fn(),
Expand All @@ -14,11 +15,10 @@ vi.mock('node:fs', () => ({
},
}));

import { isBotAuthor, getIgnoredAuthors } from '../bot-filter';

describe('Bot Filter Utility', () => {
beforeEach(() => {
vi.clearAllMocks();
_resetBotFilterCache();
});

it('detects default bot names and suffixes', () => {
Expand Down
84 changes: 44 additions & 40 deletions lib/bot-filter.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,61 @@
import fs from 'node:fs';
import path from 'node:path';
import { existsSync, readFileSync } from 'node:fs';

const CONFIG_PATH = './commitpulse.config.json';

let cachedIgnoredAuthors: string[] | null = null;

/**
* Resets the cached ignored authors.
* Exported for test environments to clear module state between mock setups.
*/
export function _resetBotFilterCache(): void {
cachedIgnoredAuthors = null;
}

/**
* Reads and returns the list of ignored authors configured in the .commitpulse.json file.
* Returns an empty array if the file does not exist or fails to parse.
* Retrieves the list of ignored authors, reading from config lazily on demand.
*/
export function getIgnoredAuthors(): string[] {
try {
const configPath = path.join(process.cwd(), '.commitpulse.json');
if (fs.existsSync(configPath)) {
const configContent = fs.readFileSync(configPath, 'utf-8');
const config = JSON.parse(configContent);
if (config && Array.isArray(config.ignored_authors)) {
return config.ignored_authors.map((author: string) => author.toLowerCase());
if (cachedIgnoredAuthors === null) {
try {
if (existsSync(CONFIG_PATH)) {
const fileContent = readFileSync(CONFIG_PATH, 'utf-8');
const config = JSON.parse(fileContent);

if (Array.isArray(config.ignored_authors)) {
cachedIgnoredAuthors = config.ignored_authors
.map((author: string) => (typeof author === 'string' ? author.toLowerCase() : ''))
.filter(Boolean);
} else {
cachedIgnoredAuthors = [];
}
} else {
cachedIgnoredAuthors = [];
}
} catch {
cachedIgnoredAuthors = [];
}
} catch {
// Ignore error and fallback to empty array
}
return [];

// Fallback ensures TypeScript receives `string[]` even if null checks are strict
return cachedIgnoredAuthors ?? [];
}

/**
* Checks if the given username is an automated bot or dependency system.
* Matches:
* 1. Custom authors defined in the ignored_authors list inside .commitpulse.json
* 2. Default common bot names (dependabot, renovate, semantic-release-bot)
* 3. Usernames ending with [bot] or -bot
* Checks if a given username belongs to a known bot or an ignored author.
*/
export function isBotAuthor(username: string): boolean {
if (!username) return false;
const lowerUsername = username.toLowerCase();
export function isBotAuthor(author: string | null | undefined): boolean {
if (!author) return false;

// 1. Check custom configuration file
const ignored = getIgnoredAuthors();
if (ignored.includes(lowerUsername)) {
return true;
}
const normalized = author.toLowerCase();

// 2. Automatic regex detection for common bot naming conventions
// e.g. dependabot[bot], renovate[bot]
if (/\[bot\]$/i.test(username)) {
// 1. Default bot patterns
const defaultBots = ['dependabot', 'renovate', 'renovate-bot', 'github-actions[bot]'];
if (defaultBots.includes(normalized) || normalized.endsWith('[bot]')) {
return true;
}

// 3. Usernames ending with -bot or explicitly matching default bot names
if (
lowerUsername.endsWith('-bot') ||
lowerUsername === 'dependabot' ||
lowerUsername === 'renovate'
) {
return true;
}

return false;
// 2. Custom ignored authors from config
const ignored = getIgnoredAuthors();
return ignored.includes(normalized);
}
3 changes: 2 additions & 1 deletion lib/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
GraphLink,
} from '@/types';
import { calculateStreak, aggregateCalendars, convertLocalToUtc } from '@/lib/calculate';
import { isBotAuthor } from './bot-filter';
import { isBotAuthor, getIgnoredAuthors } from './bot-filter';
import { DistributedCache } from '@/lib/cache';
import { LANGUAGE_COLORS } from '@/lib/svg/languageColors';
import { CONTRIBUTION_MILESTONES, STREAK_MILESTONES } from './svg/constants';
Expand Down Expand Up @@ -1607,6 +1607,7 @@ export async function getOrgDashboardData(

let members = membersOrError;
if (options.excludeBots) {
const ignoredAuthors = getIgnoredAuthors();
members = members.filter((member) => !isBotAuthor(member));
}

Expand Down
5 changes: 3 additions & 2 deletions services/github/burnout-analyzer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'server-only';
import { getGitHubTokens } from '@/lib/github';
import { DistributedCache } from '@/lib/cache';
import { isBotAuthor } from '@/lib/bot-filter';
import { isBotAuthor, getIgnoredAuthors } from '@/lib/bot-filter';
import dbConnect from '@/lib/mongodb';
import { User } from '@/models/User';

Expand Down Expand Up @@ -172,8 +172,9 @@
throw new Error('No contribution data found for this repository.');
}

const ignoredAuthors = excludeBots ? getIgnoredAuthors() : [];
const filteredRawData = excludeBots
? rawData.filter((c) => c.author && !isBotAuthor(c.author.login))
? rawData.filter((c) => c.author && !isBotAuthor(c.author.login, ignoredAuthors))

Check failure on line 177 in services/github/burnout-analyzer.ts

View workflow job for this annotation

GitHub Actions / Format · Lint · Typecheck · Test

Expected 1 arguments, but got 2.
: rawData;

const totalCommits = filteredRawData.reduce((acc, c) => acc + (c.total || 0), 0);
Expand Down
Loading