Skip to content

Commit e521d91

Browse files
committed
feat(meta): require collaborators to me active
1 parent 39e3ae1 commit e521d91

File tree

4 files changed

+502
-1
lines changed

4 files changed

+502
-1
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Inactive Collaborators Report
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
# Run once a week on Monday at 00:00 UTC
7+
- cron: '0 0 * * 1'
8+
9+
jobs:
10+
check-inactive-collaborators:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: read
14+
issues: write
15+
16+
steps:
17+
- name: Harden Runner
18+
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
19+
with:
20+
egress-policy: audit
21+
22+
- name: Checkout repository
23+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
24+
25+
- name: Create inactive collaborators report
26+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
27+
with:
28+
github-token: ${{ secrets.READ_ONLY_ORG_TOKEN }}
29+
script: |
30+
const { reportInactiveCollaborators } = await import("./apps/site/scripts/find-inactive-members.mjs");
31+
32+
await reportInactiveCollaborators(core, github);

CONTRIBUTING.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ Thank you for your interest in contributing to the Node.js Website. Before you p
44

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

5556
</details>
5657

58+
### Maintaining Collaborator Status
59+
60+
Once you become a collaborator, you are expected to uphold certain responsibilities and standards to maintain your status:
61+
62+
- **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.
63+
64+
- **Remain Active**: Collaborators are expected to interact with the repository at least once in the past twelve months. This can include:
65+
- Reviewing pull requests
66+
- Opening or commenting on issues
67+
- Contributing commits
68+
69+
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.
70+
71+
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.
72+
5773
## Getting started
5874

5975
The steps below will give you a general idea of how to prepare your local environment for the Node.js Website and general steps
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import assert from 'node:assert/strict';
2+
import { beforeEach, describe, it, mock } from 'node:test';
3+
4+
import {
5+
findInactiveMembers,
6+
isActiveMember,
7+
getDateMonthsAgo,
8+
reportInactiveCollaborators,
9+
createOrUpdateInactiveCollaboratorsIssue,
10+
findInactiveCollaboratorsIssue,
11+
formatIssueBody,
12+
} from '../index.mjs';
13+
14+
// Test constants
15+
const MOCK_DATE = new Date('2025-05-23T14:33:31Z');
16+
const CUTOFF_DATE = '2024-05-23';
17+
const TEST_MEMBERS = [
18+
{ login: 'active-user' },
19+
{ login: 'inactive-user' },
20+
{ login: 'active-user-issues' },
21+
];
22+
23+
describe('Inactive Collaborators Tests', () => {
24+
let core, github;
25+
26+
mock.timers.enable({ apis: ['Date'], now: MOCK_DATE });
27+
28+
beforeEach(() => {
29+
// Simplified mocks
30+
const logs = [],
31+
warnings = [];
32+
core = {
33+
info: msg => logs.push(msg),
34+
warning: msg => warnings.push(msg),
35+
getLogs: () => [...logs],
36+
getWarnings: () => [...warnings],
37+
clearLogs: () => {
38+
logs.length = 0;
39+
},
40+
};
41+
42+
github = {
43+
rest: {
44+
search: {
45+
commits: async ({ q }) => ({
46+
data: {
47+
total_count: q.includes('author:active-user') ? 5 : 0,
48+
items: q.includes('author:active-user')
49+
? [{ sha: 'abc123' }]
50+
: [],
51+
},
52+
}),
53+
issuesAndPullRequests: async ({ q }) => ({
54+
data: {
55+
total_count: q.includes('involves:active-user-issues') ? 3 : 0,
56+
items: q.includes('involves:active-user-issues')
57+
? [{ number: 123 }]
58+
: [],
59+
},
60+
}),
61+
},
62+
teams: {
63+
listMembersInOrg: async () => ({ data: TEST_MEMBERS }),
64+
},
65+
issues: {
66+
listForRepo: async ({ repo }) => ({
67+
data:
68+
repo === 'repo-with-issue'
69+
? [
70+
{
71+
number: 42,
72+
title: 'Inactive Collaborators Report',
73+
body: 'Previous report',
74+
},
75+
]
76+
: [],
77+
}),
78+
create: async ({ title, body }) => ({
79+
data: { number: 99, title, body },
80+
}),
81+
update: async ({ issue_number, body }) => ({
82+
data: { number: issue_number, body },
83+
}),
84+
},
85+
},
86+
};
87+
});
88+
89+
describe('Utilities and core functionality', () => {
90+
it('correctly formats dates with different month offsets', () => {
91+
assert.equal(getDateMonthsAgo(12), CUTOFF_DATE);
92+
assert.equal(getDateMonthsAgo(0), '2025-05-23');
93+
assert.equal(getDateMonthsAgo(6), '2024-11-23');
94+
});
95+
96+
it('correctly identifies active and inactive users', async () => {
97+
assert.equal(
98+
await isActiveMember('active-user', CUTOFF_DATE, github),
99+
true
100+
);
101+
assert.equal(
102+
await isActiveMember('active-user-issues', CUTOFF_DATE, github),
103+
true
104+
);
105+
assert.equal(
106+
await isActiveMember('inactive-user', CUTOFF_DATE, github),
107+
false
108+
);
109+
});
110+
111+
it('finds inactive members from the team list', async () => {
112+
const inactiveMembers = await findInactiveMembers(
113+
TEST_MEMBERS,
114+
core,
115+
github
116+
);
117+
118+
assert.partialDeepStrictEqual(inactiveMembers, [
119+
{ login: 'inactive-user' },
120+
]);
121+
});
122+
});
123+
124+
describe('Issue management', () => {
125+
it('formats issue body correctly', () => {
126+
const inactiveMembers = [
127+
{
128+
login: 'inactive-user',
129+
inactive_since: CUTOFF_DATE,
130+
},
131+
];
132+
133+
const body = formatIssueBody(inactiveMembers, CUTOFF_DATE);
134+
135+
assert.ok(body.includes('# Inactive Collaborators Report'));
136+
assert.ok(body.includes('## Inactive Collaborators (1)'));
137+
assert.ok(body.includes('@inactive-user'));
138+
});
139+
140+
it('handles empty inactive members list', () => {
141+
const body = formatIssueBody([], CUTOFF_DATE);
142+
assert.ok(body.includes('No inactive collaborators were found'));
143+
assert.ok(!body.includes('| Login |'));
144+
});
145+
146+
it('manages issue creation and updates', async () => {
147+
const inactiveMembers = [
148+
{ login: 'inactive-user', inactive_since: CUTOFF_DATE },
149+
];
150+
151+
// Test finding issues
152+
const existingIssue = await findInactiveCollaboratorsIssue(
153+
github,
154+
'nodejs',
155+
'repo-with-issue'
156+
);
157+
const nonExistingIssue = await findInactiveCollaboratorsIssue(
158+
github,
159+
'nodejs',
160+
'repo-without-issue'
161+
);
162+
163+
assert.equal(existingIssue?.number, 42);
164+
assert.equal(nonExistingIssue, null);
165+
166+
// Test updating existing issues
167+
const updatedIssueNum = await createOrUpdateInactiveCollaboratorsIssue({
168+
github,
169+
core,
170+
org: 'nodejs',
171+
repo: 'repo-with-issue',
172+
inactiveMembers,
173+
cutoffDate: CUTOFF_DATE,
174+
});
175+
assert.equal(updatedIssueNum, 42);
176+
177+
// Test creating new issues
178+
const newIssueNum = await createOrUpdateInactiveCollaboratorsIssue({
179+
github,
180+
core,
181+
org: 'nodejs',
182+
repo: 'repo-without-issue',
183+
inactiveMembers,
184+
cutoffDate: CUTOFF_DATE,
185+
});
186+
assert.equal(newIssueNum, 99);
187+
});
188+
});
189+
190+
describe('Complete workflow', () => {
191+
it('correctly executes the full report generation workflow', async () => {
192+
await reportInactiveCollaborators(core, github, {
193+
org: 'nodejs',
194+
teamSlug: 'team',
195+
repo: 'repo',
196+
monthsInactive: 12,
197+
});
198+
199+
const logs = core.getLogs();
200+
assert.ok(
201+
logs.some(log => log.includes('Checking inactive collaborators'))
202+
);
203+
assert.ok(
204+
logs.some(log =>
205+
log.includes('Inactive collaborators report available at:')
206+
)
207+
);
208+
});
209+
210+
it('uses default parameters when not specified', async () => {
211+
const customGithub = {
212+
...github,
213+
rest: {
214+
...github.rest,
215+
teams: {
216+
listMembersInOrg: async ({ org, team_slug }) => {
217+
assert.equal(org, 'nodejs');
218+
assert.equal(team_slug, 'nodejs-website');
219+
return { data: [] };
220+
},
221+
},
222+
},
223+
};
224+
225+
await reportInactiveCollaborators(core, customGithub);
226+
});
227+
});
228+
});

0 commit comments

Comments
 (0)