Skip to content
Merged
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
53 changes: 44 additions & 9 deletions .github/workflows/bot-detection.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,27 @@ jobs:
const highRiskAccounts = new Map();
const commentsByUser = new Map();
const userCreatedDates = new Map();
const accountsSeen = new Set();
let userLookupFailures = 0;

async function ensureUserCreatedDate(login) {
if (!login) return;
accountsSeen.add(login);
if (userCreatedDates.has(login)) return;
try {
const { data: userInfo } = await github.rest.users.getByUsername({ username: login });
userCreatedDates.set(login, new Date(userInfo.created_at));
} catch (e) {
userLookupFailures += 1;
userCreatedDates.set(login, null);
}
}

for (const pr of prs.slice(0, MAX_PR)) {
if (new Date(pr.updated_at) < cutoff) continue;

await ensureUserCreatedDate(pr.user?.login);

const issueComments = [];
if (github.paginate?.iterator) {
for await (const response of github.paginate.iterator(github.rest.issues.listComments, {
Expand Down Expand Up @@ -109,14 +126,7 @@ jobs:
const login = comment.user?.login;
if (!login) continue;

if (!userCreatedDates.has(login)) {
try {
const { data: userInfo } = await github.rest.users.getByUsername({ username: login });
userCreatedDates.set(login, new Date(userInfo.created_at));
} catch (e) {
userCreatedDates.set(login, null);
}
}
await ensureUserCreatedDate(login);

if (!commentsByUser.has(login)) {
commentsByUser.set(login, []);
Expand All @@ -129,6 +139,24 @@ jobs:
}
}

// Also consider recent issue creators (not just PR comments).
// This helps catch new accounts that only opened issues/PRs (even if already closed).
try {
const { data: recentIssues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'all',
since: cutoff.toISOString(),
per_page: 100,
});
Comment on lines +145 to +151
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new issue listing code doesn't use pagination like the PR listing does (lines 36-58). With per_page: 100, this will only check the first 100 items. In active repositories with more than 100 issues/PRs in the last 6 hours, newer accounts that only created issues beyond the first 100 would be missed.

Consider using pagination similar to the PR listing pattern to ensure all recent issues are checked.

Suggested change
const { data: recentIssues } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'all',
since: cutoff.toISOString(),
per_page: 100,
});
const recentIssues = await github.paginate(
github.rest.issues.listForRepo,
{
owner: context.repo.owner,
repo: context.repo.repo,
state: 'all',
since: cutoff.toISOString(),
per_page: 100,
}
);

Copilot uses AI. Check for mistakes.

for (const issue of recentIssues) {
await ensureUserCreatedDate(issue.user?.login);
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The github.rest.issues.listForRepo API returns both issues and pull requests. Since PR creators are already being checked at line 82, this code will redundantly check PR creators again. Additionally, this could cause incorrect counting in the accountsSeen metric since the same user would be processed twice (once as PR creator, once via listForRepo).

Consider filtering out pull requests by checking for the pull_request property, similar to the pattern used at line 199:

for (const issue of recentIssues) {
  if (!issue.pull_request) {  // Only process actual issues, not PRs
    await ensureUserCreatedDate(issue.user?.login);
  }
}
Suggested change
await ensureUserCreatedDate(issue.user?.login);
// github.rest.issues.listForRepo returns both issues and PRs; skip PRs here
if (!issue.pull_request && issue.user?.login) {
await ensureUserCreatedDate(issue.user.login);
}

Copilot uses AI. Check for mistakes.
}
} catch (e) {
// If issue listing fails, continue with PR/comment-based detection.
}

// Identify high-risk accounts
const now = new Date();
for (const [login, createdDate] of userCreatedDates) {
Expand All @@ -146,7 +174,14 @@ jobs:
}

if (highRiskAccounts.size === 0) {
appendSummary(`✅ Bot Detection: no new accounts (<${MIN_ACCOUNT_AGE_DAYS}d) found in last ${HOURS_BACK}h.`);
let msg = `✅ Bot Detection: no high-risk accounts (<${MIN_ACCOUNT_AGE_DAYS}d) detected in last ${HOURS_BACK}h.`;
if (accountsSeen.size > 0) {
msg += ` Checked ${accountsSeen.size} unique account(s).`;
}
if (userLookupFailures > 0) {
msg += ` ⚠️ ${userLookupFailures} account lookup(s) failed.`;
}
appendSummary(msg);
return;
}

Expand Down