1
1
#!/usr/bin/env node
2
2
3
- // Identify inactive TSC members.
3
+ // Identify inactive TSC voting members.
4
4
5
5
// 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.
10
8
11
9
import cp from 'node:child_process' ;
12
10
import fs from 'node:fs' ;
@@ -20,9 +18,8 @@ const args = parseArgs({
20
18
} ) ;
21
19
22
20
const verbose = args . values . verbose ;
23
- const SINCE = args . positionals [ 0 ] || '3 months ago' ;
24
21
25
- async function runGitCommand ( cmd , options = { } ) {
22
+ async function runShellCommand ( cmd , options = { } ) {
26
23
const childProcess = cp . spawn ( '/bin/sh' , [ '-c' , cmd ] , {
27
24
cwd : options . cwd ?? new URL ( '..' , import . meta. url ) ,
28
25
encoding : 'utf8' ,
@@ -34,17 +31,14 @@ async function runGitCommand(cmd, options = {}) {
34
31
const errorHandler = new Promise (
35
32
( _ , reject ) => childProcess . on ( 'error' , reject ) ,
36
33
) ;
37
- let returnValue = options . mapFn ? new Set ( ) : '' ;
34
+ let returnValue = options . returnAsArray ? [ ] : '' ;
38
35
await Promise . race ( [ errorHandler , Promise . resolve ( ) ] ) ;
39
36
// If no mapFn, return the value. If there is a mapFn, use it to make a Set to
40
37
// return.
41
38
for await ( const line of lines ) {
42
39
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 ) ;
48
42
} else {
49
43
returnValue += line ;
50
44
}
@@ -60,6 +54,13 @@ async function getTscFromReadme() {
60
54
const returnedArray = [ ] ;
61
55
let foundTscHeading = false ;
62
56
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
+
63
64
// If we've found the TSC heading already, stop processing at the next
64
65
// heading.
65
66
if ( foundTscHeading && line . startsWith ( '#' ) ) {
@@ -84,36 +85,6 @@ async function getTscFromReadme() {
84
85
return returnedArray ;
85
86
}
86
87
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
-
117
88
async function getVotingRecords ( tscMembers , votes ) {
118
89
const votingRecords = { } ;
119
90
for ( const member of tscMembers ) {
@@ -122,7 +93,7 @@ async function getVotingRecords(tscMembers, votes) {
122
93
for ( const vote of votes ) {
123
94
// Get the vote data.
124
95
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' ) ,
126
97
) ;
127
98
for ( const member in voteData . votes ) {
128
99
if ( tscMembers . includes ( member ) ) {
@@ -133,22 +104,22 @@ async function getVotingRecords(tscMembers, votes) {
133
104
return votingRecords ;
134
105
}
135
106
136
- async function moveTscToEmeritus ( peopleToMove ) {
107
+ async function moveVotingToRegular ( peopleToMove ) {
137
108
const readmeText = readline . createInterface ( {
138
109
input : fs . createReadStream ( new URL ( '../README.md' , import . meta. url ) ) ,
139
110
crlfDelay : Infinity ,
140
111
} ) ;
141
112
let fileContents = '' ;
142
- let inTscSection = false ;
143
- let inTscEmeritusSection = false ;
113
+ let inTscVotingSection = false ;
114
+ let inTscRegularSection = false ;
144
115
let memberFirstLine = '' ;
145
116
const textToMove = [ ] ;
146
117
let moveToInactive = false ;
147
118
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
149
120
// the list, print out the remaining entries to be moved because they come
150
121
// alphabetically after the last item.
151
- if ( inTscEmeritusSection && line === '' &&
122
+ if ( inTscRegularSection && line === '' &&
152
123
fileContents . endsWith ( '>\n' ) ) {
153
124
while ( textToMove . length ) {
154
125
fileContents += textToMove . pop ( ) ;
@@ -158,21 +129,21 @@ async function moveTscToEmeritus(peopleToMove) {
158
129
// If we've found the TSC heading already, stop processing at the
159
130
// next heading.
160
131
if ( line . startsWith ( '#' ) ) {
161
- inTscSection = false ;
162
- inTscEmeritusSection = false ;
132
+ inTscVotingSection = false ;
133
+ inTscRegularSection = false ;
163
134
}
164
135
165
- const isTsc = inTscSection && line . length ;
166
- const isTscEmeritus = inTscEmeritusSection && line . length ;
136
+ const isTscVoting = inTscVotingSection && line . length ;
137
+ const isTscRegular = inTscRegularSection && line . length ;
167
138
168
- if ( line === '### TSC (Technical Steering Committee) ' ) {
169
- inTscSection = true ;
139
+ if ( line === '#### TSC voting members ' ) {
140
+ inTscVotingSection = true ;
170
141
}
171
- if ( line === '### TSC emeriti ' ) {
172
- inTscEmeritusSection = true ;
142
+ if ( line === '#### TSC regular members ' ) {
143
+ inTscRegularSection = true ;
173
144
}
174
145
175
- if ( isTsc ) {
146
+ if ( isTscVoting ) {
176
147
if ( line . startsWith ( '* ' ) ) {
177
148
memberFirstLine = line ;
178
149
const match = line . match ( / ^ \* \[ ( [ ^ \] ] + ) / ) ;
@@ -191,7 +162,7 @@ async function moveTscToEmeritus(peopleToMove) {
191
162
}
192
163
}
193
164
194
- if ( isTscEmeritus ) {
165
+ if ( isTscRegular ) {
195
166
if ( line . startsWith ( '* ' ) ) {
196
167
memberFirstLine = line ;
197
168
} else if ( line . startsWith ( ' **' ) ) {
@@ -207,79 +178,62 @@ async function moveTscToEmeritus(peopleToMove) {
207
178
}
208
179
}
209
180
210
- if ( ! isTsc && ! isTscEmeritus ) {
181
+ if ( ! isTscVoting && ! isTscRegular ) {
211
182
fileContents += `${ line } \n` ;
212
183
}
213
184
}
214
185
215
186
return fileContents ;
216
187
}
217
188
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.
221
193
const tscMembersAtEnd = await getTscFromReadme ( ) ;
222
194
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.
234
196
// 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 } ,
238
200
) ;
239
201
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 ( ) ;
245
204
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 ) ,
251
213
) ;
252
214
253
215
// Check voting record.
254
216
const votingRecords = await getVotingRecords ( tscMembers , votes ) ;
255
- const noVotes = tscMembers . filter (
217
+ const inactive = tscMembers . filter (
256
218
( member ) => votingRecords [ member ] === 0 ,
257
219
) ;
258
220
259
- const inactive = lightAttendance . filter ( ( member ) => noVotes . includes ( member ) ) ;
260
-
261
221
if ( inactive . length ) {
262
222
// The stdout output is consumed in find-inactive-tsc.yml. If format of output
263
223
// changes, find-inactive-tsc.yml may need to be updated.
264
224
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 } ` ) ;
272
227
273
228
if ( process . env . GITHUB_ACTIONS ) {
274
229
// Using console.warn() to avoid messing with find-inactive-tsc which
275
230
// consumes stdout.
276
231
console . warn ( 'Generating new README.md file...' ) ;
277
- const newReadmeText = await moveTscToEmeritus ( inactive ) ;
232
+ const newReadmeText = await moveVotingToRegular ( inactive ) ;
278
233
fs . writeFileSync ( new URL ( '../README.md' , import . meta. url ) , newReadmeText ) ;
279
234
}
280
235
}
281
236
282
237
if ( verbose ) {
283
- console . log ( attendance ) ;
284
238
console . log ( votingRecords ) ;
285
239
}
0 commit comments