Skip to content

feat(meta): require collaborators to be active #7775

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 29, 2025
Merged
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
35 changes: 35 additions & 0 deletions .github/workflows/find-inactive-collaborators.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Find inactive collaborators

on:
schedule:
# Run every Monday at 4:05 AM UTC.
- cron: 5 4 * * 1

workflow_dispatch:

env:
NODE_VERSION: lts/*

permissions: {}

jobs:
find:
if: github.repository == 'nodejs/node'
runs-on: ubuntu-latest

steps:
- name: Harden Runner
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Create inactive collaborators report
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ secrets.READ_ONLY_PUBLIC_REPO_TOKEN }}
script: |
const { reportInactiveCollaborators } = await import("${{github.workspace}}/apps/site/scripts/find-inactive-collaborators/index.mjs");

await reportInactiveCollaborators(core, github);
18 changes: 17 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ Thank you for your interest in contributing to the Node.js Website. Before you p

- [Code of Conduct](https://github.com/nodejs/node/blob/HEAD/CODE_OF_CONDUCT.md)
- [Contributing](#contributing)
- [Becoming a collaborator](#becoming-a-collaborator)
- [Becoming a Collaborator](#becoming-a-collaborator)
- [Maintaining Collaborator Status](#maintaining-collaborator-status)
- [Getting started](#getting-started)
- [CLI Commands](#cli-commands)
- [Cloudflare Deployment](#cloudflare-deployment)
Expand Down Expand Up @@ -54,6 +55,21 @@ If you're an active contributor seeking to become a member, we recommend you con

</details>

### Maintaining Collaborator Status

Once you become a collaborator, you are expected to uphold certain responsibilities and standards to maintain your status:

- **Adhere to Policies**: Collaborators must abide by the [Node.js Moderation Policy](https://github.com/nodejs/admin/blob/HEAD/Moderation-Policy.md) and [Code of Conduct](https://github.com/nodejs/node/blob/HEAD/CODE_OF_CONDUCT.md) at all times.

- **Remain Active**: Collaborators are expected to interact with the repository at least once in the past twelve months. This can include:
- Reviewing pull requests
- Opening or commenting on issues
- Contributing commits

If a collaborator becomes inactive for more than twelve months, they may be removed from the active collaborators list. They can be reinstated upon returning to active participation by going through the full nomination process again.

Violations of the Code of Conduct or Moderation Policy may result in immediate removal of collaborator status, depending on the severity of the violation and the decision of the Technical Steering Committee and/or the OpenJS Foundation.

## Getting started

The steps below will give you a general idea of how to prepare your local environment for the Node.js Website and general steps
Expand Down
226 changes: 226 additions & 0 deletions apps/site/scripts/find-inactive-collaborators/__tests__/index.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import assert from 'node:assert/strict';
import { beforeEach, describe, it, mock } from 'node:test';

import {
findInactiveMembers,
isActiveMember,
getDateMonthsAgo,
reportInactiveCollaborators,
createOrUpdateInactiveCollaboratorsIssue,
findInactiveCollaboratorsIssue,
formatIssueBody,
} from '../index.mjs';

// Test constants
const MOCK_DATE = new Date('2025-05-23T14:33:31Z');
const CUTOFF_DATE = '2024-05-23';
const TEST_MEMBERS = [
{ login: 'active-user' },
{ login: 'inactive-user' },
{ login: 'active-user-issues' },
];

describe('Inactive Collaborators Tests', () => {
let core, github;

mock.timers.enable({ apis: ['Date'], now: MOCK_DATE });

beforeEach(() => {
// Simplified mocks
const logs = [],
warnings = [];
core = {
info: msg => logs.push(msg),
warning: msg => warnings.push(msg),
getLogs: () => [...logs],
getWarnings: () => [...warnings],
clearLogs: () => {
logs.length = 0;
},
};

github = {
rest: {
search: {
commits: async ({ q }) => ({
data: {
total_count: q.includes('author:active-user') ? 5 : 0,
items: q.includes('author:active-user')
? [{ sha: 'abc123' }]
: [],
},
}),
issuesAndPullRequests: async ({ q }) => ({
data: {
total_count: q.includes('involves:active-user-issues') ? 3 : 0,
items: q.includes('involves:active-user-issues')
? [{ number: 123 }]
: [],
},
}),
},
teams: {
listMembersInOrg: async () => ({ data: TEST_MEMBERS }),
},
issues: {
listForRepo: async ({ repo }) => ({
data:
repo === 'repo-with-issue'
? [
{
number: 42,
title: 'Inactive Collaborators Report',
body: 'Previous report',
},
]
: [],
}),
create: async ({ title, body }) => ({
data: { number: 99, title, body },
}),
update: async ({ issue_number, body }) => ({
data: { number: issue_number, body },
}),
},
},
};
});

describe('Utilities and core functionality', () => {
it('correctly formats dates with different month offsets', () => {
assert.equal(getDateMonthsAgo(12), CUTOFF_DATE);
assert.equal(getDateMonthsAgo(0), '2025-05-23');
assert.equal(getDateMonthsAgo(6), '2024-11-23');
});

it('correctly identifies active and inactive users', async () => {
assert.equal(
await isActiveMember('active-user', CUTOFF_DATE, github),
true
);
assert.equal(
await isActiveMember('active-user-issues', CUTOFF_DATE, github),
true
);
assert.equal(
await isActiveMember('inactive-user', CUTOFF_DATE, github),
false
);
});

it('finds inactive members from the team list', async () => {
const inactiveMembers = await findInactiveMembers(
TEST_MEMBERS,
core,
github
);

assert.partialDeepStrictEqual(inactiveMembers, [
{ login: 'inactive-user' },
]);
});
});

describe('Issue management', () => {
it('formats issue body correctly', () => {
const inactiveMembers = [
{
login: 'inactive-user',
inactive_since: CUTOFF_DATE,
},
];

const body = formatIssueBody(inactiveMembers, CUTOFF_DATE);

assert.ok(body.includes('# Inactive Collaborators Report'));
assert.ok(body.includes('## Inactive Collaborators (1)'));
assert.ok(body.includes('@inactive-user'));
});

it('handles empty inactive members list', () => {
assert.ok(!formatIssueBody([], CUTOFF_DATE));
});

it('manages issue creation and updates', async () => {
const inactiveMembers = [
{ login: 'inactive-user', inactive_since: CUTOFF_DATE },
];

// Test finding issues
const existingIssue = await findInactiveCollaboratorsIssue(
github,
'nodejs',
'repo-with-issue'
);
const nonExistingIssue = await findInactiveCollaboratorsIssue(
github,
'nodejs',
'repo-without-issue'
);

assert.equal(existingIssue?.number, 42);
assert.equal(nonExistingIssue, null);

// Test updating existing issues
const updatedIssueNum = await createOrUpdateInactiveCollaboratorsIssue({
github,
core,
org: 'nodejs',
repo: 'repo-with-issue',
inactiveMembers,
cutoffDate: CUTOFF_DATE,
});
assert.equal(updatedIssueNum, 42);

// Test creating new issues
const newIssueNum = await createOrUpdateInactiveCollaboratorsIssue({
github,
core,
org: 'nodejs',
repo: 'repo-without-issue',
inactiveMembers,
cutoffDate: CUTOFF_DATE,
});
assert.equal(newIssueNum, 99);
});
});

describe('Complete workflow', () => {
it('correctly executes the full report generation workflow', async () => {
await reportInactiveCollaborators(core, github, {
org: 'nodejs',
teamSlug: 'team',
repo: 'repo',
monthsInactive: 12,
});

const logs = core.getLogs();
assert.ok(
logs.some(log => log.includes('Checking inactive collaborators'))
);
assert.ok(
logs.some(log =>
log.includes('Inactive collaborators report available at:')
)
);
});

it('uses default parameters when not specified', async () => {
const customGithub = {
...github,
rest: {
...github.rest,
teams: {
listMembersInOrg: async ({ org, team_slug }) => {
assert.equal(org, 'nodejs');
assert.equal(team_slug, 'nodejs-website');
return { data: [] };
},
},
},
};

await reportInactiveCollaborators(core, customGithub);
});
});
});
Loading
Loading