11#!/usr/bin/env node
22
3- // Identify inactive TSC members.
3+ // Identify inactive TSC voting members.
44
55// From the TSC Charter:
6- // A TSC member is automatically removed from the TSC if, during a 3-month
7- // period, all of the following are true:
8- // * They attend fewer than 25% of the regularly scheduled meetings.
9- // * They do not participate in any TSC votes.
6+ // A TSC voting member is automatically converted to a TSC regular member if
7+ // they do not participate in three consecutive TSC votes.
108
119import cp from 'node:child_process' ;
1210import fs from 'node:fs' ;
@@ -20,9 +18,8 @@ const args = parseArgs({
2018} ) ;
2119
2220const verbose = args . values . verbose ;
23- const SINCE = args . positionals [ 0 ] || '3 months ago' ;
2421
25- async function runGitCommand ( cmd , options = { } ) {
22+ async function runShellCommand ( cmd , options = { } ) {
2623 const childProcess = cp . spawn ( '/bin/sh' , [ '-c' , cmd ] , {
2724 cwd : options . cwd ?? new URL ( '..' , import . meta. url ) ,
2825 encoding : 'utf8' ,
@@ -34,17 +31,14 @@ async function runGitCommand(cmd, options = {}) {
3431 const errorHandler = new Promise (
3532 ( _ , reject ) => childProcess . on ( 'error' , reject ) ,
3633 ) ;
37- let returnValue = options . mapFn ? new Set ( ) : '' ;
34+ let returnValue = options . returnAsArray ? [ ] : '' ;
3835 await Promise . race ( [ errorHandler , Promise . resolve ( ) ] ) ;
3936 // If no mapFn, return the value. If there is a mapFn, use it to make a Set to
4037 // return.
4138 for await ( const line of lines ) {
4239 await Promise . race ( [ errorHandler , Promise . resolve ( ) ] ) ;
43- if ( options . mapFn ) {
44- const val = options . mapFn ( line ) ;
45- if ( val ) {
46- returnValue . add ( val ) ;
47- }
40+ if ( options . returnAsArray ) {
41+ returnValue . push ( line ) ;
4842 } else {
4943 returnValue += line ;
5044 }
@@ -60,6 +54,13 @@ async function getTscFromReadme() {
6054 const returnedArray = [ ] ;
6155 let foundTscHeading = false ;
6256 for await ( const line of readmeText ) {
57+ // Until three votes have passed from March 16, 2023, we will need this.
58+ // After that point, we can use this for setting `foundTscHeading` below
59+ // and remove this.
60+ if ( line === '#### TSC voting members' ) {
61+ continue ;
62+ }
63+
6364 // If we've found the TSC heading already, stop processing at the next
6465 // heading.
6566 if ( foundTscHeading && line . startsWith ( '#' ) ) {
@@ -84,36 +85,6 @@ async function getTscFromReadme() {
8485 return returnedArray ;
8586}
8687
87- async function getAttendance ( tscMembers , meetings ) {
88- const attendance = { } ;
89- for ( const member of tscMembers ) {
90- attendance [ member ] = 0 ;
91- }
92- for ( const meeting of meetings ) {
93- // Get the file contents.
94- const meetingFile =
95- await fs . promises . readFile ( path . join ( '.tmp' , meeting ) , 'utf8' ) ;
96- // Extract the attendee list.
97- const startMarker = '## Present' ;
98- const start = meetingFile . indexOf ( startMarker ) + startMarker . length ;
99- const end = meetingFile . indexOf ( '## Agenda' ) ;
100- meetingFile . substring ( start , end ) . trim ( ) . split ( '\n' )
101- . map ( ( line ) => {
102- const match = line . match ( / @ ( \S + ) / ) ;
103- if ( match ) {
104- return match [ 1 ] ;
105- }
106- // Using `console.warn` so that stdout output is not generated.
107- // The stdout output is consumed in find-inactive-tsc.yml.
108- console . warn ( `Attendee entry does not contain GitHub handle: ${ line } ` ) ;
109- return '' ;
110- } )
111- . filter ( ( handle ) => tscMembers . includes ( handle ) )
112- . forEach ( ( handle ) => { attendance [ handle ] ++ ; } ) ;
113- }
114- return attendance ;
115- }
116-
11788async function getVotingRecords ( tscMembers , votes ) {
11889 const votingRecords = { } ;
11990 for ( const member of tscMembers ) {
@@ -122,7 +93,7 @@ async function getVotingRecords(tscMembers, votes) {
12293 for ( const vote of votes ) {
12394 // Get the vote data.
12495 const voteData = JSON . parse (
125- await fs . promises . readFile ( path . join ( '.tmp' , vote ) , 'utf8' ) ,
96+ await fs . promises . readFile ( path . join ( '.tmp/votes ' , vote ) , 'utf8' ) ,
12697 ) ;
12798 for ( const member in voteData . votes ) {
12899 if ( tscMembers . includes ( member ) ) {
@@ -133,22 +104,22 @@ async function getVotingRecords(tscMembers, votes) {
133104 return votingRecords ;
134105}
135106
136- async function moveTscToEmeritus ( peopleToMove ) {
107+ async function moveVotingToRegular ( peopleToMove ) {
137108 const readmeText = readline . createInterface ( {
138109 input : fs . createReadStream ( new URL ( '../README.md' , import . meta. url ) ) ,
139110 crlfDelay : Infinity ,
140111 } ) ;
141112 let fileContents = '' ;
142- let inTscSection = false ;
143- let inTscEmeritusSection = false ;
113+ let inTscVotingSection = false ;
114+ let inTscRegularSection = false ;
144115 let memberFirstLine = '' ;
145116 const textToMove = [ ] ;
146117 let moveToInactive = false ;
147118 for await ( const line of readmeText ) {
148- // If we've been processing TSC emeriti and we reach the end of
119+ // If we've been processing TSC regular members and we reach the end of
149120 // the list, print out the remaining entries to be moved because they come
150121 // alphabetically after the last item.
151- if ( inTscEmeritusSection && line === '' &&
122+ if ( inTscRegularSection && line === '' &&
152123 fileContents . endsWith ( '>\n' ) ) {
153124 while ( textToMove . length ) {
154125 fileContents += textToMove . pop ( ) ;
@@ -158,21 +129,21 @@ async function moveTscToEmeritus(peopleToMove) {
158129 // If we've found the TSC heading already, stop processing at the
159130 // next heading.
160131 if ( line . startsWith ( '#' ) ) {
161- inTscSection = false ;
162- inTscEmeritusSection = false ;
132+ inTscVotingSection = false ;
133+ inTscRegularSection = false ;
163134 }
164135
165- const isTsc = inTscSection && line . length ;
166- const isTscEmeritus = inTscEmeritusSection && line . length ;
136+ const isTscVoting = inTscVotingSection && line . length ;
137+ const isTscRegular = inTscRegularSection && line . length ;
167138
168- if ( line === '### TSC (Technical Steering Committee) ' ) {
169- inTscSection = true ;
139+ if ( line === '#### TSC voting members ' ) {
140+ inTscVotingSection = true ;
170141 }
171- if ( line === '### TSC emeriti ' ) {
172- inTscEmeritusSection = true ;
142+ if ( line === '#### TSC regular members ' ) {
143+ inTscRegularSection = true ;
173144 }
174145
175- if ( isTsc ) {
146+ if ( isTscVoting ) {
176147 if ( line . startsWith ( '* ' ) ) {
177148 memberFirstLine = line ;
178149 const match = line . match ( / ^ \* \[ ( [ ^ \] ] + ) / ) ;
@@ -191,7 +162,7 @@ async function moveTscToEmeritus(peopleToMove) {
191162 }
192163 }
193164
194- if ( isTscEmeritus ) {
165+ if ( isTscRegular ) {
195166 if ( line . startsWith ( '* ' ) ) {
196167 memberFirstLine = line ;
197168 } else if ( line . startsWith ( ' **' ) ) {
@@ -207,79 +178,62 @@ async function moveTscToEmeritus(peopleToMove) {
207178 }
208179 }
209180
210- if ( ! isTsc && ! isTscEmeritus ) {
181+ if ( ! isTscVoting && ! isTscRegular ) {
211182 fileContents += `${ line } \n` ;
212183 }
213184 }
214185
215186 return fileContents ;
216187}
217188
218- // Get current TSC members, then get TSC members at start of period. Only check
219- // TSC members who are on both lists. This way, we don't flag someone who has
220- // only been on the TSC for a week and therefore hasn't attended any meetings.
189+ // Get current TSC voting members, then get TSC voting members at start of
190+ // period. Only check TSC voting members who are on both lists. This way, we
191+ // don't flag someone who hasn't been on the TSC long enough to have missed 3
192+ // consecutive votes.
221193const tscMembersAtEnd = await getTscFromReadme ( ) ;
222194
223- const startCommit = await runGitCommand ( `git rev-list -1 --before '${ SINCE } ' HEAD` ) ;
224- await runGitCommand ( `git checkout ${ startCommit } -- README.md` ) ;
225- const tscMembersAtStart = await getTscFromReadme ( ) ;
226- await runGitCommand ( 'git reset HEAD README.md' ) ;
227- await runGitCommand ( 'git checkout -- README.md' ) ;
228-
229- const tscMembers = tscMembersAtEnd . filter (
230- ( memberAtEnd ) => tscMembersAtStart . includes ( memberAtEnd ) ,
231- ) ;
232-
233- // Get all meetings since SINCE.
195+ // Get the last three votes.
234196// Assumes that the TSC repo is cloned in the .tmp dir.
235- const meetings = await runGitCommand (
236- `git whatchanged --since ' ${ SINCE } ' --name-only --pretty=format: meetings` ,
237- { cwd : '.tmp' , mapFn : ( line ) => line } ,
197+ const votes = await runShellCommand (
198+ 'ls *.json | sort -rn | head -3' ,
199+ { cwd : '.tmp/votes ' , returnAsArray : true } ,
238200) ;
239201
240- // Get TSC meeting attendance.
241- const attendance = await getAttendance ( tscMembers , meetings ) ;
242- const lightAttendance = tscMembers . filter (
243- ( member ) => attendance [ member ] < meetings . size * 0.25 ,
244- ) ;
202+ // Reverse the votes list so the oldest of the three votes is first.
203+ votes . reverse ( ) ;
245204
246- // Get all votes since SINCE.
247- // Assumes that the TSC repo is cloned in the .tmp dir.
248- const votes = await runGitCommand (
249- `git whatchanged --since '${ SINCE } ' --name-only --pretty=format: votes/*.json` ,
250- { cwd : '.tmp' , mapFn : ( line ) => line } ,
205+ const startCommit = await runShellCommand ( `git rev-list -1 --before '${ votes [ 0 ] } ' HEAD` ) ;
206+ await runShellCommand ( `git checkout ${ startCommit } -- README.md` ) ;
207+ const tscMembersAtStart = await getTscFromReadme ( ) ;
208+ await runShellCommand ( 'git reset HEAD README.md' ) ;
209+ await runShellCommand ( 'git checkout -- README.md' ) ;
210+
211+ const tscMembers = tscMembersAtEnd . filter (
212+ ( memberAtEnd ) => tscMembersAtStart . includes ( memberAtEnd ) ,
251213) ;
252214
253215// Check voting record.
254216const votingRecords = await getVotingRecords ( tscMembers , votes ) ;
255- const noVotes = tscMembers . filter (
217+ const inactive = tscMembers . filter (
256218 ( member ) => votingRecords [ member ] === 0 ,
257219) ;
258220
259- const inactive = lightAttendance . filter ( ( member ) => noVotes . includes ( member ) ) ;
260-
261221if ( inactive . length ) {
262222 // The stdout output is consumed in find-inactive-tsc.yml. If format of output
263223 // changes, find-inactive-tsc.yml may need to be updated.
264224 console . log ( `INACTIVE_TSC_HANDLES=${ inactive . map ( ( entry ) => '@' + entry ) . join ( ' ' ) } ` ) ;
265- const commitDetails = inactive . map ( ( entry ) => {
266- let details = `Since ${ SINCE } , ` ;
267- details += `${ entry } attended ${ attendance [ entry ] } out of ${ meetings . size } meetings` ;
268- details += ` and voted in ${ votingRecords [ entry ] } of ${ votes . size } votes.` ;
269- return details ;
270- } ) ;
271- console . log ( `DETAILS_FOR_COMMIT_BODY=${ commitDetails . join ( ' ' ) } ` ) ;
225+ const commitDetails = `${ inactive . join ( ' ' ) } did not participate in three consecutive TSC votes: ${ votes . join ( ' ' ) } ` ;
226+ console . log ( `DETAILS_FOR_COMMIT_BODY=${ commitDetails } ` ) ;
272227
273228 if ( process . env . GITHUB_ACTIONS ) {
274229 // Using console.warn() to avoid messing with find-inactive-tsc which
275230 // consumes stdout.
276231 console . warn ( 'Generating new README.md file...' ) ;
277- const newReadmeText = await moveTscToEmeritus ( inactive ) ;
232+ const newReadmeText = await moveVotingToRegular ( inactive ) ;
278233 fs . writeFileSync ( new URL ( '../README.md' , import . meta. url ) , newReadmeText ) ;
279234 }
280235}
281236
282237if ( verbose ) {
283- console . log ( attendance ) ;
284238 console . log ( votingRecords ) ;
285239}
0 commit comments