2021-11-21 15:10:04 +00:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
2023-03-16 17:51:32 +00:00
|
|
|
// Identify inactive TSC voting members.
|
2021-11-21 15:10:04 +00:00
|
|
|
|
|
|
|
// From the TSC Charter:
|
2023-03-16 17:51:32 +00:00
|
|
|
// A TSC voting member is automatically converted to a TSC regular member if
|
|
|
|
// they do not participate in three consecutive TSC votes.
|
2021-11-21 15:10:04 +00:00
|
|
|
|
|
|
|
import cp from 'node:child_process';
|
|
|
|
import fs from 'node:fs';
|
|
|
|
import path from 'node:path';
|
|
|
|
import readline from 'node:readline';
|
2022-07-22 17:20:54 +00:00
|
|
|
import { parseArgs } from 'node:util';
|
2021-11-21 15:10:04 +00:00
|
|
|
|
2022-07-22 17:20:54 +00:00
|
|
|
const args = parseArgs({
|
|
|
|
allowPositionals: true,
|
2022-12-18 16:39:39 +00:00
|
|
|
options: { verbose: { type: 'boolean', short: 'v' } },
|
2022-07-22 17:20:54 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
const verbose = args.values.verbose;
|
2021-11-21 15:10:04 +00:00
|
|
|
|
2023-03-16 17:51:32 +00:00
|
|
|
async function runShellCommand(cmd, options = {}) {
|
2021-11-21 15:10:04 +00:00
|
|
|
const childProcess = cp.spawn('/bin/sh', ['-c', cmd], {
|
|
|
|
cwd: options.cwd ?? new URL('..', import.meta.url),
|
|
|
|
encoding: 'utf8',
|
|
|
|
stdio: ['inherit', 'pipe', 'inherit'],
|
|
|
|
});
|
|
|
|
const lines = readline.createInterface({
|
|
|
|
input: childProcess.stdout,
|
|
|
|
});
|
|
|
|
const errorHandler = new Promise(
|
2022-12-18 16:39:39 +00:00
|
|
|
(_, reject) => childProcess.on('error', reject),
|
2021-11-21 15:10:04 +00:00
|
|
|
);
|
2023-03-16 17:51:32 +00:00
|
|
|
let returnValue = options.returnAsArray ? [] : '';
|
2021-11-21 15:10:04 +00:00
|
|
|
await Promise.race([errorHandler, Promise.resolve()]);
|
|
|
|
// If no mapFn, return the value. If there is a mapFn, use it to make a Set to
|
|
|
|
// return.
|
|
|
|
for await (const line of lines) {
|
|
|
|
await Promise.race([errorHandler, Promise.resolve()]);
|
2023-03-16 17:51:32 +00:00
|
|
|
if (options.returnAsArray) {
|
|
|
|
returnValue.push(line);
|
2021-11-21 15:10:04 +00:00
|
|
|
} else {
|
|
|
|
returnValue += line;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return Promise.race([errorHandler, Promise.resolve(returnValue)]);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getTscFromReadme() {
|
|
|
|
const readmeText = readline.createInterface({
|
|
|
|
input: fs.createReadStream(new URL('../README.md', import.meta.url)),
|
|
|
|
crlfDelay: Infinity,
|
|
|
|
});
|
|
|
|
const returnedArray = [];
|
|
|
|
let foundTscHeading = false;
|
|
|
|
for await (const line of readmeText) {
|
2023-03-16 17:51:32 +00:00
|
|
|
// Until three votes have passed from March 16, 2023, we will need this.
|
|
|
|
// After that point, we can use this for setting `foundTscHeading` below
|
|
|
|
// and remove this.
|
|
|
|
if (line === '#### TSC voting members') {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-11-21 15:10:04 +00:00
|
|
|
// If we've found the TSC heading already, stop processing at the next
|
|
|
|
// heading.
|
|
|
|
if (foundTscHeading && line.startsWith('#')) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
const isTsc = foundTscHeading && line.length;
|
|
|
|
|
|
|
|
if (line === '### TSC (Technical Steering Committee)') {
|
|
|
|
foundTscHeading = true;
|
|
|
|
}
|
|
|
|
if (line.startsWith('* ') && isTsc) {
|
|
|
|
const handle = line.match(/^\* \[([^\]]+)]/)[1];
|
|
|
|
returnedArray.push(handle);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!foundTscHeading) {
|
|
|
|
throw new Error('Could not find TSC section of README');
|
|
|
|
}
|
|
|
|
|
|
|
|
return returnedArray;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getVotingRecords(tscMembers, votes) {
|
|
|
|
const votingRecords = {};
|
|
|
|
for (const member of tscMembers) {
|
|
|
|
votingRecords[member] = 0;
|
|
|
|
}
|
|
|
|
for (const vote of votes) {
|
|
|
|
// Get the vote data.
|
|
|
|
const voteData = JSON.parse(
|
2023-03-16 17:51:32 +00:00
|
|
|
await fs.promises.readFile(path.join('.tmp/votes', vote), 'utf8'),
|
2021-11-21 15:10:04 +00:00
|
|
|
);
|
|
|
|
for (const member in voteData.votes) {
|
2022-01-14 06:22:42 +00:00
|
|
|
if (tscMembers.includes(member)) {
|
|
|
|
votingRecords[member]++;
|
|
|
|
}
|
2021-11-21 15:10:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return votingRecords;
|
|
|
|
}
|
|
|
|
|
2023-03-16 17:51:32 +00:00
|
|
|
async function moveVotingToRegular(peopleToMove) {
|
2021-11-21 15:10:04 +00:00
|
|
|
const readmeText = readline.createInterface({
|
|
|
|
input: fs.createReadStream(new URL('../README.md', import.meta.url)),
|
|
|
|
crlfDelay: Infinity,
|
|
|
|
});
|
|
|
|
let fileContents = '';
|
2023-03-16 17:51:32 +00:00
|
|
|
let inTscVotingSection = false;
|
|
|
|
let inTscRegularSection = false;
|
2021-11-21 15:10:04 +00:00
|
|
|
let memberFirstLine = '';
|
|
|
|
const textToMove = [];
|
|
|
|
let moveToInactive = false;
|
|
|
|
for await (const line of readmeText) {
|
2023-03-16 17:51:32 +00:00
|
|
|
// If we've been processing TSC regular members and we reach the end of
|
2021-11-21 15:10:04 +00:00
|
|
|
// the list, print out the remaining entries to be moved because they come
|
|
|
|
// alphabetically after the last item.
|
2023-03-16 17:51:32 +00:00
|
|
|
if (inTscRegularSection && line === '' &&
|
2021-11-21 15:10:04 +00:00
|
|
|
fileContents.endsWith('>\n')) {
|
|
|
|
while (textToMove.length) {
|
|
|
|
fileContents += textToMove.pop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we've found the TSC heading already, stop processing at the
|
|
|
|
// next heading.
|
|
|
|
if (line.startsWith('#')) {
|
2023-03-16 17:51:32 +00:00
|
|
|
inTscVotingSection = false;
|
|
|
|
inTscRegularSection = false;
|
2021-11-21 15:10:04 +00:00
|
|
|
}
|
|
|
|
|
2023-03-16 17:51:32 +00:00
|
|
|
const isTscVoting = inTscVotingSection && line.length;
|
|
|
|
const isTscRegular = inTscRegularSection && line.length;
|
2021-11-21 15:10:04 +00:00
|
|
|
|
2023-03-16 17:51:32 +00:00
|
|
|
if (line === '#### TSC voting members') {
|
|
|
|
inTscVotingSection = true;
|
2021-11-21 15:10:04 +00:00
|
|
|
}
|
2023-03-16 17:51:32 +00:00
|
|
|
if (line === '#### TSC regular members') {
|
|
|
|
inTscRegularSection = true;
|
2021-11-21 15:10:04 +00:00
|
|
|
}
|
|
|
|
|
2023-03-16 17:51:32 +00:00
|
|
|
if (isTscVoting) {
|
2021-11-21 15:10:04 +00:00
|
|
|
if (line.startsWith('* ')) {
|
|
|
|
memberFirstLine = line;
|
|
|
|
const match = line.match(/^\* \[([^\]]+)/);
|
|
|
|
if (match && peopleToMove.includes(match[1])) {
|
|
|
|
moveToInactive = true;
|
|
|
|
}
|
|
|
|
} else if (line.startsWith(' **')) {
|
|
|
|
if (moveToInactive) {
|
|
|
|
textToMove.push(`${memberFirstLine}\n${line}\n`);
|
|
|
|
moveToInactive = false;
|
|
|
|
} else {
|
|
|
|
fileContents += `${memberFirstLine}\n${line}\n`;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
fileContents += `${line}\n`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-16 17:51:32 +00:00
|
|
|
if (isTscRegular) {
|
2021-11-21 15:10:04 +00:00
|
|
|
if (line.startsWith('* ')) {
|
|
|
|
memberFirstLine = line;
|
|
|
|
} else if (line.startsWith(' **')) {
|
|
|
|
const currentLine = `${memberFirstLine}\n${line}\n`;
|
|
|
|
// If textToMove is empty, this still works because when undefined is
|
|
|
|
// used in a comparison with <, the result is always false.
|
2022-10-28 04:21:20 +00:00
|
|
|
while (textToMove[0]?.toLowerCase() < currentLine.toLowerCase()) {
|
2021-11-21 15:10:04 +00:00
|
|
|
fileContents += textToMove.shift();
|
|
|
|
}
|
|
|
|
fileContents += currentLine;
|
|
|
|
} else {
|
|
|
|
fileContents += `${line}\n`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-16 17:51:32 +00:00
|
|
|
if (!isTscVoting && !isTscRegular) {
|
2021-11-21 15:10:04 +00:00
|
|
|
fileContents += `${line}\n`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return fileContents;
|
|
|
|
}
|
|
|
|
|
2023-03-16 17:51:32 +00:00
|
|
|
// Get current TSC voting members, then get TSC voting members at start of
|
|
|
|
// period. Only check TSC voting members who are on both lists. This way, we
|
|
|
|
// don't flag someone who hasn't been on the TSC long enough to have missed 3
|
|
|
|
// consecutive votes.
|
2021-11-21 15:10:04 +00:00
|
|
|
const tscMembersAtEnd = await getTscFromReadme();
|
|
|
|
|
2023-03-16 17:51:32 +00:00
|
|
|
// Get the last three votes.
|
2021-11-21 15:10:04 +00:00
|
|
|
// Assumes that the TSC repo is cloned in the .tmp dir.
|
2023-03-16 17:51:32 +00:00
|
|
|
const votes = await runShellCommand(
|
|
|
|
'ls *.json | sort -rn | head -3',
|
|
|
|
{ cwd: '.tmp/votes', returnAsArray: true },
|
2021-11-21 15:10:04 +00:00
|
|
|
);
|
|
|
|
|
2023-03-16 17:51:32 +00:00
|
|
|
// Reverse the votes list so the oldest of the three votes is first.
|
|
|
|
votes.reverse();
|
2021-11-21 15:10:04 +00:00
|
|
|
|
2023-03-16 17:51:32 +00:00
|
|
|
const startCommit = await runShellCommand(`git rev-list -1 --before '${votes[0]}' HEAD`);
|
|
|
|
await runShellCommand(`git checkout ${startCommit} -- README.md`);
|
|
|
|
const tscMembersAtStart = await getTscFromReadme();
|
|
|
|
await runShellCommand('git reset HEAD README.md');
|
|
|
|
await runShellCommand('git checkout -- README.md');
|
|
|
|
|
|
|
|
const tscMembers = tscMembersAtEnd.filter(
|
|
|
|
(memberAtEnd) => tscMembersAtStart.includes(memberAtEnd),
|
2021-11-21 15:10:04 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
// Check voting record.
|
|
|
|
const votingRecords = await getVotingRecords(tscMembers, votes);
|
2023-03-16 17:51:32 +00:00
|
|
|
const inactive = tscMembers.filter(
|
2022-12-18 16:39:39 +00:00
|
|
|
(member) => votingRecords[member] === 0,
|
2021-11-21 15:10:04 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
if (inactive.length) {
|
2021-11-24 18:33:04 +00:00
|
|
|
// The stdout output is consumed in find-inactive-tsc.yml. If format of output
|
|
|
|
// changes, find-inactive-tsc.yml may need to be updated.
|
2021-11-25 08:44:55 +00:00
|
|
|
console.log(`INACTIVE_TSC_HANDLES=${inactive.map((entry) => '@' + entry).join(' ')}`);
|
2023-03-16 17:51:32 +00:00
|
|
|
const commitDetails = `${inactive.join(' ')} did not participate in three consecutive TSC votes: ${votes.join(' ')}`;
|
|
|
|
console.log(`DETAILS_FOR_COMMIT_BODY=${commitDetails}`);
|
2021-11-24 18:33:04 +00:00
|
|
|
|
2022-01-10 10:14:31 +00:00
|
|
|
if (process.env.GITHUB_ACTIONS) {
|
|
|
|
// Using console.warn() to avoid messing with find-inactive-tsc which
|
|
|
|
// consumes stdout.
|
|
|
|
console.warn('Generating new README.md file...');
|
2023-03-16 17:51:32 +00:00
|
|
|
const newReadmeText = await moveVotingToRegular(inactive);
|
2022-01-10 10:14:31 +00:00
|
|
|
fs.writeFileSync(new URL('../README.md', import.meta.url), newReadmeText);
|
|
|
|
}
|
2021-11-21 15:10:04 +00:00
|
|
|
}
|
2022-07-22 17:20:54 +00:00
|
|
|
|
|
|
|
if (verbose) {
|
|
|
|
console.log(votingRecords);
|
|
|
|
}
|