forked from tc39/ecma426
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request tc39#15 from nicolo-ribaudo/ipr-check-action
Add IPR check CI job
- Loading branch information
Showing
3 changed files
with
198 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
name: 'ecma-262' | ||
|
||
on: | ||
pull_request_target: | ||
branches: | ||
- '*' | ||
push: | ||
branches: | ||
- main | ||
|
||
jobs: | ||
build: | ||
name: 'check IPR form' | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v3 | ||
- uses: actions/setup-node@v3 | ||
- run: node scripts/ipr-check.js tc39/source-map-spec main | ||
env: | ||
GH_TOKEN: ${{ secrets.GH_IPR_TOKEN }} | ||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
.venv | ||
source-map.html | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
#!/usr/bin/env node | ||
|
||
// Copied from https://github.com/tc39/ecma262/blob/c316ec72f6e227d88d4ef67a3e16b486c5ca356d/scripts/check-form.js, | ||
// with some simplifications (we do not need to handle IPR for some legacy commits) | ||
// - none of the current committers still need to sign the IPR form | ||
// - none of the current committers use multiple GitHub accounts to commit to this repository | ||
|
||
const { execSync } = require('child_process'); | ||
|
||
// web URL: `https://docs.google.com/spreadsheets/d/${sheetID}/edit` | ||
const sheetID = '1if5bU0aV5MJ27GGKnRzyAozeKP-ILXYl5r3dzvkGFmg'; | ||
|
||
const { | ||
GOOGLE_API_KEY: key, // TC39 API key for google sheets | ||
GH_TOKEN | ||
} = process.env; | ||
|
||
if (!GH_TOKEN) { | ||
throw 'GH_TOKEN env var required'; | ||
} | ||
if (!key) { | ||
throw 'GOOGLE_API_KEY env var required'; | ||
} | ||
|
||
const sheetData = `https://sheets.googleapis.com/v4/spreadsheets/${sheetID}/values/Sheet1!A2:A?key=${key}`; | ||
|
||
const [,, slug, branch] = process.argv; | ||
|
||
if (!slug || !branch) { | ||
throw 'args required: slug, branch'; | ||
} | ||
|
||
const sha = String(execSync(`git rev-parse --short ${branch}`)).trim(); | ||
|
||
const request = async (url, method = 'GET', postData) => { | ||
// adapted from https://medium.com/@gevorggalstyan/how-to-promisify-node-js-http-https-requests-76a5a58ed90c | ||
const lib = url.startsWith('https://') ? require('https') : require('http'); | ||
|
||
const [h, path] = url.split('://')[1].split('/'); | ||
const [host, port] = h.split(':'); | ||
|
||
const params = { | ||
host, | ||
port: port || url.startsWith('https://') ? 443 : 80, | ||
method, | ||
headers: { | ||
Authorization: `token ${GH_TOKEN}`, | ||
'User-Agent': 'curl/7.54.0' | ||
} | ||
}; | ||
|
||
return new Promise((resolve, reject) => { | ||
const req = lib.request(url, params, res => { | ||
if (res.statusCode < 200 || res.statusCode >= 300) { | ||
return reject(new Error(`Status Code: ${res.statusCode}; ${url}`)); | ||
} | ||
|
||
const data = []; | ||
|
||
res.on('data', chunk => { | ||
data.push(chunk); | ||
}); | ||
|
||
res.on('end', () => resolve(String(Buffer.concat(data)))); | ||
}); | ||
|
||
req.on('error', reject); | ||
|
||
if (postData) { | ||
req.write(postData); | ||
} | ||
|
||
req.end(); | ||
}); | ||
}; | ||
|
||
const perPage = 100; | ||
|
||
function getAuthorFromCommit(commitObj) { | ||
if (!commitObj) { | ||
return false; | ||
} | ||
const { author } = commitObj; | ||
if (!author) { | ||
throw new Error("Missing author for commit " + commitObj.sha); | ||
} | ||
return author.login; | ||
} | ||
|
||
async function getAllCommits(page = 1) { | ||
const commitsURL = `https://api.github.com/repos/${slug}/commits?anon=1&per_page=${perPage}&page=${page}&sha=${sha}`; | ||
const commits = await request(commitsURL).then((json) => JSON.parse(json)); | ||
return [...new Set([].concat( | ||
commits.flatMap(x => getAuthorFromCommit(x) || []), | ||
commits.length < perPage ? [] : await getAllCommits(page + 1), | ||
))]; | ||
} | ||
|
||
const authors = getAllCommits().then((authors) => { | ||
const knowns = authors.filter(x => typeof x === 'string'); | ||
console.log(`Found ${knowns.length} authors: ${knowns.join(',')}\n`); | ||
return knowns; | ||
}); | ||
|
||
const teamURL = (team) => `https://api.github.com/orgs/tc39/teams/${team}`; | ||
|
||
function getMembers(teamID, page = 1) { | ||
const memberURL = `https://api.github.com/teams/${teamID}/members?per_page=100&page=${page}`; | ||
const data = request(memberURL).then((json) => JSON.parse(json)); | ||
return data.then((data) => { | ||
if (data.length === 0) { | ||
return data; | ||
} | ||
return getMembers(teamID, page + 1).then(nextPage => { | ||
return data.concat(nextPage); | ||
}); | ||
}); | ||
} | ||
|
||
function handler(kind) { | ||
return (data) => { | ||
const names = new Set(data.map(x => x.login.toLowerCase())); | ||
console.log(`Found ${names.size} ${kind}: ${[...names].join(',')}\n`); | ||
return names; | ||
} | ||
} | ||
|
||
const delegates = request(teamURL('delegates')).then((json) => JSON.parse(json)).then(data => { | ||
return getMembers(data.id); | ||
}).then(handler('delegates')); | ||
|
||
const emeriti = request(teamURL('emeriti')).then((json) => JSON.parse(json)).then(data => { | ||
return getMembers(data.id); | ||
}).then(handler('emeriti')); | ||
|
||
const usernames = request(sheetData).then((json) => JSON.parse(json)).then(data => { | ||
if (!Array.isArray(data.values)) { | ||
throw 'invalid data'; | ||
} | ||
const usernames = new Set( | ||
data.values | ||
.flat(1) | ||
.map(x => x.replace(/^(https?:\/\/)?github\.com\//, '').replace(/^@/, '').toLowerCase()) | ||
.filter(x => /^[a-z0-9_-]{1,39}$/gi.test(x)) | ||
.sort((a, b) => a.localeCompare(b)) | ||
); | ||
console.log(`Found ${usernames.size} usernames: ` + [...usernames].join(',') + '\n'); | ||
return usernames; | ||
}); | ||
|
||
const exceptions = new Set([ | ||
'jaro-sevcik', // Google empolyee | ||
'jkrems', // Google employee | ||
'josephschorr', // former Google empolyee | ||
'sideshowbarker', // Mozilla employee | ||
].map(x => x.toLowerCase())); | ||
|
||
Promise.all([usernames, authors, delegates, emeriti]).then(([usernames, authors, delegates, emeriti]) => { | ||
const missing = authors.filter(author => { | ||
const a = author.toLowerCase(); | ||
const signed = usernames.has(a) | ||
|| delegates.has(a) | ||
|| emeriti.has(a) | ||
|| exceptions.has(a); | ||
return !signed; | ||
}); | ||
if (missing.length > 0) { | ||
throw `Missing ${missing.length} authors: ${missing}`; | ||
} else { | ||
console.log('All authors have signed the form, or are delegates or emeriti!'); | ||
} | ||
}).catch((e) => { | ||
console.error(e); | ||
process.exitCode = 1; | ||
}); |