-
Notifications
You must be signed in to change notification settings - Fork 382
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 #3000 from wbamberg/add-proposals-tables
Support rendering of proposal tables
- Loading branch information
Showing
6 changed files
with
348 additions
and
1 deletion.
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 |
---|---|---|
@@ -1,6 +1,7 @@ | ||
/api/node_modules | ||
/assets | ||
/assets.tar.gz | ||
/data/msc | ||
/env* | ||
/node_modules | ||
/resources | ||
|
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
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,81 @@ | ||
{{/* | ||
|
||
This template is used to render tables of MSC proposals. | ||
|
||
It expects there to be a "proposals.json" under /data/msc. | ||
It expects "proposals.json" to contain an array of objects, | ||
one for each MSC state. Each object contains: | ||
* `title`: human-readable title for the state, like "Proposal In Review" | ||
* `label`: the GitHub label used for the state, like "proposal-in-review" | ||
* `proposals`: an array of objects, each of which represents an MSC and contains: | ||
* `number`: GitHub issue number | ||
* `url`: GitHub URL for this issue | ||
* `title`: Issue title | ||
* `created_at`: issue creation date | ||
* `updated_at`: issue last-updated date | ||
* `authors`: array of GitHub user names representing authors of this MSC | ||
* `shepherd`: GitHub user name representing the shepherd of this MSC, or null | ||
* `documentation`: Links to further documentation referenced in the GitHub issue | ||
|
||
This data is scraped from GitHub using the /scripts/proposals.js Node script. | ||
The script is run in CI: so typically if you run a local server the data will | ||
be missing and no tables will be generated. If you do want to see the tables locally, | ||
you can run the script locally: | ||
|
||
npm install | ||
npm run get-proposals | ||
|
||
If this template does find the data, it renders one table for each MSC state, | ||
containing a row for each MSC in that state. | ||
|
||
*/}} | ||
|
||
{{ $states := .Site.Data.msc.proposals }} | ||
|
||
{{ range $states }} | ||
<h3 id="{{.label}}" class="proposal-table-title">{{ .title }}</h3> | ||
{{ if .proposals }} | ||
<table class="msc-table table"> | ||
<thead> | ||
<tr> | ||
<th>MSC</th> | ||
<th>Title</th> | ||
<th>Created at</th> | ||
<th>Updated at</th> | ||
<th>Docs</th> | ||
<th>Author</th> | ||
<th>Shepherd</th> | ||
</tr> | ||
</thead> | ||
|
||
<tbody> | ||
{{ range .proposals }} | ||
|
||
{{ $index := 0 }} | ||
{{ $docs_links_list := slice }} | ||
{{ range .documentation }} | ||
{{ $index = add $index 1 }} | ||
{{ $docs_link := printf "<a href=\"%s\">%d</a>" . $index }} | ||
{{ $docs_links_list = $docs_links_list | append $docs_link }} | ||
{{ end }} | ||
{{ $docs_links := delimit $docs_links_list ", " }} | ||
|
||
{{ $authors_list := apply .authors "printf" "<a href=\"https://github.com/%s\">@%s</a>" "." "." }} | ||
{{ $authors := delimit $authors_list ", " }} | ||
|
||
<tr> | ||
<td><a href="{{ .url }}">{{ .number }}</a></td> | ||
<td>{{ .title }}</td> | ||
<td>{{ .created_at }}</td> | ||
<td>{{ .updated_at }}</td> | ||
<td>{{ with $docs_links }}{{ $docs_links }}{{ end }}</td> | ||
<td>{{ $authors }}</td> | ||
<td>{{ with .shepherd }}<a href="https://github.com/{{ . }}">@{{ . }}</a>{{ end }}</td> | ||
</tr> | ||
{{ end }} | ||
</tbody> | ||
</table> | ||
{{ else }} | ||
<p>No MSCs are currently in this state.</p> | ||
{{ end }} | ||
{{ end }} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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,219 @@ | ||
"use strict"; | ||
|
||
/** | ||
* This Node script fetches MSC proposals and stores them in /data/msc, | ||
* so they can be used by a Hugo template to render summary tables of them | ||
* in the specification. | ||
* | ||
* In detail, it: | ||
* - fetches all GitHub issues from matrix-doc that have the `proposal` label | ||
* - groups them by their state in the MSC process | ||
* - does some light massaging of them so it's easier for the Hugo template to work with them | ||
* - store them at /data/msc | ||
*/ | ||
|
||
// built-in modules | ||
const path = require('path'); | ||
const fs = require('fs'); | ||
|
||
// third-party modules | ||
const fetch = require('node-fetch'); | ||
|
||
// We will write proposals into the /data/msc directory | ||
const outputDir = path.join(__dirname, "../data/msc"); | ||
|
||
/** | ||
* This defines the different proposal lifecycle states. | ||
* Each state has: | ||
* - `label`: a GitHub label used to identify issues in this state | ||
* - `title`: used for things like headings in renderings of the proposals | ||
*/ | ||
const states = [ | ||
{ | ||
label: "proposal-in-review", | ||
title: "Proposal In Review" | ||
}, | ||
{ | ||
label: "proposed-final-comment-period", | ||
title: "Proposed Final Comment Period" | ||
}, | ||
{ | ||
label: "final-comment-period", | ||
title: "Final Comment Period" | ||
}, | ||
{ | ||
label: "finished-final-comment-period", | ||
title: "Finished Final Comment Period" | ||
}, | ||
{ | ||
label: "spec-pr-missing", | ||
title: "Spec PR Missing" | ||
}, | ||
{ | ||
label: "spec-pr-in-review", | ||
title: "Spec PR In Review" | ||
}, | ||
{ | ||
label: "merged", | ||
title: "Merged" | ||
}, | ||
{ | ||
label: "proposal-postoned", | ||
title: "Postponed" | ||
}, | ||
{ | ||
label: "abandoned", | ||
title: "Abandoned" | ||
}, | ||
{ | ||
label: "obsolete", | ||
title: "Obsolete" | ||
} | ||
]; | ||
|
||
let issues = []; | ||
|
||
/** | ||
* Fetch all the MSC proposals from GitHub. | ||
* | ||
* GitHub only lets us fetch 100 items at a time, and it gives us a `link` | ||
* response header containing the URL for the next batch. | ||
* So we will keep fetching until the response doesn't contain the "next" link. | ||
*/ | ||
async function getIssues() { | ||
|
||
/** | ||
* A pretty ugly function to get us the "next" link in the header if there | ||
* was one, or `null` otherwise. | ||
*/ | ||
function getNextLink(header) { | ||
const links = header.split(","); | ||
for (const link of links) { | ||
const linkPieces = link.split(";"); | ||
if (linkPieces[1] == ` rel=\"next\"`) { | ||
const next = linkPieces[0].trim(); | ||
return next.substring(1, next.length-1); | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
let pageLink = "https://api.github.com/repos/matrix-org/matrix-doc/issues?state=all&labels=proposal&per_page=100"; | ||
while (pageLink) { | ||
const response = await fetch(pageLink); | ||
const issuesForPage = await response.json(); | ||
issues = issues.concat(issuesForPage); | ||
const linkHeader = response.headers.get("link"); | ||
pageLink = getNextLink(linkHeader); | ||
} | ||
} | ||
|
||
getIssues().then(processIssues); | ||
|
||
/** | ||
* Rather than just store the complete issue, we'll extract | ||
* only the pieces we need. | ||
* We'll also do some transformation of the issues, jsut because | ||
* it's easier to do in JS than in the template. | ||
*/ | ||
function getProposalFromIssue(issue) { | ||
|
||
/** | ||
* A helper function to fetch the contents of special | ||
* directives in the issue body. | ||
* Looks for a directive in the format: | ||
* `^${directiveName}: (.+?)$`, returning the matched | ||
* part or null if the directive wasn't found. | ||
*/ | ||
function getDirective(directiveName, issue) { | ||
const re = new RegExp(`^${directiveName}: (.+?)$`, "m"); | ||
const found = issue.body.match(re); | ||
return found? found[1]: null; | ||
} | ||
|
||
function getDocumentation(issue) { | ||
const found = getDirective("Documentation", issue); | ||
if (found) { | ||
return found.split(",").map(a => a.trim()); | ||
} else { | ||
return []; | ||
} | ||
} | ||
|
||
function getAuthors(issue) { | ||
const found = getDirective("Author", issue); | ||
if (found) { | ||
return found.split(",").map(a => a.trim().substr(1)); | ||
} else { | ||
return [`${issue.user.login}`]; | ||
} | ||
} | ||
|
||
function getShepherd(issue) { | ||
const found = getDirective("Shepherd", issue); | ||
if (found) { | ||
return found.substr(1); | ||
} else { | ||
return null; | ||
} | ||
} | ||
|
||
return { | ||
number: issue.number, | ||
url: issue.html_url, | ||
title: issue.title, | ||
created_at: issue.created_at.substr(0, 10), | ||
updated_at: issue.updated_at.substr(0, 10), | ||
authors: getAuthors(issue), | ||
shepherd: getShepherd(issue), | ||
documentation: getDocumentation(issue) | ||
} | ||
} | ||
|
||
/** | ||
* Returns the intersection of two arrays. | ||
*/ | ||
function intersection(array1, array2) { | ||
return array1.filter(i => array2.includes(i)); | ||
} | ||
|
||
/** | ||
* Given all the GitHub issues with a "proposal" label: | ||
* - group issues by the state they are in, and for each group: | ||
* - extract the bits we need from each issue | ||
* - write the result under /data/msc | ||
*/ | ||
function processIssues() { | ||
if (!fs.existsSync(outputDir)){ | ||
fs.mkdirSync(outputDir); | ||
} | ||
const output = []; | ||
// make a group of "work in progress" proposals, | ||
// which are identified by not having any of the state labels | ||
const stateLabels = states.map(s => s.label); | ||
const worksInProgress = issues.filter(issue => { | ||
const labelsForIssue = issue.labels.map(l => l.name); | ||
return intersection(labelsForIssue, stateLabels).length === 0; | ||
}); | ||
output.push({ | ||
title: "Work In Progress", | ||
label: "work-in-progress", | ||
proposals: worksInProgress.map(issue => getProposalFromIssue(issue)) | ||
}); | ||
// for each defined state | ||
for (const state of states) { | ||
// get the set of issues for that state | ||
const issuesForState = issues.filter(msc => { | ||
return msc.labels.some(l => l.name === state.label); | ||
}); | ||
// store it in /data | ||
output.push({ | ||
title: state.title, | ||
label: state.label, | ||
proposals: issuesForState.map(issue => getProposalFromIssue(issue)) | ||
}); | ||
} | ||
const outputData = JSON.stringify(output, null, '\t'); | ||
const outputFile = path.join(outputDir, `proposals.json`); | ||
fs.writeFileSync(outputFile, outputData); | ||
} |