1+ import { readFile } from 'node:fs/promises' ;
2+
3+ const CONFIG = {
4+ FILE : 'MEMBERS.md' ,
5+ HEADER : '## Node.js Website Team (`@nodejs/nodejs-website`)' ,
6+ INACTIVE_MONTHS : 12 ,
7+ ISSUE_TITLE : 'Inactive Collaborator Report' ,
8+ ISSUE_LABELS : [ 'meta' , 'inactive-collaborator-report' ] ,
9+ } ;
10+
11+ // Get date N months ago in YYYY-MM-DD format
12+ const getDateMonthsAgo = ( months = CONFIG . INACTIVE_MONTHS ) => {
13+ const date = new Date ( ) ;
14+ date . setMonth ( date . getMonth ( ) - months ) ;
15+ return date . toISOString ( ) . split ( 'T' ) [ 0 ] ;
16+ } ;
17+
18+ // Check if there's already an open issue
19+ async function hasOpenIssue ( github , context ) {
20+ const { owner, repo } = context . repo ;
21+ const { data : issues } = await github . rest . issues . listForRepo ( {
22+ owner,
23+ repo,
24+ state : 'open' ,
25+ labels : CONFIG . ISSUE_LABELS [ 1 ] ,
26+ per_page : 1 ,
27+ } ) ;
28+
29+ return issues . length > 0 ;
30+ }
31+
32+ // Parse collaborator usernames from governance file
33+ async function parseCollaborators ( ) {
34+ const content = await readFile ( CONFIG . FILE , 'utf8' ) ;
35+ const lines = content . split ( '\n' ) ;
36+ const collaborators = [ ] ;
37+
38+ const startIndex =
39+ lines . findIndex ( l => l . startsWith ( CONFIG . HEADER ) ) + 1 ;
40+ if ( startIndex <= 0 ) return collaborators ;
41+
42+ for ( let i = startIndex ; i < lines . length ; i ++ ) {
43+ const line = lines [ i ] ;
44+ if ( line . startsWith ( '#' ) ) break ;
45+
46+ const match = line . match ( / ^ \s * - \s * \[ ( [ ^ \] ] + ) \] / ) ;
47+ if ( match ) collaborators . push ( match [ 1 ] ) ;
48+ }
49+
50+ return collaborators ;
51+ }
52+
53+ // Check if users have been active since cutoff date
54+ async function getInactiveUsers ( github , usernames , repo , cutoffDate ) {
55+ const inactiveUsers = [ ] ;
56+
57+ for ( const username of usernames ) {
58+ // Check commits
59+ const { data : commits } = await github . rest . search . commits ( {
60+ q : `author:${ username } repo:${ repo } committer-date:>=${ cutoffDate } ` ,
61+ per_page : 1 ,
62+ } ) ;
63+
64+ // Check issues and PRs
65+ const { data : issues } = await github . rest . search . issuesAndPullRequests ( {
66+ q : `involves:${ username } repo:${ repo } updated:>=${ cutoffDate } ` ,
67+ per_page : 1 ,
68+ } ) ;
69+
70+ // User is inactive if they have no commits AND no issues/PRs
71+ if ( commits . total_count === 0 && issues . total_count === 0 ) {
72+ inactiveUsers . push ( username ) ;
73+ }
74+ }
75+
76+ return inactiveUsers ;
77+ }
78+
79+ // Generate report for inactive members
80+ function formatReport ( inactiveMembers , cutoffDate ) {
81+ if ( ! inactiveMembers . length ) return null ;
82+
83+ const today = getDateMonthsAgo ( 0 ) ;
84+ return `# Inactive Collaborators Report
85+
86+ Last updated: ${ today }
87+ Checking for inactivity since: ${ cutoffDate }
88+
89+ ## Inactive Collaborators (${ inactiveMembers . length } )
90+
91+ | Login |
92+ | ----- |
93+ ${ inactiveMembers . map ( m => `| @${ m } |` ) . join ( '\n' ) }
94+
95+ ## What happens next?
96+
97+ @nodejs/nodejs-website should review this list and contact inactive collaborators to confirm their continued interest in participating in the project.` ;
98+ }
99+
100+ async function createIssue ( github , context , report ) {
101+ if ( ! report ) return ;
102+
103+ const { owner, repo } = context . repo ;
104+ await github . rest . issues . create ( {
105+ owner,
106+ repo,
107+ title : CONFIG . ISSUE_TITLE ,
108+ body : report ,
109+ labels : CONFIG . ISSUE_LABELS ,
110+ } ) ;
111+ }
112+
113+ export default async function ( github , context ) {
114+ // Check for existing open issue first - exit early if one exists
115+ if ( await hasOpenIssue ( github , context ) ) {
116+ return ;
117+ }
118+
119+ const cutoffDate = getDateMonthsAgo ( ) ;
120+ const collaborators = await parseCollaborators ( ) ;
121+
122+ const inactiveMembers = await getInactiveUsers (
123+ github ,
124+ collaborators ,
125+ `${ context . repo . owner } /${ context . repo . repo } ` ,
126+ cutoffDate
127+ ) ;
128+ const report = formatReport ( inactiveMembers , cutoffDate ) ;
129+
130+ await createIssue ( github , context , report ) ;
131+ }
0 commit comments