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