Support rendering of proposal tables
parent
e88a18ca5d
commit
643cdd19c8
@ -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 }}
|
@ -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);
|
||||
}
|
Loading…
Reference in New Issue