Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ const CONFIG = {
const getDateMonthsAgo = (months = CONFIG.INACTIVE_MONTHS) => {
const date = new Date();
date.setMonth(date.getMonth() - months);
return date.toISOString().split("T")[0];
const result = date.toISOString().split("T")[0];
console.log(`getDateMonthsAgo(${months}): ${result}`);
return result;
};

// Check if there's already an open issue
async function hasOpenIssue(github, context) {
console.log("Checking for existing open issues...");
const { owner, repo } = context.repo;
const { data: issues } = await github.rest.issues.listForRepo({
owner,
Expand All @@ -26,34 +29,52 @@ async function hasOpenIssue(github, context) {
per_page: 1,
});

return issues.length > 0;
const hasIssue = issues.length > 0;
console.log(`Found ${issues.length} open issues with label "${CONFIG.ISSUE_LABELS[1]}"`);
return hasIssue;
}

// Parse collaborator usernames from governance file
async function parseCollaborators() {
console.log(`Reading collaborators from ${CONFIG.FILE}...`);
const content = await readFile(CONFIG.FILE, "utf8");
const lines = content.split("\n");
const collaborators = [];

const startIndex = lines.findIndex((l) => l.startsWith(CONFIG.HEADER)) + 1;
if (startIndex <= 0) return collaborators;
console.log(`Header found at line ${startIndex - 1}, starting parse at line ${startIndex}`);

if (startIndex <= 0) {
console.log("Header not found, returning empty collaborators array");
return collaborators;
}

for (let i = startIndex; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith("#")) break;
if (line.startsWith("#")) {
console.log(`Reached next section at line ${i}, stopping parse`);
break;
}

const match = line.match(/^\s*-\s*\[([^\]]+)\]/);
if (match) collaborators.push(match[1]);
if (match) {
collaborators.push(match[1].slice(1));
console.log(`Found collaborator: ${match[1]}`);
}
}

console.log(`Total collaborators found: ${collaborators.length}`);
return collaborators;
}

// Check if users have been active since cutoff date
async function getInactiveUsers(github, usernames, repo, cutoffDate) {
console.log(`Checking activity for ${usernames.length} users since ${cutoffDate}...`);
const inactiveUsers = [];

for (const username of usernames) {
console.log(`Checking activity for ${username}...`);

// Check commits
const { data: commits } = await github.rest.search.commits({
q: `author:${username} repo:${repo} committer-date:>=${cutoffDate}`,
Expand All @@ -62,25 +83,36 @@ async function getInactiveUsers(github, usernames, repo, cutoffDate) {

// Check issues and PRs
const { data: issues } = await github.rest.search.issuesAndPullRequests({
q: `involves:${username} repo:${repo} updated:>=${cutoffDate}`,
q: `commenter:${username} repo:${repo} updated:>=${cutoffDate}`,
per_page: 1,
});

console.log(`${username}: ${commits.total_count} commits, ${issues.total_count} issues/PRs`);

// User is inactive if they have no commits AND no issues/PRs
if (commits.total_count === 0 && issues.total_count === 0) {
inactiveUsers.push(username);
console.log(`${username} is inactive`);
} else {
console.log(`${username} is active`);
}
}

console.log(`Total inactive users: ${inactiveUsers.length}`);
return inactiveUsers;
}

// Generate report for inactive members
function formatReport(inactiveMembers, cutoffDate) {
if (!inactiveMembers.length) return null;
console.log(`Formatting report for ${inactiveMembers.length} inactive members...`);

if (!inactiveMembers.length) {
console.log("No inactive members found, skipping report generation");
return null;
}

const today = getDateMonthsAgo(0);
return `# Inactive Collaborators Report
const report = `# Inactive Collaborators Report

Last updated: ${today}
Checking for inactivity since: ${cutoffDate}
Expand All @@ -94,11 +126,18 @@ ${inactiveMembers.map((m) => `| @${m} |`).join("\n")}
## What happens next?

@nodejs/nodejs-website should review this list and contact inactive collaborators to confirm their continued interest in participating in the project.`;

console.log("Report generated successfully");
return report;
}

async function createIssue(github, context, report) {
if (!report) return;
if (!report) {
console.log("No report to create issue from");
return;
}

console.log("Creating new issue...");
const { owner, repo } = context.repo;
await github.rest.issues.create({
owner,
Expand All @@ -107,17 +146,26 @@ async function createIssue(github, context, report) {
body: report,
labels: CONFIG.ISSUE_LABELS,
});
console.log("Issue created successfully");
}

export default async function (github, context) {
console.log("Starting inactive collaborator check...");

// Check for existing open issue first - exit early if one exists
if (await hasOpenIssue(github, context)) {
console.log("Open issue already exists, exiting early");
return;
}

const cutoffDate = getDateMonthsAgo();
const collaborators = await parseCollaborators();

if (collaborators.length === 0) {
console.log("No collaborators found, exiting");
return;
}

const inactiveMembers = await getInactiveUsers(
github,
collaborators,
Expand All @@ -127,4 +175,5 @@ export default async function (github, context) {
const report = formatReport(inactiveMembers, cutoffDate);

await createIssue(github, context, report);
console.log("Inactive collaborator check completed");
}
2 changes: 1 addition & 1 deletion .github/workflows/inactive-collaborator-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ jobs:
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const { default: report } = await import("${{github.workspace}}/.github/scripts/inactive-collaborators-report.mjs");
const { default: report } = await import("${{github.workspace}}/.github/scripts/inactive-collaborator-report.mjs");
report(github, context);
Loading